1use super::ir::{
9 AuthoringIR, IRArgument, IREnum, IREnumValue, IRField, IRInputField, IRInputType, IRInterface,
10 IRMutation, IRUnion,
11};
12#[allow(unused_imports)] use crate::error::FraiseQLError;
14use crate::{
15 error::Result,
16 schema::{
17 ArgumentDefinition, AutoParams as SchemaAutoParams, CompiledSchema, CursorType,
18 DeprecationInfo, EnumDefinition, EnumValueDefinition, FieldDefinition, FieldDenyPolicy,
19 FieldType, InputFieldDefinition, InputObjectDefinition, InterfaceDefinition,
20 MutationDefinition, QueryDefinition, SubscriptionDefinition, TypeDefinition,
21 UnionDefinition,
22 domain_types::{SqlSource, TypeName},
23 },
24 validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
25};
26
27pub struct CodeGenerator {
29 optimize: bool,
38}
39
40impl CodeGenerator {
41 #[must_use]
43 pub const fn new(optimize: bool) -> Self {
44 Self { optimize }
45 }
46
47 pub fn generate(&self, ir: &AuthoringIR) -> Result<CompiledSchema> {
117 let types = ir
118 .types
119 .iter()
120 .map(|t| {
121 TypeDefinition {
122 name: TypeName::new(t.name.clone()),
123 sql_source: SqlSource::new(
124 t.sql_source.clone().unwrap_or_else(|| t.name.clone()),
125 ),
126 jsonb_column: "data".to_string(),
127 fields: Self::map_fields(&t.fields),
128 description: t.description.clone(),
129 sql_projection_hint: None, implements: Vec::new(), requires_role: None,
133 is_error: false,
134 relay: false,
135 relationships: Vec::new(),
136 }
137 })
138 .collect();
139
140 let queries = ir
141 .queries
142 .iter()
143 .map(|q| {
144 QueryDefinition {
145 name: q.name.clone(),
146 return_type: q.return_type.clone(),
147 returns_list: q.returns_list,
148 nullable: q.nullable,
149 arguments: Self::map_arguments(&q.arguments),
150 sql_source: q.sql_source.clone(),
151 description: q.description.clone(),
152 auto_params: SchemaAutoParams {
153 has_where: q.auto_params.has_where,
154 has_order_by: q.auto_params.has_order_by,
155 has_limit: q.auto_params.has_limit,
156 has_offset: q.auto_params.has_offset,
157 },
158 deprecation: None, jsonb_column: "data".to_string(), relay: false,
161 relay_cursor_column: None,
162 relay_cursor_type: CursorType::default(),
163 inject_params: indexmap::IndexMap::default(),
164 cache_ttl_seconds: None,
165 additional_views: vec![],
166 requires_role: None,
167 rest_path: None,
168 rest_method: None,
169 }
170 })
171 .collect();
172
173 let mutations = ir.mutations.iter().map(Self::map_mutation).collect();
174
175 let subscriptions = ir
176 .subscriptions
177 .iter()
178 .map(|s| {
179 SubscriptionDefinition {
180 name: s.name.clone(),
181 return_type: s.return_type.clone(),
182 arguments: Self::map_arguments(&s.arguments),
183 description: s.description.clone(),
184 topic: None, filter: None, fields: Vec::new(), filter_fields: Vec::new(), deprecation: None, }
190 })
191 .collect();
192
193 let enums = ir.enums.iter().map(Self::map_enum).collect();
195
196 let interfaces = ir.interfaces.iter().map(Self::map_interface).collect();
198
199 let unions = ir.unions.iter().map(Self::map_union).collect();
201
202 let input_types = ir.input_types.iter().map(Self::map_input_type).collect();
204
205 let custom_scalars = Self::build_custom_type_registry(&ir.scalars)?;
207
208 let mut schema = CompiledSchema {
209 types,
210 enums,
211 input_types,
212 interfaces,
213 unions,
214 queries,
215 mutations,
216 subscriptions,
217 directives: Vec::new(),
221 fact_tables: std::collections::HashMap::new(),
226 observers: Vec::new(),
229 federation: None,
231 security: None,
233 observers_config: None,
235 subscriptions_config: None,
236 validation_config: None,
237 debug_config: None,
238 mcp_config: None,
239 schema_sdl: None,
240 schema_format_version: Some(crate::schema::CURRENT_SCHEMA_FORMAT_VERSION),
242 custom_scalars,
244 ..CompiledSchema::default()
246 };
247 schema.build_indexes();
248 Ok(schema)
249 }
250
251 fn map_fields(ir_fields: &[IRField]) -> Vec<FieldDefinition> {
253 ir_fields
254 .iter()
255 .map(|f| {
256 let field_type = FieldType::parse(&f.field_type);
257 FieldDefinition {
258 name: f.name.clone().into(),
259 field_type,
260 nullable: f.nullable,
261 description: f.description.clone(),
262 default_value: None, vector_config: None, alias: None, deprecation: None, requires_scope: None, on_deny: FieldDenyPolicy::default(),
268 encryption: None,
269 }
270 })
271 .collect()
272 }
273
274 fn map_arguments(ir_args: &[IRArgument]) -> Vec<ArgumentDefinition> {
276 ir_args
277 .iter()
278 .map(|a| {
279 let arg_type = FieldType::parse(&a.arg_type);
280 ArgumentDefinition {
281 name: a.name.clone(),
282 arg_type,
283 nullable: a.nullable,
284 default_value: a.default_value.clone(),
285 description: a.description.clone(),
286 deprecation: None, }
288 })
289 .collect()
290 }
291
292 fn map_mutation(m: &IRMutation) -> MutationDefinition {
294 use super::ir::MutationOperation as IRMutationOp;
295 use crate::schema::MutationOperation;
296
297 let operation = match m.operation {
302 IRMutationOp::Create => MutationOperation::Insert {
303 table: m.return_type.to_lowercase(), },
305 IRMutationOp::Update => MutationOperation::Update {
306 table: m.return_type.to_lowercase(),
307 },
308 IRMutationOp::Delete => MutationOperation::Delete {
309 table: m.return_type.to_lowercase(),
310 },
311 IRMutationOp::Custom => MutationOperation::Custom,
312 };
313
314 let sql_source = match &operation {
315 MutationOperation::Insert { table }
316 | MutationOperation::Update { table }
317 | MutationOperation::Delete { table }
318 if !table.is_empty() =>
319 {
320 Some(table.clone())
321 },
322 _ => None,
323 };
324
325 MutationDefinition {
326 name: m.name.clone(),
327 return_type: m.return_type.clone(),
328 arguments: Self::map_arguments(&m.arguments),
329 description: m.description.clone(),
330 operation,
331 deprecation: None, sql_source,
333 inject_params: indexmap::IndexMap::default(),
334 invalidates_fact_tables: vec![],
335 invalidates_views: vec![],
336 rest_path: None,
337 rest_method: None,
338 upsert_function: None,
339 }
340 }
341
342 fn map_enum(e: &IREnum) -> EnumDefinition {
344 EnumDefinition {
345 name: e.name.clone(),
346 values: e.values.iter().map(Self::map_enum_value).collect(),
347 description: e.description.clone(),
348 }
349 }
350
351 fn map_enum_value(v: &IREnumValue) -> EnumValueDefinition {
353 EnumValueDefinition {
354 name: v.name.clone(),
355 description: v.description.clone(),
356 deprecation: v.deprecation_reason.as_ref().map(|reason| DeprecationInfo {
357 reason: Some(reason.clone()),
358 }),
359 }
360 }
361
362 fn map_interface(i: &IRInterface) -> InterfaceDefinition {
364 InterfaceDefinition {
365 name: i.name.clone(),
366 fields: Self::map_fields(&i.fields),
367 description: i.description.clone(),
368 }
369 }
370
371 fn map_union(u: &IRUnion) -> UnionDefinition {
373 UnionDefinition {
374 name: u.name.clone(),
375 member_types: u.types.clone(),
376 description: u.description.clone(),
377 }
378 }
379
380 fn map_input_type(i: &IRInputType) -> InputObjectDefinition {
382 InputObjectDefinition {
383 name: i.name.clone(),
384 fields: Self::map_input_fields(&i.fields),
385 description: i.description.clone(),
386 metadata: None,
387 }
388 }
389
390 fn map_input_fields(ir_fields: &[IRInputField]) -> Vec<InputFieldDefinition> {
392 ir_fields
393 .iter()
394 .map(|f| {
395 InputFieldDefinition {
396 name: f.name.clone(),
397 field_type: f.field_type.clone(), description: f.description.clone(),
400 default_value: f.default_value.as_ref().map(|v| v.to_json().to_string()),
401 deprecation: None, validation_rules: Vec::new(), }
405 })
406 .collect()
407 }
408
409 fn build_custom_type_registry(
411 ir_scalars: &[super::ir::IRScalar],
412 ) -> Result<CustomTypeRegistry> {
413 let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
414
415 for ir_scalar in ir_scalars {
416 let def = CustomTypeDef {
417 name: ir_scalar.name.clone(),
418 description: ir_scalar.description.clone(),
419 specified_by_url: ir_scalar.specified_by_url.clone(),
420 validation_rules: ir_scalar.validation_rules.clone(),
421 elo_expression: None, base_type: ir_scalar.base_type.clone(),
423 };
424
425 registry.register(ir_scalar.name.clone(), def)?;
426 }
427
428 Ok(registry)
429 }
430
431 #[must_use]
433 pub const fn optimize(&self) -> bool {
434 self.optimize
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 #![allow(clippy::unwrap_used)] use super::{
443 super::ir::{AutoParams, IRArgument, IRField, IRQuery, IRSubscription, IRType},
444 *,
445 };
446 use crate::schema::{CursorType, GraphQLValue};
447
448 #[test]
449 fn test_code_generator_new() {
450 let generator = CodeGenerator::new(true);
451 assert!(generator.optimize());
452
453 let generator = CodeGenerator::new(false);
454 assert!(!generator.optimize());
455 }
456
457 #[test]
458 fn test_generate_empty_schema() {
459 let generator = CodeGenerator::new(true);
460 let ir = AuthoringIR::new();
461
462 let schema = generator
463 .generate(&ir)
464 .unwrap_or_else(|e| panic!("generate empty schema should succeed: {e}"));
465 assert!(schema.types.is_empty());
466 assert!(schema.queries.is_empty());
467 }
468
469 #[test]
470 fn test_generate_types_with_fields() {
471 let generator = CodeGenerator::new(true);
472 let mut ir = AuthoringIR::new();
473
474 ir.types.push(IRType {
475 name: "User".to_string(),
476 fields: vec![
477 IRField {
478 name: "id".to_string(),
479 field_type: "ID!".to_string(),
480 nullable: false,
481 description: Some("User ID".to_string()),
482 sql_column: Some("id".to_string()),
483 },
484 IRField {
485 name: "name".to_string(),
486 field_type: "String".to_string(),
487 nullable: true,
488 description: None,
489 sql_column: None,
490 },
491 IRField {
492 name: "age".to_string(),
493 field_type: "Int".to_string(),
494 nullable: true,
495 description: None,
496 sql_column: None,
497 },
498 ],
499 sql_source: Some("v_user".to_string()),
500 description: Some("User type".to_string()),
501 });
502
503 let schema = generator
504 .generate(&ir)
505 .unwrap_or_else(|e| panic!("generate types with fields should succeed: {e}"));
506 assert_eq!(schema.types.len(), 1);
507
508 let user_type = &schema.types[0];
509 assert_eq!(user_type.name, "User");
510 assert_eq!(user_type.sql_source, "v_user");
511 assert_eq!(user_type.fields.len(), 3);
512
513 assert_eq!(user_type.fields[0].name, "id");
515 assert_eq!(user_type.fields[0].field_type, FieldType::Id);
516 assert!(!user_type.fields[0].nullable);
517
518 assert_eq!(user_type.fields[1].name, "name");
519 assert_eq!(user_type.fields[1].field_type, FieldType::String);
520
521 assert_eq!(user_type.fields[2].name, "age");
522 assert_eq!(user_type.fields[2].field_type, FieldType::Int);
523 }
524
525 #[test]
526 fn test_generate_queries_with_arguments() {
527 let generator = CodeGenerator::new(true);
528 let mut ir = AuthoringIR::new();
529
530 ir.types.push(IRType {
531 name: "User".to_string(),
532 fields: vec![],
533 sql_source: None,
534 description: None,
535 });
536
537 ir.queries.push(IRQuery {
538 name: "user".to_string(),
539 return_type: "User".to_string(),
540 returns_list: false,
541 nullable: true,
542 arguments: vec![IRArgument {
543 name: "id".to_string(),
544 arg_type: "ID!".to_string(),
545 nullable: false,
546 default_value: None,
547 description: Some("User ID to fetch".to_string()),
548 }],
549 sql_source: Some("v_user".to_string()),
550 description: Some("Fetch a single user".to_string()),
551 auto_params: AutoParams::default(),
552 });
553
554 ir.queries.push(IRQuery {
555 name: "users".to_string(),
556 return_type: "User".to_string(),
557 returns_list: true,
558 nullable: false,
559 arguments: vec![IRArgument {
560 name: "limit".to_string(),
561 arg_type: "Int".to_string(),
562 nullable: true,
563 default_value: Some(GraphQLValue::Int(10)),
564 description: None,
565 }],
566 sql_source: Some("v_user".to_string()),
567 description: None,
568 auto_params: AutoParams {
569 has_where: true,
570 has_order_by: true,
571 has_limit: true,
572 has_offset: true,
573 },
574 });
575
576 let schema = generator
577 .generate(&ir)
578 .unwrap_or_else(|e| panic!("generate queries with arguments should succeed: {e}"));
579 assert_eq!(schema.queries.len(), 2);
580
581 let user_query = &schema.queries[0];
583 assert_eq!(user_query.name, "user");
584 assert!(!user_query.returns_list);
585 assert!(user_query.nullable);
586 assert_eq!(user_query.arguments.len(), 1);
587 assert_eq!(user_query.arguments[0].name, "id");
588 assert_eq!(user_query.arguments[0].arg_type, FieldType::Id);
589
590 let users_query = &schema.queries[1];
592 assert_eq!(users_query.name, "users");
593 assert!(users_query.returns_list);
594 assert_eq!(
595 users_query.sql_source.as_deref(),
596 Some("v_user"),
597 "codegen must thread sql_source from IR"
598 );
599 assert!(users_query.auto_params.has_where);
600 assert!(users_query.auto_params.has_order_by);
601 assert_eq!(users_query.arguments[0].default_value, Some(GraphQLValue::Int(10)));
602 assert_eq!(users_query.relay_cursor_type, CursorType::Int64);
603 assert!(users_query.inject_params.is_empty());
604 assert!(users_query.cache_ttl_seconds.is_none());
605 }
606
607 #[test]
608 fn test_generate_mutations() {
609 use super::super::ir::MutationOperation as IRMutationOp;
610
611 let generator = CodeGenerator::new(true);
612 let mut ir = AuthoringIR::new();
613
614 ir.mutations.push(IRMutation {
615 name: "createUser".to_string(),
616 return_type: "User".to_string(),
617 nullable: false,
618 arguments: vec![IRArgument {
619 name: "name".to_string(),
620 arg_type: "String!".to_string(),
621 nullable: false,
622 default_value: None,
623 description: None,
624 }],
625 description: Some("Create a new user".to_string()),
626 operation: IRMutationOp::Create,
627 });
628
629 let schema = generator
630 .generate(&ir)
631 .unwrap_or_else(|e| panic!("generate mutations should succeed: {e}"));
632 assert_eq!(schema.mutations.len(), 1);
633
634 let mutation = &schema.mutations[0];
635 assert_eq!(mutation.name, "createUser");
636 assert!(matches!(
638 &mutation.operation,
639 crate::schema::MutationOperation::Insert { table } if table == "user"
640 ));
641 assert_eq!(
644 mutation.sql_source,
645 Some("user".to_string()),
646 "codegen must thread sql_source from IR"
647 );
648 assert_eq!(mutation.arguments.len(), 1);
649 assert!(mutation.inject_params.is_empty());
650 assert!(mutation.invalidates_fact_tables.is_empty());
651 assert!(mutation.invalidates_views.is_empty());
652 }
653
654 #[test]
655 fn test_generate_subscriptions() {
656 let generator = CodeGenerator::new(true);
657 let mut ir = AuthoringIR::new();
658
659 ir.subscriptions.push(IRSubscription {
660 name: "userCreated".to_string(),
661 return_type: "User".to_string(),
662 arguments: vec![IRArgument {
663 name: "tenantId".to_string(),
664 arg_type: "ID!".to_string(),
665 nullable: false,
666 default_value: None,
667 description: None,
668 }],
669 description: Some("Subscribe to user creation events".to_string()),
670 });
671
672 let schema = generator
673 .generate(&ir)
674 .unwrap_or_else(|e| panic!("generate subscriptions should succeed: {e}"));
675 assert_eq!(schema.subscriptions.len(), 1);
676
677 let subscription = &schema.subscriptions[0];
678 assert_eq!(subscription.name, "userCreated");
679 assert_eq!(subscription.return_type, "User");
680 assert_eq!(subscription.arguments.len(), 1);
681 assert_eq!(subscription.arguments[0].name, "tenantId");
682 }
683
684 #[test]
685 fn test_field_type_parsing_list_types() {
686 let generator = CodeGenerator::new(true);
687 let mut ir = AuthoringIR::new();
688
689 ir.types.push(IRType {
690 name: "Post".to_string(),
691 fields: vec![
692 IRField {
693 name: "tags".to_string(),
694 field_type: "[String]".to_string(),
695 nullable: true,
696 description: None,
697 sql_column: None,
698 },
699 IRField {
700 name: "comments".to_string(),
701 field_type: "[Comment!]!".to_string(),
702 nullable: false,
703 description: None,
704 sql_column: None,
705 },
706 ],
707 sql_source: None,
708 description: None,
709 });
710
711 let schema = generator
712 .generate(&ir)
713 .unwrap_or_else(|e| panic!("generate list type fields should succeed: {e}"));
714 let post_type = &schema.types[0];
715
716 assert!(matches!(
718 &post_type.fields[0].field_type,
719 FieldType::List(inner) if **inner == FieldType::String
720 ));
721
722 assert!(matches!(
724 &post_type.fields[1].field_type,
725 FieldType::List(inner) if matches!(**inner, FieldType::Object(ref name) if name == "Comment")
726 ));
727 }
728
729 #[test]
730 fn test_generate_enums() {
731 use super::super::ir::{IREnum, IREnumValue};
732
733 let generator = CodeGenerator::new(true);
734 let mut ir = AuthoringIR::new();
735
736 ir.enums.push(IREnum {
737 name: "OrderStatus".to_string(),
738 values: vec![
739 IREnumValue {
740 name: "PENDING".to_string(),
741 description: Some("Order is pending".to_string()),
742 deprecation_reason: None,
743 },
744 IREnumValue {
745 name: "COMPLETED".to_string(),
746 description: None,
747 deprecation_reason: None,
748 },
749 IREnumValue {
750 name: "CANCELLED".to_string(),
751 description: None,
752 deprecation_reason: Some("Use REJECTED instead".to_string()),
753 },
754 ],
755 description: Some("Possible order statuses".to_string()),
756 });
757
758 let schema = generator
759 .generate(&ir)
760 .unwrap_or_else(|e| panic!("generate enums should succeed: {e}"));
761 assert_eq!(schema.enums.len(), 1);
762
763 let order_status = &schema.enums[0];
764 assert_eq!(order_status.name, "OrderStatus");
765 assert_eq!(order_status.values.len(), 3);
766 assert_eq!(order_status.values[0].name, "PENDING");
767 assert_eq!(order_status.values[0].description, Some("Order is pending".to_string()));
768 assert!(order_status.values[2].deprecation.is_some());
769 }
770
771 #[test]
772 fn test_generate_interfaces() {
773 use super::super::ir::IRInterface;
774
775 let generator = CodeGenerator::new(true);
776 let mut ir = AuthoringIR::new();
777
778 ir.interfaces.push(IRInterface {
779 name: "Node".to_string(),
780 fields: vec![IRField {
781 name: "id".to_string(),
782 field_type: "ID!".to_string(),
783 nullable: false,
784 description: Some("Unique identifier".to_string()),
785 sql_column: None,
786 }],
787 description: Some("An object with an ID".to_string()),
788 });
789
790 let schema = generator
791 .generate(&ir)
792 .unwrap_or_else(|e| panic!("generate interfaces should succeed: {e}"));
793 assert_eq!(schema.interfaces.len(), 1);
794
795 let node = &schema.interfaces[0];
796 assert_eq!(node.name, "Node");
797 assert_eq!(node.fields.len(), 1);
798 assert_eq!(node.fields[0].name, "id");
799 assert_eq!(node.fields[0].field_type, FieldType::Id);
800 }
801
802 #[test]
803 fn test_generate_unions() {
804 use super::super::ir::IRUnion;
805
806 let generator = CodeGenerator::new(true);
807 let mut ir = AuthoringIR::new();
808
809 ir.unions.push(IRUnion {
810 name: "SearchResult".to_string(),
811 types: vec![
812 "User".to_string(),
813 "Post".to_string(),
814 "Comment".to_string(),
815 ],
816 description: Some("Possible search result types".to_string()),
817 });
818
819 let schema = generator
820 .generate(&ir)
821 .unwrap_or_else(|e| panic!("generate unions should succeed: {e}"));
822 assert_eq!(schema.unions.len(), 1);
823
824 let search_result = &schema.unions[0];
825 assert_eq!(search_result.name, "SearchResult");
826 assert_eq!(search_result.member_types.len(), 3);
827 assert_eq!(search_result.member_types[0], "User");
828 }
829
830 #[test]
831 fn test_generate_input_types() {
832 use super::super::ir::{IRInputField, IRInputType};
833
834 let generator = CodeGenerator::new(true);
835 let mut ir = AuthoringIR::new();
836
837 ir.input_types.push(IRInputType {
838 name: "CreateUserInput".to_string(),
839 fields: vec![
840 IRInputField {
841 name: "name".to_string(),
842 field_type: "String!".to_string(),
843 nullable: false,
844 default_value: None,
845 description: Some("User's name".to_string()),
846 },
847 IRInputField {
848 name: "age".to_string(),
849 field_type: "Int".to_string(),
850 nullable: true,
851 default_value: Some(GraphQLValue::Int(18)),
852 description: None,
853 },
854 ],
855 description: Some("Input for creating a user".to_string()),
856 });
857
858 let schema = generator
859 .generate(&ir)
860 .unwrap_or_else(|e| panic!("generate input types should succeed: {e}"));
861 assert_eq!(schema.input_types.len(), 1);
862
863 let create_user = &schema.input_types[0];
864 assert_eq!(create_user.name, "CreateUserInput");
865 assert_eq!(create_user.fields.len(), 2);
866 assert_eq!(create_user.fields[0].name, "name");
867 assert_eq!(create_user.fields[1].default_value, Some("18".to_string()));
868 }
869
870 #[test]
871 fn test_generate_with_empty_scalars() {
872 let generator = CodeGenerator::new(true);
873 let mut ir = AuthoringIR::new();
874 ir.scalars = vec![];
875
876 let schema = generator
877 .generate(&ir)
878 .unwrap_or_else(|e| panic!("generate with empty scalars should succeed: {e}"));
879 assert_eq!(schema.custom_scalars.count(), 0);
880 }
881
882 #[test]
883 fn test_generate_with_single_custom_scalar() {
884 use super::super::ir::IRScalar;
885
886 let generator = CodeGenerator::new(true);
887 let mut ir = AuthoringIR::new();
888
889 ir.scalars.push(IRScalar {
890 name: "LibraryCode".to_string(),
891 description: Some("Unique library book identifier".to_string()),
892 specified_by_url: None,
893 validation_rules: vec![],
894 base_type: Some("String".to_string()),
895 });
896
897 let schema = generator
898 .generate(&ir)
899 .unwrap_or_else(|e| panic!("generate with single custom scalar should succeed: {e}"));
900 assert_eq!(schema.custom_scalars.count(), 1);
901 assert!(schema.custom_scalars.exists("LibraryCode"));
902
903 let def = schema.custom_scalars.get("LibraryCode").unwrap();
904 assert_eq!(def.name, "LibraryCode");
905 assert_eq!(def.description, Some("Unique library book identifier".to_string()));
906 assert_eq!(def.base_type, Some("String".to_string()));
907 }
908
909 #[test]
910 fn test_generate_with_multiple_custom_scalars() {
911 use super::super::ir::IRScalar;
912
913 let generator = CodeGenerator::new(true);
914 let mut ir = AuthoringIR::new();
915
916 ir.scalars.push(IRScalar {
917 name: "LibraryCode".to_string(),
918 description: None,
919 specified_by_url: None,
920 validation_rules: vec![],
921 base_type: Some("String".to_string()),
922 });
923
924 ir.scalars.push(IRScalar {
925 name: "StudentID".to_string(),
926 description: Some("University student identifier".to_string()),
927 specified_by_url: None,
928 validation_rules: vec![],
929 base_type: None,
930 });
931
932 ir.scalars.push(IRScalar {
933 name: "PatientID".to_string(),
934 description: Some("Hospital patient identifier".to_string()),
935 specified_by_url: Some("https://hl7.org/".to_string()),
936 validation_rules: vec![],
937 base_type: Some("String".to_string()),
938 });
939
940 let schema = generator.generate(&ir).unwrap_or_else(|e| {
941 panic!("generate with multiple custom scalars should succeed: {e}")
942 });
943 assert_eq!(schema.custom_scalars.count(), 3);
944
945 assert!(schema.custom_scalars.exists("LibraryCode"));
947 assert!(schema.custom_scalars.exists("StudentID"));
948 assert!(schema.custom_scalars.exists("PatientID"));
949
950 let patient_id = schema.custom_scalars.get("PatientID").unwrap();
952 assert_eq!(patient_id.specified_by_url, Some("https://hl7.org/".to_string()));
953 }
954
955 #[test]
956 fn test_generate_preserves_scalar_description() {
957 use super::super::ir::IRScalar;
958
959 let generator = CodeGenerator::new(true);
960 let mut ir = AuthoringIR::new();
961
962 ir.scalars.push(IRScalar {
963 name: "Email".to_string(),
964 description: Some("RFC 5322 compliant email address".to_string()),
965 specified_by_url: Some("https://tools.ietf.org/html/rfc5322".to_string()),
966 validation_rules: vec![],
967 base_type: Some("String".to_string()),
968 });
969
970 let schema = generator.generate(&ir).unwrap_or_else(|e| {
971 panic!("generate preserving scalar description should succeed: {e}")
972 });
973 let email_def = schema.custom_scalars.get("Email").unwrap();
974
975 assert_eq!(email_def.description, Some("RFC 5322 compliant email address".to_string()));
976 assert_eq!(
977 email_def.specified_by_url,
978 Some("https://tools.ietf.org/html/rfc5322".to_string())
979 );
980 }
981
982 #[test]
983 fn test_generate_scalars_with_validation_rules() {
984 use super::super::ir::IRScalar;
985 use crate::validation::ValidationRule;
986
987 let generator = CodeGenerator::new(true);
988 let mut ir = AuthoringIR::new();
989
990 ir.scalars.push(IRScalar {
991 name: "StudentID".to_string(),
992 description: None,
993 specified_by_url: None,
994 validation_rules: vec![ValidationRule::Length {
995 min: Some(5),
996 max: Some(15),
997 }],
998 base_type: Some("String".to_string()),
999 });
1000
1001 let schema = generator.generate(&ir).unwrap_or_else(|e| {
1002 panic!("generate scalars with validation rules should succeed: {e}")
1003 });
1004 let student_id = schema.custom_scalars.get("StudentID").unwrap();
1005
1006 assert_eq!(student_id.validation_rules.len(), 1);
1007 }
1008}