A better Rust ATProto crate
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(¶ms);
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(¶ms);
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}