1use std::collections::HashSet;
6
7use anyhow::{Context, Result};
8use fraiseql_core::schema::{
9 ArgumentDefinition, AutoParams, CompiledSchema, DirectiveDefinition, DirectiveLocationKind,
10 EnumDefinition, EnumValueDefinition, FieldDefinition, FieldType, InputFieldDefinition,
11 InputObjectDefinition, InterfaceDefinition, MutationDefinition, MutationOperation,
12 QueryDefinition, SubscriptionDefinition, SubscriptionFilter, TypeDefinition, UnionDefinition,
13};
14use fraiseql_core::validation::{CustomTypeDef, CustomTypeRegistry};
15use tracing::{info, warn};
16
17use super::{
18 intermediate::{
19 IntermediateArgument, IntermediateAutoParams, IntermediateDirective, IntermediateEnum,
20 IntermediateEnumValue, IntermediateField, IntermediateInputField, IntermediateInputObject,
21 IntermediateInterface, IntermediateMutation, IntermediateQuery, IntermediateSchema,
22 IntermediateScalar, IntermediateSubscription, IntermediateType, IntermediateUnion,
23 },
24 rich_filters::{RichFilterConfig, compile_rich_filters},
25};
26
27pub struct SchemaConverter;
29
30impl SchemaConverter {
31 pub fn convert(intermediate: IntermediateSchema) -> Result<CompiledSchema> {
44 info!("Converting intermediate schema to compiled format");
45
46 let types = intermediate
48 .types
49 .into_iter()
50 .map(Self::convert_type)
51 .collect::<Result<Vec<_>>>()
52 .context("Failed to convert types")?;
53
54 let queries = intermediate
56 .queries
57 .into_iter()
58 .map(Self::convert_query)
59 .collect::<Result<Vec<_>>>()
60 .context("Failed to convert queries")?;
61
62 let mutations = intermediate
64 .mutations
65 .into_iter()
66 .map(Self::convert_mutation)
67 .collect::<Result<Vec<_>>>()
68 .context("Failed to convert mutations")?;
69
70 let enums = intermediate.enums.into_iter().map(Self::convert_enum).collect::<Vec<_>>();
72
73 let input_types = intermediate
75 .input_types
76 .into_iter()
77 .map(Self::convert_input_object)
78 .collect::<Vec<_>>();
79
80 let interfaces = intermediate
82 .interfaces
83 .into_iter()
84 .map(Self::convert_interface)
85 .collect::<Result<Vec<_>>>()
86 .context("Failed to convert interfaces")?;
87
88 let unions = intermediate.unions.into_iter().map(Self::convert_union).collect::<Vec<_>>();
90
91 let subscriptions = intermediate
93 .subscriptions
94 .into_iter()
95 .map(Self::convert_subscription)
96 .collect::<Result<Vec<_>>>()
97 .context("Failed to convert subscriptions")?;
98
99 let directives = intermediate
101 .directives
102 .unwrap_or_default()
103 .into_iter()
104 .map(Self::convert_directive)
105 .collect::<Result<Vec<_>>>()
106 .context("Failed to convert directives")?;
107
108 let fact_tables = intermediate
110 .fact_tables
111 .unwrap_or_default()
112 .into_iter()
113 .map(|ft| {
114 let metadata =
115 serde_json::to_value(&ft).expect("Failed to serialize fact table metadata");
116 (ft.table_name, metadata)
117 })
118 .collect();
119
120 let mut compiled = CompiledSchema {
121 types,
122 enums,
123 input_types,
124 interfaces,
125 unions,
126 queries,
127 mutations,
128 subscriptions,
129 directives,
130 fact_tables, observers: Vec::new(), federation: None, security: intermediate.security, schema_sdl: None, custom_scalars: CustomTypeRegistry::default(), };
138
139 if let Some(custom_scalars_vec) = intermediate.custom_scalars {
141 for scalar_def in custom_scalars_vec {
142 let custom_type = Self::convert_custom_scalar(scalar_def)?;
143 compiled.custom_scalars.register(
144 custom_type.name.clone(),
145 custom_type,
146 ).context("Failed to register custom scalar")?;
147 }
148 }
149
150 let rich_filter_config = RichFilterConfig::default();
152 compile_rich_filters(&mut compiled, &rich_filter_config)
153 .context("Failed to compile rich filter types")?;
154
155 Self::validate(&compiled)?;
157
158 info!("Schema conversion successful");
159 Ok(compiled)
160 }
161
162 fn convert_type(intermediate: IntermediateType) -> Result<TypeDefinition> {
164 let fields = intermediate
165 .fields
166 .into_iter()
167 .map(Self::convert_field)
168 .collect::<Result<Vec<_>>>()
169 .context(format!("Failed to convert type '{}'", intermediate.name))?;
170
171 Ok(TypeDefinition {
172 name: intermediate.name,
173 fields,
174 description: intermediate.description,
175 sql_source: String::new(), jsonb_column: String::new(), sql_projection_hint: None, implements: intermediate.implements,
179 })
180 }
181
182 fn convert_enum(intermediate: IntermediateEnum) -> EnumDefinition {
184 let values = intermediate.values.into_iter().map(Self::convert_enum_value).collect();
185
186 EnumDefinition {
187 name: intermediate.name,
188 values,
189 description: intermediate.description,
190 }
191 }
192
193 fn convert_enum_value(intermediate: IntermediateEnumValue) -> EnumValueDefinition {
195 let deprecation = intermediate
196 .deprecated
197 .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
198
199 EnumValueDefinition {
200 name: intermediate.name,
201 description: intermediate.description,
202 deprecation,
203 }
204 }
205
206 fn convert_custom_scalar(intermediate: IntermediateScalar) -> Result<CustomTypeDef> {
208 Ok(CustomTypeDef {
209 name: intermediate.name,
210 description: intermediate.description,
211 specified_by_url: intermediate.specified_by_url,
212 validation_rules: intermediate.validation_rules,
213 elo_expression: None,
214 base_type: intermediate.base_type,
215 })
216 }
217
218 fn convert_input_object(intermediate: IntermediateInputObject) -> InputObjectDefinition {
220 let fields = intermediate.fields.into_iter().map(Self::convert_input_field).collect();
221
222 InputObjectDefinition {
223 name: intermediate.name,
224 fields,
225 description: intermediate.description,
226 metadata: None,
227 }
228 }
229
230 fn convert_input_field(intermediate: IntermediateInputField) -> InputFieldDefinition {
232 let deprecation = intermediate
233 .deprecated
234 .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
235
236 let default_value = intermediate.default.map(|v| v.to_string());
238
239 InputFieldDefinition {
240 name: intermediate.name,
241 field_type: intermediate.field_type,
242 description: intermediate.description,
243 default_value,
244 deprecation,
245 validation_rules: Vec::new(),
246 }
247 }
248
249 fn convert_interface(intermediate: IntermediateInterface) -> Result<InterfaceDefinition> {
251 let fields = intermediate
252 .fields
253 .into_iter()
254 .map(Self::convert_field)
255 .collect::<Result<Vec<_>>>()
256 .context(format!("Failed to convert interface '{}'", intermediate.name))?;
257
258 Ok(InterfaceDefinition {
259 name: intermediate.name,
260 fields,
261 description: intermediate.description,
262 })
263 }
264
265 fn convert_union(intermediate: IntermediateUnion) -> UnionDefinition {
267 let mut union_def =
268 UnionDefinition::new(&intermediate.name).with_members(intermediate.member_types);
269 if let Some(desc) = intermediate.description {
270 union_def = union_def.with_description(&desc);
271 }
272 union_def
273 }
274
275 fn convert_field(intermediate: IntermediateField) -> Result<FieldDefinition> {
279 let field_type = Self::parse_field_type(&intermediate.field_type)?;
280
281 let deprecation = intermediate.directives.as_ref().and_then(|directives| {
283 directives.iter().find(|d| d.name == "deprecated").map(|d| {
284 let reason = d
285 .arguments
286 .as_ref()
287 .and_then(|args| args.get("reason").and_then(|v| v.as_str()).map(String::from));
288 fraiseql_core::schema::DeprecationInfo { reason }
289 })
290 });
291
292 Ok(FieldDefinition {
293 name: intermediate.name,
294 field_type,
295 nullable: intermediate.nullable,
296 default_value: None,
297 description: intermediate.description,
298 vector_config: None,
299 alias: None,
300 deprecation,
301 requires_scope: intermediate.requires_scope,
302 })
303 }
304
305 fn parse_field_type(type_name: &str) -> Result<FieldType> {
309 match type_name {
310 "String" => Ok(FieldType::String),
311 "Int" => Ok(FieldType::Int),
312 "Float" => Ok(FieldType::Float),
313 "Boolean" => Ok(FieldType::Boolean),
314 "ID" => Ok(FieldType::Id),
315 "DateTime" => Ok(FieldType::DateTime),
316 "Date" => Ok(FieldType::Date),
317 "Time" => Ok(FieldType::Time),
318 "Json" => Ok(FieldType::Json),
319 "UUID" => Ok(FieldType::Uuid),
320 "Decimal" => Ok(FieldType::Decimal),
321 "Vector" => Ok(FieldType::Vector),
322 custom => Ok(FieldType::Object(custom.to_string())),
324 }
325 }
326
327 fn convert_query(intermediate: IntermediateQuery) -> Result<QueryDefinition> {
329 let arguments = intermediate
330 .arguments
331 .into_iter()
332 .map(Self::convert_argument)
333 .collect::<Result<Vec<_>>>()
334 .context(format!("Failed to convert query '{}'", intermediate.name))?;
335
336 let auto_params =
337 intermediate.auto_params.map(Self::convert_auto_params).unwrap_or_default();
338
339 let deprecation = intermediate
340 .deprecated
341 .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
342
343 Ok(QueryDefinition {
344 name: intermediate.name,
345 return_type: intermediate.return_type,
346 returns_list: intermediate.returns_list,
347 nullable: intermediate.nullable,
348 arguments,
349 sql_source: intermediate.sql_source,
350 description: intermediate.description,
351 auto_params,
352 deprecation,
353 jsonb_column: intermediate.jsonb_column.unwrap_or_else(|| "data".to_string()),
354 })
355 }
356
357 fn convert_mutation(intermediate: IntermediateMutation) -> Result<MutationDefinition> {
359 let arguments = intermediate
360 .arguments
361 .into_iter()
362 .map(Self::convert_argument)
363 .collect::<Result<Vec<_>>>()
364 .context(format!("Failed to convert mutation '{}'", intermediate.name))?;
365
366 let operation = Self::parse_mutation_operation(
367 intermediate.operation.as_deref(),
368 intermediate.sql_source.as_deref(),
369 )?;
370
371 let deprecation = intermediate
372 .deprecated
373 .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
374
375 Ok(MutationDefinition {
376 name: intermediate.name,
377 return_type: intermediate.return_type,
378 arguments,
379 description: intermediate.description,
380 operation,
381 deprecation,
382 })
383 }
384
385 fn parse_mutation_operation(
389 operation: Option<&str>,
390 sql_source: Option<&str>,
391 ) -> Result<MutationOperation> {
392 match operation {
393 Some("CREATE" | "INSERT") => {
394 let table = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
396 Ok(MutationOperation::Insert { table })
397 },
398 Some("UPDATE") => {
399 let table = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
400 Ok(MutationOperation::Update { table })
401 },
402 Some("DELETE") => {
403 let table = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
404 Ok(MutationOperation::Delete { table })
405 },
406 Some("FUNCTION") => {
407 let name = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
408 Ok(MutationOperation::Function { name })
409 },
410 Some("CUSTOM") | None => Ok(MutationOperation::Custom),
411 Some(op) => {
412 anyhow::bail!("Unknown mutation operation: {op}")
413 },
414 }
415 }
416
417 fn convert_argument(intermediate: IntermediateArgument) -> Result<ArgumentDefinition> {
419 let arg_type = Self::parse_field_type(&intermediate.arg_type)?;
420
421 let deprecation = intermediate
422 .deprecated
423 .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
424
425 Ok(ArgumentDefinition {
426 name: intermediate.name,
427 arg_type,
428 nullable: intermediate.nullable,
429 default_value: intermediate.default,
430 description: None,
431 deprecation,
432 })
433 }
434
435 const fn convert_auto_params(intermediate: IntermediateAutoParams) -> AutoParams {
437 AutoParams {
438 has_limit: intermediate.limit,
439 has_offset: intermediate.offset,
440 has_where: intermediate.where_clause,
441 has_order_by: intermediate.order_by,
442 }
443 }
444
445 fn convert_subscription(
447 intermediate: IntermediateSubscription,
448 ) -> Result<SubscriptionDefinition> {
449 let arguments = intermediate
450 .arguments
451 .into_iter()
452 .map(Self::convert_argument)
453 .collect::<Result<Vec<_>>>()
454 .context(format!("Failed to convert subscription '{}'", intermediate.name))?;
455
456 let filter = intermediate.filter.map(|f| {
458 let argument_paths = f.conditions.into_iter().map(|c| (c.argument, c.path)).collect();
459 SubscriptionFilter {
460 argument_paths,
461 static_filters: Vec::new(),
462 }
463 });
464
465 let deprecation = intermediate
467 .deprecated
468 .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
469
470 Ok(SubscriptionDefinition {
471 name: intermediate.name,
472 return_type: intermediate.return_type,
473 arguments,
474 description: intermediate.description,
475 topic: intermediate.topic,
476 filter,
477 fields: intermediate.fields,
478 deprecation,
479 })
480 }
481
482 fn convert_directive(intermediate: IntermediateDirective) -> Result<DirectiveDefinition> {
484 let arguments = intermediate
485 .arguments
486 .into_iter()
487 .map(Self::convert_argument)
488 .collect::<Result<Vec<_>>>()
489 .context(format!("Failed to convert directive '{}'", intermediate.name))?;
490
491 let locations = intermediate
493 .locations
494 .into_iter()
495 .filter_map(|loc| Self::parse_directive_location(&loc))
496 .collect();
497
498 Ok(DirectiveDefinition {
499 name: intermediate.name,
500 description: intermediate.description,
501 locations,
502 arguments,
503 is_repeatable: intermediate.repeatable,
504 })
505 }
506
507 fn parse_directive_location(location: &str) -> Option<DirectiveLocationKind> {
509 match location {
510 "SCHEMA" => Some(DirectiveLocationKind::Schema),
512 "SCALAR" => Some(DirectiveLocationKind::Scalar),
513 "OBJECT" => Some(DirectiveLocationKind::Object),
514 "FIELD_DEFINITION" => Some(DirectiveLocationKind::FieldDefinition),
515 "ARGUMENT_DEFINITION" => Some(DirectiveLocationKind::ArgumentDefinition),
516 "INTERFACE" => Some(DirectiveLocationKind::Interface),
517 "UNION" => Some(DirectiveLocationKind::Union),
518 "ENUM" => Some(DirectiveLocationKind::Enum),
519 "ENUM_VALUE" => Some(DirectiveLocationKind::EnumValue),
520 "INPUT_OBJECT" => Some(DirectiveLocationKind::InputObject),
521 "INPUT_FIELD_DEFINITION" => Some(DirectiveLocationKind::InputFieldDefinition),
522 "QUERY" => Some(DirectiveLocationKind::Query),
524 "MUTATION" => Some(DirectiveLocationKind::Mutation),
525 "SUBSCRIPTION" => Some(DirectiveLocationKind::Subscription),
526 "FIELD" => Some(DirectiveLocationKind::Field),
527 "FRAGMENT_DEFINITION" => Some(DirectiveLocationKind::FragmentDefinition),
528 "FRAGMENT_SPREAD" => Some(DirectiveLocationKind::FragmentSpread),
529 "INLINE_FRAGMENT" => Some(DirectiveLocationKind::InlineFragment),
530 "VARIABLE_DEFINITION" => Some(DirectiveLocationKind::VariableDefinition),
531 _ => {
532 warn!("Unknown directive location: {}", location);
533 None
534 },
535 }
536 }
537
538 fn validate(schema: &CompiledSchema) -> Result<()> {
547 info!("Validating compiled schema");
548
549 let mut type_names = HashSet::new();
551 for type_def in &schema.types {
552 type_names.insert(type_def.name.clone());
553 }
554
555 let mut interface_names = HashSet::new();
557 for interface_def in &schema.interfaces {
558 interface_names.insert(interface_def.name.clone());
559 }
560
561 type_names.insert("Int".to_string());
563 type_names.insert("Float".to_string());
564 type_names.insert("String".to_string());
565 type_names.insert("Boolean".to_string());
566 type_names.insert("ID".to_string());
567
568 for query in &schema.queries {
570 if !type_names.contains(&query.return_type) {
571 warn!("Query '{}' references unknown type: {}", query.name, query.return_type);
572 anyhow::bail!(
573 "Query '{}' references unknown type '{}'",
574 query.name,
575 query.return_type
576 );
577 }
578
579 for arg in &query.arguments {
581 let type_name = Self::extract_type_name(&arg.arg_type);
582 if !type_names.contains(&type_name) {
583 anyhow::bail!(
584 "Query '{}' argument '{}' references unknown type '{}'",
585 query.name,
586 arg.name,
587 type_name
588 );
589 }
590 }
591 }
592
593 for mutation in &schema.mutations {
595 if !type_names.contains(&mutation.return_type) {
596 anyhow::bail!(
597 "Mutation '{}' references unknown type '{}'",
598 mutation.name,
599 mutation.return_type
600 );
601 }
602
603 for arg in &mutation.arguments {
605 let type_name = Self::extract_type_name(&arg.arg_type);
606 if !type_names.contains(&type_name) {
607 anyhow::bail!(
608 "Mutation '{}' argument '{}' references unknown type '{}'",
609 mutation.name,
610 arg.name,
611 type_name
612 );
613 }
614 }
615 }
616
617 for type_def in &schema.types {
619 for interface_name in &type_def.implements {
620 if !interface_names.contains(interface_name) {
621 anyhow::bail!(
622 "Type '{}' implements unknown interface '{}'",
623 type_def.name,
624 interface_name
625 );
626 }
627
628 if let Some(interface) = schema.find_interface(interface_name) {
630 for interface_field in &interface.fields {
631 let type_has_field = type_def.fields.iter().any(|f| {
632 f.name == interface_field.name
633 && f.field_type == interface_field.field_type
634 });
635 if !type_has_field {
636 anyhow::bail!(
637 "Type '{}' implements interface '{}' but is missing field '{}'",
638 type_def.name,
639 interface_name,
640 interface_field.name
641 );
642 }
643 }
644 }
645 }
646 }
647
648 info!("Schema validation passed");
649 Ok(())
650 }
651
652 fn extract_type_name(field_type: &FieldType) -> String {
656 match field_type {
657 FieldType::String => "String".to_string(),
658 FieldType::Int => "Int".to_string(),
659 FieldType::Float => "Float".to_string(),
660 FieldType::Boolean => "Boolean".to_string(),
661 FieldType::Id => "ID".to_string(),
662 FieldType::DateTime => "DateTime".to_string(),
663 FieldType::Date => "Date".to_string(),
664 FieldType::Time => "Time".to_string(),
665 FieldType::Json => "Json".to_string(),
666 FieldType::Uuid => "UUID".to_string(),
667 FieldType::Decimal => "Decimal".to_string(),
668 FieldType::Vector => "Vector".to_string(),
669 FieldType::Scalar(name) => name.clone(),
670 FieldType::Object(name) => name.clone(),
671 FieldType::Enum(name) => name.clone(),
672 FieldType::Input(name) => name.clone(),
673 FieldType::Interface(name) => name.clone(),
674 FieldType::Union(name) => name.clone(),
675 FieldType::List(inner) => Self::extract_type_name(inner),
676 }
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn test_convert_minimal_schema() {
686 let intermediate = IntermediateSchema {
687 security: None,
688 version: "2.0.0".to_string(),
689 types: vec![],
690 enums: vec![],
691 input_types: vec![],
692 interfaces: vec![],
693 unions: vec![],
694 queries: vec![],
695 mutations: vec![],
696 subscriptions: vec![],
697 fragments: None,
698 directives: None,
699 fact_tables: None,
700 aggregate_queries: None,
701 observers: None,
702 custom_scalars: None,
703 };
704
705 let compiled = SchemaConverter::convert(intermediate).unwrap();
706 assert_eq!(compiled.types.len(), 0);
707 assert_eq!(compiled.queries.len(), 0);
708 assert_eq!(compiled.mutations.len(), 0);
709 }
710
711 #[test]
712 fn test_convert_type_with_fields() {
713 let intermediate = IntermediateSchema {
714 security: None,
715 version: "2.0.0".to_string(),
716 types: vec![IntermediateType {
717 name: "User".to_string(),
718 fields: vec![
719 IntermediateField {
720 name: "id".to_string(),
721 field_type: "Int".to_string(),
722 nullable: false,
723 description: None,
724 directives: None,
725 requires_scope: None,
726 },
727 IntermediateField {
728 name: "name".to_string(),
729 field_type: "String".to_string(),
730 nullable: false,
731 description: None,
732 directives: None,
733 requires_scope: None,
734 },
735 ],
736 description: Some("User type".to_string()),
737 implements: vec![],
738 }],
739 enums: vec![],
740 input_types: vec![],
741 interfaces: vec![],
742 unions: vec![],
743 queries: vec![],
744 mutations: vec![],
745 subscriptions: vec![],
746 fragments: None,
747 directives: None,
748 fact_tables: None,
749 aggregate_queries: None,
750 observers: None,
751 custom_scalars: None,
752 };
753
754 let compiled = SchemaConverter::convert(intermediate).unwrap();
755 assert_eq!(compiled.types.len(), 1);
756 assert_eq!(compiled.types[0].name, "User");
757 assert_eq!(compiled.types[0].fields.len(), 2);
758 assert_eq!(compiled.types[0].fields[0].field_type, FieldType::Int);
759 assert_eq!(compiled.types[0].fields[1].field_type, FieldType::String);
760 }
761
762 #[test]
763 fn test_validate_unknown_type_reference() {
764 let intermediate = IntermediateSchema {
765 security: None,
766 version: "2.0.0".to_string(),
767 types: vec![],
768 enums: vec![],
769 input_types: vec![],
770 interfaces: vec![],
771 unions: vec![],
772 queries: vec![IntermediateQuery {
773 name: "users".to_string(),
774 return_type: "UnknownType".to_string(),
775 returns_list: true,
776 nullable: false,
777 arguments: vec![],
778 description: None,
779 sql_source: Some("v_user".to_string()),
780 auto_params: None,
781 deprecated: None,
782 jsonb_column: None,
783 }],
784 mutations: vec![],
785 subscriptions: vec![],
786 fragments: None,
787 directives: None,
788 fact_tables: None,
789 aggregate_queries: None,
790 observers: None,
791 custom_scalars: None,
792 };
793
794 let result = SchemaConverter::convert(intermediate);
795 assert!(result.is_err());
796 assert!(result.unwrap_err().to_string().contains("unknown type 'UnknownType'"));
797 }
798
799 #[test]
800 fn test_convert_query_with_arguments() {
801 let intermediate = IntermediateSchema {
802 security: None,
803 version: "2.0.0".to_string(),
804 types: vec![IntermediateType {
805 name: "User".to_string(),
806 fields: vec![],
807 description: None,
808 implements: vec![],
809 }],
810 enums: vec![],
811 input_types: vec![],
812 interfaces: vec![],
813 unions: vec![],
814 queries: vec![IntermediateQuery {
815 name: "users".to_string(),
816 return_type: "User".to_string(),
817 returns_list: true,
818 nullable: false,
819 arguments: vec![IntermediateArgument {
820 name: "limit".to_string(),
821 arg_type: "Int".to_string(),
822 nullable: false,
823 default: Some(serde_json::json!(10)),
824 deprecated: None,
825 }],
826 description: Some("Get users".to_string()),
827 sql_source: Some("v_user".to_string()),
828 auto_params: Some(IntermediateAutoParams {
829 limit: true,
830 offset: true,
831 where_clause: false,
832 order_by: false,
833 }),
834 deprecated: None,
835 jsonb_column: None,
836 }],
837 mutations: vec![],
838 subscriptions: vec![],
839 fragments: None,
840 directives: None,
841 fact_tables: None,
842 aggregate_queries: None,
843 observers: None,
844 custom_scalars: None,
845 };
846
847 let compiled = SchemaConverter::convert(intermediate).unwrap();
848 assert_eq!(compiled.queries.len(), 1);
849 assert_eq!(compiled.queries[0].arguments.len(), 1);
850 assert_eq!(compiled.queries[0].arguments[0].arg_type, FieldType::Int);
851 assert!(compiled.queries[0].auto_params.has_limit);
852 }
853
854 #[test]
855 fn test_convert_field_with_deprecated_directive() {
856 use crate::schema::intermediate::IntermediateAppliedDirective;
857
858 let intermediate = IntermediateSchema {
859 security: None,
860 version: "2.0.0".to_string(),
861 types: vec![IntermediateType {
862 name: "User".to_string(),
863 fields: vec![
864 IntermediateField {
865 name: "oldId".to_string(),
866 field_type: "Int".to_string(),
867 nullable: false,
868 description: None,
869 directives: Some(vec![IntermediateAppliedDirective {
870 name: "deprecated".to_string(),
871 arguments: Some(serde_json::json!({"reason": "Use 'id' instead"})),
872 }]),
873 requires_scope: None,
874 },
875 IntermediateField {
876 name: "id".to_string(),
877 field_type: "Int".to_string(),
878 nullable: false,
879 description: None,
880 directives: None,
881 requires_scope: None,
882 },
883 ],
884 description: None,
885 implements: vec![],
886 }],
887 enums: vec![],
888 input_types: vec![],
889 interfaces: vec![],
890 unions: vec![],
891 queries: vec![],
892 mutations: vec![],
893 subscriptions: vec![],
894 fragments: None,
895 directives: None,
896 fact_tables: None,
897 aggregate_queries: None,
898 observers: None,
899 custom_scalars: None,
900 };
901
902 let compiled = SchemaConverter::convert(intermediate).unwrap();
903 assert_eq!(compiled.types.len(), 1);
904 assert_eq!(compiled.types[0].fields.len(), 2);
905
906 let old_id_field = &compiled.types[0].fields[0];
908 assert_eq!(old_id_field.name, "oldId");
909 assert!(old_id_field.is_deprecated());
910 assert_eq!(old_id_field.deprecation_reason(), Some("Use 'id' instead"));
911
912 let id_field = &compiled.types[0].fields[1];
914 assert_eq!(id_field.name, "id");
915 assert!(!id_field.is_deprecated());
916 assert_eq!(id_field.deprecation_reason(), None);
917 }
918
919 #[test]
920 fn test_convert_enum() {
921 use crate::schema::intermediate::{
922 IntermediateDeprecation, IntermediateEnum, IntermediateEnumValue,
923 };
924
925 let intermediate = IntermediateSchema {
926 security: None,
927 version: "2.0.0".to_string(),
928 types: vec![],
929 enums: vec![IntermediateEnum {
930 name: "OrderStatus".to_string(),
931 values: vec![
932 IntermediateEnumValue {
933 name: "PENDING".to_string(),
934 description: None,
935 deprecated: None,
936 },
937 IntermediateEnumValue {
938 name: "PROCESSING".to_string(),
939 description: Some("Currently being processed".to_string()),
940 deprecated: None,
941 },
942 IntermediateEnumValue {
943 name: "CANCELLED".to_string(),
944 description: None,
945 deprecated: Some(IntermediateDeprecation {
946 reason: Some("Use VOIDED instead".to_string()),
947 }),
948 },
949 ],
950 description: Some("Order status enum".to_string()),
951 }],
952 input_types: vec![],
953 interfaces: vec![],
954 unions: vec![],
955 queries: vec![],
956 mutations: vec![],
957 subscriptions: vec![],
958 fragments: None,
959 directives: None,
960 fact_tables: None,
961 aggregate_queries: None,
962 observers: None,
963 custom_scalars: None,
964 };
965
966 let compiled = SchemaConverter::convert(intermediate).unwrap();
967 assert_eq!(compiled.enums.len(), 1);
968
969 let status_enum = &compiled.enums[0];
970 assert_eq!(status_enum.name, "OrderStatus");
971 assert_eq!(status_enum.description, Some("Order status enum".to_string()));
972 assert_eq!(status_enum.values.len(), 3);
973
974 assert_eq!(status_enum.values[0].name, "PENDING");
976 assert!(!status_enum.values[0].is_deprecated());
977
978 assert_eq!(status_enum.values[1].name, "PROCESSING");
980 assert_eq!(
981 status_enum.values[1].description,
982 Some("Currently being processed".to_string())
983 );
984
985 assert_eq!(status_enum.values[2].name, "CANCELLED");
987 assert!(status_enum.values[2].is_deprecated());
988 }
989
990 #[test]
991 fn test_convert_input_object() {
992 use crate::schema::intermediate::{
993 IntermediateDeprecation, IntermediateInputField, IntermediateInputObject,
994 };
995
996 let intermediate = IntermediateSchema {
997 security: None,
998 version: "2.0.0".to_string(),
999 types: vec![],
1000 enums: vec![],
1001 input_types: vec![IntermediateInputObject {
1002 name: "UserFilter".to_string(),
1003 fields: vec![
1004 IntermediateInputField {
1005 name: "name".to_string(),
1006 field_type: "String".to_string(),
1007 nullable: true,
1008 description: None,
1009 default: None,
1010 deprecated: None,
1011 },
1012 IntermediateInputField {
1013 name: "active".to_string(),
1014 field_type: "Boolean".to_string(),
1015 nullable: true,
1016 description: Some("Filter by active status".to_string()),
1017 default: Some(serde_json::json!(true)),
1018 deprecated: None,
1019 },
1020 IntermediateInputField {
1021 name: "oldField".to_string(),
1022 field_type: "String".to_string(),
1023 nullable: true,
1024 description: None,
1025 default: None,
1026 deprecated: Some(IntermediateDeprecation {
1027 reason: Some("Use newField instead".to_string()),
1028 }),
1029 },
1030 ],
1031 description: Some("User filter input".to_string()),
1032 }],
1033 interfaces: vec![],
1034 unions: vec![],
1035 queries: vec![],
1036 mutations: vec![],
1037 subscriptions: vec![],
1038 fragments: None,
1039 directives: None,
1040 fact_tables: None,
1041 aggregate_queries: None,
1042 observers: None,
1043 custom_scalars: None,
1044 };
1045
1046 let compiled = SchemaConverter::convert(intermediate).unwrap();
1047 assert_eq!(compiled.input_types.len(), 50);
1049
1050 let filter = compiled.input_types.iter().find(|t| t.name == "UserFilter").unwrap();
1052 assert_eq!(filter.name, "UserFilter");
1053 assert_eq!(filter.description, Some("User filter input".to_string()));
1054 assert_eq!(filter.fields.len(), 3);
1055
1056 let name_field = filter.find_field("name").unwrap();
1058 assert_eq!(name_field.field_type, "String");
1059 assert!(!name_field.is_deprecated());
1060
1061 let active_field = filter.find_field("active").unwrap();
1063 assert_eq!(active_field.field_type, "Boolean");
1064 assert_eq!(active_field.default_value, Some("true".to_string()));
1065 assert_eq!(active_field.description, Some("Filter by active status".to_string()));
1066
1067 let old_field = filter.find_field("oldField").unwrap();
1069 assert!(old_field.is_deprecated());
1070 }
1071
1072 #[test]
1073 fn test_rich_filter_types_generated() {
1074 let intermediate = IntermediateSchema {
1075 security: None,
1076 version: "2.0.0".to_string(),
1077 types: vec![],
1078 enums: vec![],
1079 input_types: vec![],
1080 interfaces: vec![],
1081 unions: vec![],
1082 queries: vec![],
1083 mutations: vec![],
1084 subscriptions: vec![],
1085 fragments: None,
1086 directives: None,
1087 fact_tables: None,
1088 aggregate_queries: None,
1089 observers: None,
1090 custom_scalars: None,
1091 };
1092
1093 let compiled = SchemaConverter::convert(intermediate).unwrap();
1094
1095 assert_eq!(compiled.input_types.len(), 49);
1097
1098 let email_where = compiled
1100 .input_types
1101 .iter()
1102 .find(|t| t.name == "EmailAddressWhereInput")
1103 .expect("EmailAddressWhereInput should be generated");
1104
1105 assert!(email_where.fields.len() > 6);
1107 assert!(email_where.fields.iter().any(|f| f.name == "eq"));
1108 assert!(email_where.fields.iter().any(|f| f.name == "neq"));
1109 assert!(email_where.fields.iter().any(|f| f.name == "contains"));
1110 assert!(email_where.fields.iter().any(|f| f.name == "isnull"));
1111
1112 let vin_where = compiled
1114 .input_types
1115 .iter()
1116 .find(|t| t.name == "VINWhereInput")
1117 .expect("VINWhereInput should be generated");
1118
1119 assert!(vin_where.fields.len() > 6);
1120 assert!(vin_where.fields.iter().any(|f| f.name == "eq"));
1121 }
1122
1123 #[test]
1124 fn test_rich_filter_types_have_sql_templates() {
1125 let intermediate = IntermediateSchema {
1126 security: None,
1127 version: "2.0.0".to_string(),
1128 types: vec![],
1129 enums: vec![],
1130 input_types: vec![],
1131 interfaces: vec![],
1132 unions: vec![],
1133 queries: vec![],
1134 mutations: vec![],
1135 subscriptions: vec![],
1136 fragments: None,
1137 directives: None,
1138 fact_tables: None,
1139 aggregate_queries: None,
1140 observers: None,
1141 custom_scalars: None,
1142 };
1143
1144 let compiled = SchemaConverter::convert(intermediate).unwrap();
1145
1146 let email_where = compiled
1148 .input_types
1149 .iter()
1150 .find(|t| t.name == "EmailAddressWhereInput")
1151 .expect("EmailAddressWhereInput should be generated");
1152
1153 assert!(
1155 email_where.metadata.is_some(),
1156 "Metadata should exist for EmailAddressWhereInput"
1157 );
1158 let metadata = email_where.metadata.as_ref().unwrap();
1159 assert!(
1160 metadata.get("operators").is_some(),
1161 "Operators should be in metadata: {metadata:?}"
1162 );
1163
1164 let operators = metadata["operators"].as_object().unwrap();
1165 assert!(!operators.is_empty(), "Operators map should not be empty: {operators:?}");
1167 assert!(
1168 operators.contains_key("domainEq"),
1169 "Missing domainEq in operators: {:?}",
1170 operators.keys().collect::<Vec<_>>()
1171 );
1172
1173 let email_domain_eq = operators["domainEq"].as_object().unwrap();
1175 assert!(email_domain_eq.contains_key("postgres"));
1176 assert!(email_domain_eq.contains_key("mysql"));
1177 assert!(email_domain_eq.contains_key("sqlite"));
1178 assert!(email_domain_eq.contains_key("sqlserver"));
1179
1180 let postgres_template = email_domain_eq["postgres"].as_str().unwrap();
1182 assert!(postgres_template.contains("SPLIT_PART"));
1183 assert!(postgres_template.contains("$field"));
1184 }
1185
1186 #[test]
1187 fn test_lookup_data_embedded_in_schema() {
1188 let intermediate = IntermediateSchema {
1189 security: None,
1190 version: "2.0.0".to_string(),
1191 types: vec![],
1192 enums: vec![],
1193 input_types: vec![],
1194 interfaces: vec![],
1195 unions: vec![],
1196 queries: vec![],
1197 mutations: vec![],
1198 subscriptions: vec![],
1199 fragments: None,
1200 directives: None,
1201 fact_tables: None,
1202 aggregate_queries: None,
1203 observers: None,
1204 custom_scalars: None,
1205 };
1206
1207 let compiled = SchemaConverter::convert(intermediate).unwrap();
1208
1209 assert!(compiled.security.is_some(), "Security section should exist");
1211 let security = compiled.security.as_ref().unwrap();
1212 assert!(
1213 security.get("lookup_data").is_some(),
1214 "Lookup data should be in security section"
1215 );
1216
1217 let lookup_data = security["lookup_data"].as_object().unwrap();
1218
1219 assert!(lookup_data.contains_key("countries"), "Countries lookup should be present");
1221 assert!(lookup_data.contains_key("currencies"), "Currencies lookup should be present");
1222 assert!(lookup_data.contains_key("timezones"), "Timezones lookup should be present");
1223 assert!(lookup_data.contains_key("languages"), "Languages lookup should be present");
1224
1225 let countries = lookup_data["countries"].as_object().unwrap();
1227 assert!(countries.contains_key("US"), "US should be in countries");
1228 assert!(countries.contains_key("FR"), "France should be in countries");
1229 assert!(countries.contains_key("GB"), "UK should be in countries");
1230
1231 let us = countries["US"].as_object().unwrap();
1233 assert_eq!(us["continent"].as_str().unwrap(), "North America");
1234 assert!(!us["in_eu"].as_bool().unwrap());
1235
1236 let fr = countries["FR"].as_object().unwrap();
1238 assert!(fr["in_eu"].as_bool().unwrap());
1239 assert!(fr["in_schengen"].as_bool().unwrap());
1240
1241 let currencies = lookup_data["currencies"].as_object().unwrap();
1243 assert!(currencies.contains_key("USD"));
1244 assert!(currencies.contains_key("EUR"));
1245 let usd = currencies["USD"].as_object().unwrap();
1246 assert_eq!(usd["symbol"].as_str().unwrap(), "$");
1247 assert_eq!(usd["decimal_places"].as_i64().unwrap(), 2);
1248
1249 let timezones = lookup_data["timezones"].as_object().unwrap();
1251 assert!(timezones.contains_key("UTC"));
1252 assert!(timezones.contains_key("EST"));
1253 let est = timezones["EST"].as_object().unwrap();
1254 assert_eq!(est["offset_minutes"].as_i64().unwrap(), -300);
1255 assert!(est["has_dst"].as_bool().unwrap());
1256 }
1257
1258 #[test]
1259 fn test_convert_interface() {
1260 use crate::schema::intermediate::{IntermediateField, IntermediateInterface};
1261
1262 let intermediate = IntermediateSchema {
1263 security: None,
1264 version: "2.0.0".to_string(),
1265 types: vec![],
1266 enums: vec![],
1267 input_types: vec![],
1268 interfaces: vec![IntermediateInterface {
1269 name: "Node".to_string(),
1270 fields: vec![IntermediateField {
1271 name: "id".to_string(),
1272 field_type: "ID".to_string(),
1273 nullable: false,
1274 description: None,
1275 directives: None,
1276 requires_scope: None,
1277 }],
1278 description: Some("An object with a globally unique ID".to_string()),
1279 }],
1280 unions: vec![],
1281 queries: vec![],
1282 mutations: vec![],
1283 subscriptions: vec![],
1284 fragments: None,
1285 directives: None,
1286 fact_tables: None,
1287 aggregate_queries: None,
1288 observers: None,
1289 custom_scalars: None,
1290 };
1291
1292 let compiled = SchemaConverter::convert(intermediate).unwrap();
1293 assert_eq!(compiled.interfaces.len(), 1);
1294
1295 let interface = &compiled.interfaces[0];
1296 assert_eq!(interface.name, "Node");
1297 assert_eq!(interface.description, Some("An object with a globally unique ID".to_string()));
1298 assert_eq!(interface.fields.len(), 1);
1299 assert_eq!(interface.fields[0].name, "id");
1300 assert_eq!(interface.fields[0].field_type, FieldType::Id);
1301 }
1302
1303 #[test]
1304 fn test_convert_type_implements_interface() {
1305 use crate::schema::intermediate::{
1306 IntermediateField, IntermediateInterface, IntermediateType,
1307 };
1308
1309 let intermediate = IntermediateSchema {
1310 security: None,
1311 version: "2.0.0".to_string(),
1312 types: vec![IntermediateType {
1313 name: "User".to_string(),
1314 fields: vec![
1315 IntermediateField {
1316 name: "id".to_string(),
1317 field_type: "ID".to_string(),
1318 nullable: false,
1319 description: None,
1320 directives: None,
1321 requires_scope: None,
1322 },
1323 IntermediateField {
1324 name: "name".to_string(),
1325 field_type: "String".to_string(),
1326 nullable: false,
1327 description: None,
1328 directives: None,
1329 requires_scope: None,
1330 },
1331 ],
1332 description: None,
1333 implements: vec!["Node".to_string()],
1334 }],
1335 enums: vec![],
1336 input_types: vec![],
1337 interfaces: vec![IntermediateInterface {
1338 name: "Node".to_string(),
1339 fields: vec![IntermediateField {
1340 name: "id".to_string(),
1341 field_type: "ID".to_string(),
1342 nullable: false,
1343 description: None,
1344 directives: None,
1345 requires_scope: None,
1346 }],
1347 description: None,
1348 }],
1349 unions: vec![],
1350 queries: vec![],
1351 mutations: vec![],
1352 subscriptions: vec![],
1353 fragments: None,
1354 directives: None,
1355 fact_tables: None,
1356 aggregate_queries: None,
1357 observers: None,
1358 custom_scalars: None,
1359 };
1360
1361 let compiled = SchemaConverter::convert(intermediate).unwrap();
1362
1363 assert_eq!(compiled.types.len(), 1);
1365 assert_eq!(compiled.types[0].implements, vec!["Node"]);
1366
1367 assert_eq!(compiled.interfaces.len(), 1);
1369 assert_eq!(compiled.interfaces[0].name, "Node");
1370 }
1371
1372 #[test]
1373 fn test_validate_unknown_interface() {
1374 use crate::schema::intermediate::{IntermediateField, IntermediateType};
1375
1376 let intermediate = IntermediateSchema {
1377 security: None,
1378 version: "2.0.0".to_string(),
1379 types: vec![IntermediateType {
1380 name: "User".to_string(),
1381 fields: vec![IntermediateField {
1382 name: "id".to_string(),
1383 field_type: "ID".to_string(),
1384 nullable: false,
1385 description: None,
1386 directives: None,
1387 requires_scope: None,
1388 }],
1389 description: None,
1390 implements: vec!["UnknownInterface".to_string()],
1391 }],
1392 enums: vec![],
1393 input_types: vec![],
1394 interfaces: vec![], unions: vec![],
1396 queries: vec![],
1397 mutations: vec![],
1398 subscriptions: vec![],
1399 fragments: None,
1400 directives: None,
1401 fact_tables: None,
1402 aggregate_queries: None,
1403 observers: None,
1404 custom_scalars: None,
1405 };
1406
1407 let result = SchemaConverter::convert(intermediate);
1408 assert!(result.is_err());
1409 assert!(result.unwrap_err().to_string().contains("unknown interface"));
1410 }
1411
1412 #[test]
1413 fn test_validate_missing_interface_field() {
1414 use crate::schema::intermediate::{
1415 IntermediateField, IntermediateInterface, IntermediateType,
1416 };
1417
1418 let intermediate = IntermediateSchema {
1419 security: None,
1420 version: "2.0.0".to_string(),
1421 types: vec![IntermediateType {
1422 name: "User".to_string(),
1423 fields: vec![
1424 IntermediateField {
1426 name: "name".to_string(),
1427 field_type: "String".to_string(),
1428 nullable: false,
1429 description: None,
1430 directives: None,
1431 requires_scope: None,
1432 },
1433 ],
1434 description: None,
1435 implements: vec!["Node".to_string()],
1436 }],
1437 enums: vec![],
1438 input_types: vec![],
1439 interfaces: vec![IntermediateInterface {
1440 name: "Node".to_string(),
1441 fields: vec![IntermediateField {
1442 name: "id".to_string(),
1443 field_type: "ID".to_string(),
1444 nullable: false,
1445 description: None,
1446 directives: None,
1447 requires_scope: None,
1448 }],
1449 description: None,
1450 }],
1451 unions: vec![],
1452 queries: vec![],
1453 mutations: vec![],
1454 subscriptions: vec![],
1455 fragments: None,
1456 directives: None,
1457 fact_tables: None,
1458 aggregate_queries: None,
1459 observers: None,
1460 custom_scalars: None,
1461 };
1462
1463 let result = SchemaConverter::convert(intermediate);
1464 assert!(result.is_err());
1465 assert!(result.unwrap_err().to_string().contains("missing field 'id'"));
1466 }
1467
1468 #[test]
1469 fn test_convert_union() {
1470 use crate::schema::intermediate::{IntermediateField, IntermediateType, IntermediateUnion};
1471
1472 let intermediate = IntermediateSchema {
1473 security: None,
1474 version: "2.0.0".to_string(),
1475 types: vec![
1476 IntermediateType {
1477 name: "User".to_string(),
1478 fields: vec![IntermediateField {
1479 name: "id".to_string(),
1480 field_type: "ID".to_string(),
1481 nullable: false,
1482 description: None,
1483 directives: None,
1484 requires_scope: None,
1485 }],
1486 description: None,
1487 implements: vec![],
1488 },
1489 IntermediateType {
1490 name: "Post".to_string(),
1491 fields: vec![IntermediateField {
1492 name: "id".to_string(),
1493 field_type: "ID".to_string(),
1494 nullable: false,
1495 description: None,
1496 directives: None,
1497 requires_scope: None,
1498 }],
1499 description: None,
1500 implements: vec![],
1501 },
1502 ],
1503 enums: vec![],
1504 input_types: vec![],
1505 interfaces: vec![],
1506 unions: vec![IntermediateUnion {
1507 name: "SearchResult".to_string(),
1508 member_types: vec!["User".to_string(), "Post".to_string()],
1509 description: Some("Result from a search query".to_string()),
1510 }],
1511 queries: vec![],
1512 mutations: vec![],
1513 subscriptions: vec![],
1514 fragments: None,
1515 directives: None,
1516 fact_tables: None,
1517 aggregate_queries: None,
1518 observers: None,
1519 custom_scalars: None,
1520 };
1521
1522 let compiled = SchemaConverter::convert(intermediate).unwrap();
1523
1524 assert_eq!(compiled.unions.len(), 1);
1526 let union_def = &compiled.unions[0];
1527 assert_eq!(union_def.name, "SearchResult");
1528 assert_eq!(union_def.member_types, vec!["User", "Post"]);
1529 assert_eq!(union_def.description, Some("Result from a search query".to_string()));
1530 }
1531
1532 #[test]
1533 fn test_convert_field_requires_scope() {
1534 use crate::schema::intermediate::{IntermediateField, IntermediateType};
1535
1536 let intermediate = IntermediateSchema {
1537 security: None,
1538 version: "2.0.0".to_string(),
1539 types: vec![IntermediateType {
1540 name: "Employee".to_string(),
1541 fields: vec![
1542 IntermediateField {
1543 name: "id".to_string(),
1544 field_type: "ID".to_string(),
1545 nullable: false,
1546 description: None,
1547 directives: None,
1548 requires_scope: None,
1549 },
1550 IntermediateField {
1551 name: "name".to_string(),
1552 field_type: "String".to_string(),
1553 nullable: false,
1554 description: None,
1555 directives: None,
1556 requires_scope: None,
1557 },
1558 IntermediateField {
1559 name: "salary".to_string(),
1560 field_type: "Float".to_string(),
1561 nullable: false,
1562 description: Some("Employee salary - protected field".to_string()),
1563 directives: None,
1564 requires_scope: Some("read:Employee.salary".to_string()),
1565 },
1566 IntermediateField {
1567 name: "ssn".to_string(),
1568 field_type: "String".to_string(),
1569 nullable: true,
1570 description: Some(
1571 "Social Security Number - highly protected".to_string(),
1572 ),
1573 directives: None,
1574 requires_scope: Some("admin".to_string()),
1575 },
1576 ],
1577 description: None,
1578 implements: vec![],
1579 }],
1580 enums: vec![],
1581 input_types: vec![],
1582 interfaces: vec![],
1583 unions: vec![],
1584 queries: vec![],
1585 mutations: vec![],
1586 subscriptions: vec![],
1587 fragments: None,
1588 directives: None,
1589 fact_tables: None,
1590 aggregate_queries: None,
1591 observers: None,
1592 custom_scalars: None,
1593 };
1594
1595 let compiled = SchemaConverter::convert(intermediate).unwrap();
1596
1597 assert_eq!(compiled.types.len(), 1);
1598 let employee_type = &compiled.types[0];
1599 assert_eq!(employee_type.name, "Employee");
1600 assert_eq!(employee_type.fields.len(), 4);
1601
1602 assert_eq!(employee_type.fields[0].name, "id");
1604 assert!(employee_type.fields[0].requires_scope.is_none());
1605
1606 assert_eq!(employee_type.fields[1].name, "name");
1608 assert!(employee_type.fields[1].requires_scope.is_none());
1609
1610 assert_eq!(employee_type.fields[2].name, "salary");
1612 assert_eq!(
1613 employee_type.fields[2].requires_scope,
1614 Some("read:Employee.salary".to_string())
1615 );
1616
1617 assert_eq!(employee_type.fields[3].name, "ssn");
1619 assert_eq!(employee_type.fields[3].requires_scope, Some("admin".to_string()));
1620 }
1621}