A better Rust ATProto crate
at main 550 lines 17 kB view raw
1//! Tests for builder generation 2 3use super::common::generate_common_types; 4use super::state_mod::{RequiredField, collect_required_fields, generate_state_module}; 5use super::{BuilderGenContext, BuilderSchema}; 6use crate::codegen::CodeGenerator; 7use crate::codegen::builder_gen::build_method; 8use crate::corpus::LexiconCorpus; 9use crate::lexicon::{ 10 LexInteger, LexObject, LexObjectProperty, LexString, LexXrpcParameters, 11 LexXrpcParametersProperty, 12}; 13use jacquard_common::smol_str::SmolStr; 14use std::collections::BTreeMap; 15 16#[test] 17fn test_common_types_generation() { 18 let tokens = generate_common_types(); 19 let code = tokens.to_string(); 20 21 // Verify key types are present 22 assert!(code.contains("struct Set")); 23 assert!(code.contains("struct Unset")); 24 assert!(code.contains("trait IsSet")); 25 assert!(code.contains("trait IsUnset")); 26 assert!(code.contains("fn into_inner")); 27 28 // Verify sealed trait pattern 29 assert!(code.contains("mod private")); 30 assert!(code.contains("trait Sealed")); 31 32 // Verify it parses as valid Rust 33 let _parsed: syn::File = syn::parse2(tokens).expect("Generated code should parse"); 34} 35 36// TODO: re-enable these tests once i have time to get them to properly check and not be order-dependent 37// #[test] 38// fn test_collect_required_fields_object() { 39// let obj = LexObject { 40// description: None, 41// required: Some(vec![ 42// SmolStr::new_static("foo"), 43// SmolStr::new_static("barBaz"), 44// ]), 45// nullable: None, 46// properties: Default::default(), 47// }; 48 49// let schema = BuilderSchema::Object(&obj); 50// let fields = collect_required_fields(&schema); 51 52// assert_eq!(fields.len(), 2); 53// assert_eq!(fields[0].name_snake, "foo"); 54// assert_eq!(fields[0].name_pascal, "Foo"); 55// assert_eq!(fields[1].name_snake, "bar_baz"); 56// assert_eq!(fields[1].name_pascal, "BarBaz"); 57// } 58 59// #[test] 60// fn test_collect_required_fields_parameters() { 61// let params = LexXrpcParameters { 62// description: None, 63// required: Some(vec![ 64// SmolStr::new_static("limit"), 65// SmolStr::new_static("cursor"), 66// ]), 67// properties: Default::default(), 68// }; 69 70// let schema = BuilderSchema::Parameters(&params); 71// let fields = collect_required_fields(&schema); 72 73// assert_eq!(fields.len(), 2); 74// assert_eq!(fields[1].name_snake, "limit"); 75// assert_eq!(fields[1].name_pascal, "Limit"); 76// assert_eq!(fields[0].name_snake, "cursor"); 77// assert_eq!(fields[0].name_pascal, "Cursor"); 78// } 79 80#[test] 81fn test_state_module_generation() { 82 let fields = vec![ 83 RequiredField::new("collection"), 84 RequiredField::new("record"), 85 RequiredField::new("repo"), 86 ]; 87 88 let tokens = generate_state_module("CreateRecord", &fields); 89 let code = tokens.to_string(); 90 91 // Verify module structure 92 assert!(code.contains("pub mod create_record_state")); 93 assert!(code.contains("pub trait State")); 94 assert!(code.contains("pub struct Empty")); 95 96 // Verify associated types 97 assert!(code.contains("type Collection")); 98 assert!(code.contains("type Record")); 99 assert!(code.contains("type Repo")); 100 101 // Verify transition types 102 assert!(code.contains("pub struct SetCollection")); 103 assert!(code.contains("pub struct SetRecord")); 104 assert!(code.contains("pub struct SetRepo")); 105 106 // Verify members module 107 assert!(code.contains("pub mod members")); 108 assert!(code.contains("pub struct collection")); 109 assert!(code.contains("pub struct record")); 110 assert!(code.contains("pub struct repo")); 111 112 // Verify sealed trait 113 assert!(code.contains("mod sealed")); 114 115 // Verify it parses as valid Rust 116 let _parsed: syn::File = syn::parse2(tokens).expect("Generated state module should parse"); 117} 118 119#[test] 120fn test_build_method_generation() { 121 let fields = vec![RequiredField::new("repo"), RequiredField::new("collection")]; 122 123 // Create a simple object with required and optional fields 124 let mut properties = BTreeMap::new(); 125 properties.insert( 126 SmolStr::new_static("repo"), 127 LexObjectProperty::String(LexString { 128 description: None, 129 format: None, 130 default: None, 131 min_length: None, 132 max_length: None, 133 min_graphemes: None, 134 max_graphemes: None, 135 r#enum: None, 136 r#const: None, 137 known_values: None, 138 }), 139 ); 140 properties.insert( 141 SmolStr::new_static("collection"), 142 LexObjectProperty::String(LexString { 143 description: None, 144 format: None, 145 default: None, 146 min_length: None, 147 max_length: None, 148 min_graphemes: None, 149 max_graphemes: None, 150 r#enum: None, 151 r#const: None, 152 known_values: None, 153 }), 154 ); 155 properties.insert( 156 SmolStr::new_static("rkey"), 157 LexObjectProperty::String(LexString { 158 description: None, 159 format: None, 160 default: None, 161 min_length: None, 162 max_length: None, 163 min_graphemes: None, 164 max_graphemes: None, 165 r#enum: None, 166 r#const: None, 167 known_values: None, 168 }), 169 ); 170 171 let obj = LexObject { 172 description: None, 173 required: Some(vec![ 174 SmolStr::new_static("repo"), 175 SmolStr::new_static("collection"), 176 ]), 177 nullable: None, 178 properties, 179 }; 180 181 let schema = BuilderSchema::Object(&obj); 182 let tokens = build_method::generate_build_method("CreateRecord", &schema, &fields, true); 183 let code = tokens.to_string(); 184 185 // Verify build method structure 186 assert!(code.contains("pub fn build")); 187 assert!(code.contains("CreateRecord")); 188 assert!(code.contains("create_record_state")); 189 assert!(code.contains("State")); 190 191 // Verify where clauses for required fields (with flexible spacing) 192 assert!(code.contains("Repo") && code.contains("IsSet")); 193 assert!(code.contains("Collection") && code.contains("IsSet")); 194 195 // Verify field extraction from tuple (fields ordered by BTreeMap key order) 196 assert!(code.contains("collection : self . __unsafe_private_named . 0 . unwrap ()")); 197 assert!(code.contains("repo : self . __unsafe_private_named . 1 . unwrap ()")); 198 assert!(code.contains("rkey : self . __unsafe_private_named . 2")); // optional, no unwrap 199 200 // Verify extra_data for LexObject 201 assert!(code.contains("extra_data : Default :: default ()")); 202 203 // Verify it parses as valid Rust 204 let _parsed: syn::File = syn::parse2(tokens).expect("Generated build method should parse"); 205} 206 207#[test] 208fn test_build_method_parameters() { 209 let fields = vec![RequiredField::new("limit")]; 210 211 let mut properties = BTreeMap::new(); 212 properties.insert( 213 SmolStr::new_static("limit"), 214 LexXrpcParametersProperty::Integer(LexInteger { 215 description: None, 216 default: None, 217 minimum: None, 218 maximum: None, 219 r#enum: None, 220 r#const: None, 221 }), 222 ); 223 properties.insert( 224 SmolStr::new_static("cursor"), 225 LexXrpcParametersProperty::String(LexString { 226 description: None, 227 format: None, 228 default: None, 229 min_length: None, 230 max_length: None, 231 min_graphemes: None, 232 max_graphemes: None, 233 r#enum: None, 234 r#const: None, 235 known_values: None, 236 }), 237 ); 238 239 let params = LexXrpcParameters { 240 description: None, 241 required: Some(vec![SmolStr::new_static("limit")]), 242 properties, 243 }; 244 245 let schema = BuilderSchema::Parameters(&params); 246 let tokens = build_method::generate_build_method("QueryParams", &schema, &fields, true); 247 let code = tokens.to_string(); 248 249 // Verify build method structure 250 assert!(code.contains("pub fn build")); 251 assert!(code.contains("QueryParams")); 252 253 // Verify NO extra_data for Parameters 254 assert!(!code.contains("extra_data")); 255 256 // Verify it parses as valid Rust 257 let _parsed: syn::File = syn::parse2(tokens).expect("Generated build method should parse"); 258} 259 260#[test] 261fn test_complete_builder_object() { 262 // Test that all components work together for a LexObject 263 let mut properties = BTreeMap::new(); 264 properties.insert( 265 SmolStr::new_static("repo"), 266 LexObjectProperty::String(LexString { 267 description: None, 268 format: None, 269 default: None, 270 min_length: None, 271 max_length: None, 272 min_graphemes: None, 273 max_graphemes: None, 274 r#enum: None, 275 r#const: None, 276 known_values: None, 277 }), 278 ); 279 properties.insert( 280 SmolStr::new_static("collection"), 281 LexObjectProperty::String(LexString { 282 description: None, 283 format: None, 284 default: None, 285 min_length: None, 286 max_length: None, 287 min_graphemes: None, 288 max_graphemes: None, 289 r#enum: None, 290 r#const: None, 291 known_values: None, 292 }), 293 ); 294 properties.insert( 295 SmolStr::new_static("rkey"), 296 LexObjectProperty::String(LexString { 297 description: None, 298 format: None, 299 default: None, 300 min_length: None, 301 max_length: None, 302 min_graphemes: None, 303 max_graphemes: None, 304 r#enum: None, 305 r#const: None, 306 known_values: None, 307 }), 308 ); 309 310 let obj = LexObject { 311 description: None, 312 required: Some(vec![ 313 SmolStr::new_static("repo"), 314 SmolStr::new_static("collection"), 315 ]), 316 nullable: None, 317 properties, 318 }; 319 320 let schema = BuilderSchema::Object(&obj); 321 let required_fields = collect_required_fields(&schema); 322 323 // Generate all components 324 let state_module = generate_state_module("CreateRecord", &required_fields); 325 let build_method = 326 build_method::generate_build_method("CreateRecord", &schema, &required_fields, true); 327 328 let state_code = state_module.to_string(); 329 let build_code = build_method.to_string(); 330 331 // Verify state module 332 assert!(state_code.contains("pub mod create_record_state")); 333 assert!(state_code.contains("pub trait State")); 334 assert!(state_code.contains("type Repo")); 335 assert!(state_code.contains("type Collection")); 336 assert!(state_code.contains("pub struct SetRepo")); 337 assert!(state_code.contains("pub struct SetCollection")); 338 339 // Verify build method 340 assert!(build_code.contains("pub fn build")); 341 assert!(build_code.contains("-> CreateRecord")); 342 343 // Combine and verify parsing 344 let combined = quote::quote! { 345 #state_module 346 #build_method 347 }; 348 let _parsed: syn::File = 349 syn::parse2(combined).expect("Complete builder components should parse"); 350} 351 352#[test] 353fn test_print_complete_builder() { 354 // Generate and print a complete builder for inspection 355 let mut properties = BTreeMap::new(); 356 properties.insert( 357 SmolStr::new_static("repo"), 358 LexObjectProperty::String(LexString { 359 description: None, 360 format: None, 361 default: None, 362 min_length: None, 363 max_length: None, 364 min_graphemes: None, 365 max_graphemes: None, 366 r#enum: None, 367 r#const: None, 368 known_values: None, 369 }), 370 ); 371 properties.insert( 372 SmolStr::new_static("collection"), 373 LexObjectProperty::String(LexString { 374 description: None, 375 format: None, 376 default: None, 377 min_length: None, 378 max_length: None, 379 min_graphemes: None, 380 max_graphemes: None, 381 r#enum: None, 382 r#const: None, 383 known_values: None, 384 }), 385 ); 386 properties.insert( 387 SmolStr::new_static("rkey"), 388 LexObjectProperty::String(LexString { 389 description: None, 390 format: None, 391 default: None, 392 min_length: None, 393 max_length: None, 394 min_graphemes: None, 395 max_graphemes: None, 396 r#enum: None, 397 r#const: None, 398 known_values: None, 399 }), 400 ); 401 402 let obj = LexObject { 403 description: None, 404 required: Some(vec![ 405 SmolStr::new_static("repo"), 406 SmolStr::new_static("collection"), 407 ]), 408 nullable: None, 409 properties, 410 }; 411 412 let schema = BuilderSchema::Object(&obj); 413 let required_fields = collect_required_fields(&schema); 414 415 // Generate all components 416 let common_types = generate_common_types(); 417 let state_module = generate_state_module("CreateRecord", &required_fields); 418 let build_method = 419 build_method::generate_build_method("CreateRecord", &schema, &required_fields, true); 420 421 // Note: Can't generate builder_struct and setters without CodeGenerator 422 // But we can print what we have 423 424 let combined = quote::quote! { 425 #common_types 426 #state_module 427 #build_method 428 }; 429 430 // Parse and format the code 431 let parsed: syn::File = syn::parse2(combined).expect("Generated code should parse"); 432 let formatted = prettyplease::unparse(&parsed); 433 434 println!("\n\n========== GENERATED BUILDER CODE ==========\n"); 435 println!("{}", formatted); 436 println!("\n========== END GENERATED BUILDER CODE ==========\n\n"); 437} 438 439#[test] 440fn test_print_complete_builder_with_codegen() { 441 // Create a minimal corpus and CodeGenerator for type conversion 442 let corpus = LexiconCorpus::new(); 443 let codegen = CodeGenerator::new(&corpus, "test_crate"); 444 445 // Create a test object with required and optional fields 446 let mut properties = BTreeMap::new(); 447 properties.insert( 448 SmolStr::new_static("repo"), 449 LexObjectProperty::String(LexString { 450 description: None, 451 format: Some(crate::lexicon::LexStringFormat::Did), 452 default: None, 453 min_length: None, 454 max_length: None, 455 min_graphemes: None, 456 max_graphemes: None, 457 r#enum: None, 458 r#const: None, 459 known_values: None, 460 }), 461 ); 462 properties.insert( 463 SmolStr::new_static("collection"), 464 LexObjectProperty::String(LexString { 465 description: None, 466 format: Some(crate::lexicon::LexStringFormat::Nsid), 467 default: None, 468 min_length: None, 469 max_length: None, 470 min_graphemes: None, 471 max_graphemes: None, 472 r#enum: None, 473 r#const: None, 474 known_values: None, 475 }), 476 ); 477 properties.insert( 478 SmolStr::new_static("rkey"), 479 LexObjectProperty::String(LexString { 480 description: None, 481 format: Some(crate::lexicon::LexStringFormat::RecordKey), 482 default: None, 483 min_length: None, 484 max_length: None, 485 min_graphemes: None, 486 max_graphemes: None, 487 r#enum: None, 488 r#const: None, 489 known_values: None, 490 }), 491 ); 492 properties.insert( 493 SmolStr::new_static("validate"), 494 LexObjectProperty::Boolean(crate::lexicon::LexBoolean { 495 description: None, 496 default: None, 497 r#const: None, 498 }), 499 ); 500 501 let obj = LexObject { 502 description: None, 503 required: Some(vec![ 504 SmolStr::new_static("repo"), 505 SmolStr::new_static("collection"), 506 ]), 507 nullable: None, 508 properties, 509 }; 510 511 // Use BuilderGenContext to generate complete builder 512 let ctx = BuilderGenContext::from_object( 513 &codegen, 514 "com.atproto.repo.createRecord", 515 "CreateRecord", 516 &obj, 517 true, 518 ); 519 520 let builder_code = ctx.generate(); 521 522 // Also generate common types 523 let common_types = generate_common_types(); 524 525 // Combine everything 526 let combined = quote::quote! { 527 #common_types 528 #builder_code 529 }; 530 531 // Parse and format the code 532 let parsed: syn::File = syn::parse2(combined).expect("Generated code should parse"); 533 let formatted = prettyplease::unparse(&parsed); 534 535 println!("\n\n========== COMPLETE BUILDER WITH STRUCT AND SETTERS ==========\n"); 536 println!("{}", formatted); 537 println!("\n========== END COMPLETE BUILDER ==========\n\n"); 538 539 // Verify key components are present 540 let code = formatted; 541 assert!(code.contains("pub struct CreateRecordBuilder")); 542 assert!(code.contains("pub mod create_record_state")); 543 assert!(code.contains("pub fn repo(")); 544 assert!(code.contains("pub fn collection(")); 545 assert!(code.contains("pub fn rkey(")); 546 assert!(code.contains("pub fn maybe_rkey(")); 547 assert!(code.contains("pub fn validate(")); 548 assert!(code.contains("pub fn build(self)")); 549 assert!(code.contains("-> CreateRecord")); 550}