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