1use super::ir::AuthoringIR;
13use crate::{
14 error::{FraiseQLError, Result},
15 schema::is_known_scalar,
16};
17
18fn extract_base_type(type_str: &str) -> &str {
27 let s = type_str.trim();
28
29 let s = s.trim_start_matches('[').trim_end_matches(']');
31 let s = s.trim_end_matches('!').trim_start_matches('!');
32
33 let s = s.trim_start_matches('[').trim_end_matches(']');
35 let s = s.trim_end_matches('!');
36
37 s.trim()
38}
39
40fn is_valid_type(base_type: &str, defined_types: &std::collections::HashSet<&str>) -> bool {
42 is_known_scalar(base_type) || defined_types.contains(base_type)
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct SchemaValidationError {
48 pub message: String,
50 pub location: String,
52}
53
54pub struct SchemaValidator {
56 }
58
59impl SchemaValidator {
60 #[must_use]
62 pub const fn new() -> Self {
63 Self {}
64 }
65
66 pub fn validate(&self, ir: AuthoringIR) -> Result<AuthoringIR> {
80 self.validate_types(&ir)?;
84 self.validate_queries(&ir)?;
85
86 if !ir.fact_tables.is_empty() {
88 self.validate_fact_tables(&ir)?;
89 }
90
91 self.validate_aggregate_types(&ir)?;
94
95 Ok(ir)
96 }
97
98 fn validate_types(&self, ir: &AuthoringIR) -> Result<()> {
100 let defined_types: std::collections::HashSet<&str> =
102 ir.types.iter().map(|t| t.name.as_str()).collect();
103
104 for ir_type in &ir.types {
106 if ir_type.name.is_empty() {
108 return Err(FraiseQLError::Validation {
109 message: "Type name cannot be empty".to_string(),
110 path: Some("types".to_string()),
111 });
112 }
113
114 for field in &ir_type.fields {
116 let base_type = extract_base_type(&field.field_type);
117
118 if !base_type.is_empty() && !is_valid_type(base_type, &defined_types) {
120 return Err(FraiseQLError::Validation {
121 message: format!(
122 "Type '{}' field '{}' references unknown type '{}'",
123 ir_type.name, field.name, base_type
124 ),
125 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
126 });
127 }
128 }
129 }
130
131 Ok(())
132 }
133
134 fn validate_queries(&self, ir: &AuthoringIR) -> Result<()> {
136 let defined_types: std::collections::HashSet<&str> =
138 ir.types.iter().map(|t| t.name.as_str()).collect();
139
140 for query in &ir.queries {
142 if query.name.is_empty() {
144 return Err(FraiseQLError::Validation {
145 message: "Query name cannot be empty".to_string(),
146 path: Some("queries".to_string()),
147 });
148 }
149
150 let base_type = extract_base_type(&query.return_type);
152 if !is_valid_type(base_type, &defined_types) {
153 return Err(FraiseQLError::Validation {
154 message: format!(
155 "Query '{}' returns unknown type '{}'",
156 query.name, query.return_type
157 ),
158 path: Some(format!("queries.{}.return_type", query.name)),
159 });
160 }
161
162 for arg in &query.arguments {
164 let base_type = extract_base_type(&arg.arg_type);
165 if !is_valid_type(base_type, &defined_types) {
166 return Err(FraiseQLError::Validation {
167 message: format!(
168 "Query '{}' argument '{}' has unknown type '{}'",
169 query.name, arg.name, arg.arg_type
170 ),
171 path: Some(format!("queries.{}.arguments.{}", query.name, arg.name)),
172 });
173 }
174 }
175 }
176
177 for mutation in &ir.mutations {
179 if mutation.name.is_empty() {
180 return Err(FraiseQLError::Validation {
181 message: "Mutation name cannot be empty".to_string(),
182 path: Some("mutations".to_string()),
183 });
184 }
185
186 let base_type = extract_base_type(&mutation.return_type);
187 if !is_valid_type(base_type, &defined_types) {
188 return Err(FraiseQLError::Validation {
189 message: format!(
190 "Mutation '{}' returns unknown type '{}'",
191 mutation.name, mutation.return_type
192 ),
193 path: Some(format!("mutations.{}.return_type", mutation.name)),
194 });
195 }
196 }
197
198 for subscription in &ir.subscriptions {
200 if subscription.name.is_empty() {
201 return Err(FraiseQLError::Validation {
202 message: "Subscription name cannot be empty".to_string(),
203 path: Some("subscriptions".to_string()),
204 });
205 }
206
207 let base_type = extract_base_type(&subscription.return_type);
208 if !is_valid_type(base_type, &defined_types) {
209 return Err(FraiseQLError::Validation {
210 message: format!(
211 "Subscription '{}' returns unknown type '{}'",
212 subscription.name, subscription.return_type
213 ),
214 path: Some(format!("subscriptions.{}.return_type", subscription.name)),
215 });
216 }
217 }
218
219 Ok(())
220 }
221
222 fn validate_fact_tables(&self, ir: &AuthoringIR) -> Result<()> {
228 for (table_name, metadata) in &ir.fact_tables {
229 if !table_name.starts_with("tf_") {
231 return Err(FraiseQLError::Validation {
232 message: format!("Fact table '{}' must start with 'tf_' prefix", table_name),
233 path: Some(format!("fact_tables.{}", table_name)),
234 });
235 }
236
237 if metadata.measures.is_empty() {
238 return Err(FraiseQLError::Validation {
239 message: format!("Fact table '{}' must have at least one measure", table_name),
240 path: Some(format!("fact_tables.{}.measures", table_name)),
241 });
242 }
243
244 if metadata.dimensions.name.is_empty() {
246 return Err(FraiseQLError::Validation {
247 message: format!("Fact table '{}' dimensions missing 'name' field", table_name),
248 path: Some(format!("fact_tables.{}.dimensions", table_name)),
249 });
250 }
251 }
252
253 Ok(())
254 }
255
256 fn validate_aggregate_types(&self, ir: &AuthoringIR) -> Result<()> {
264 for ir_type in &ir.types {
266 if ir_type.name.ends_with("Aggregate") {
267 let has_count = ir_type.fields.iter().any(|f| f.name == "count");
269 if !has_count {
270 return Err(FraiseQLError::Validation {
271 message: format!(
272 "Aggregate type '{}' must have a 'count' field",
273 ir_type.name
274 ),
275 path: Some(format!("types.{}.fields", ir_type.name)),
276 });
277 }
278 }
279
280 if ir_type.name.ends_with("GroupByInput") {
282 for field in &ir_type.fields {
283 if field.field_type != "Boolean" && field.field_type != "Boolean!" {
285 return Err(FraiseQLError::Validation {
286 message: format!(
287 "GroupByInput type '{}' field '{}' must be Boolean, got '{}'",
288 ir_type.name, field.name, field.field_type
289 ),
290 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
291 });
292 }
293 }
294 }
295
296 if ir_type.name.ends_with("HavingInput") {
298 for field in &ir_type.fields {
299 let valid_suffixes = ["_eq", "_neq", "_gt", "_gte", "_lt", "_lte"];
301 let has_valid_suffix = valid_suffixes.iter().any(|s| field.name.ends_with(s));
302
303 if !has_valid_suffix {
304 return Err(FraiseQLError::Validation {
305 message: format!(
306 "HavingInput type '{}' field '{}' must have operator suffix (_eq, _neq, _gt, _gte, _lt, _lte)",
307 ir_type.name, field.name
308 ),
309 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
310 });
311 }
312 }
313 }
314 }
315
316 Ok(())
317 }
318}
319
320impl Default for SchemaValidator {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::{
329 super::ir::{IRField, IRType},
330 *,
331 };
332 use crate::compiler::fact_table::{DimensionColumn, FactTableMetadata, MeasureColumn, SqlType};
333
334 #[test]
335 fn test_validator_new() {
336 let validator = SchemaValidator::new();
337 let ir = AuthoringIR::new();
338 validator
339 .validate(ir)
340 .unwrap_or_else(|e| panic!("validate new IR should succeed: {e}"));
341 }
342
343 #[test]
344 fn test_validate_empty_ir() {
345 let validator = SchemaValidator::new();
346 let ir = AuthoringIR::new();
347 validator
348 .validate(ir)
349 .unwrap_or_else(|e| panic!("validate empty IR should succeed: {e}"));
350 }
351
352 fn make_fact_table(measures: Vec<MeasureColumn>, dim_name: &str) -> FactTableMetadata {
353 FactTableMetadata {
354 table_name: String::new(),
355 measures,
356 dimensions: DimensionColumn {
357 name: dim_name.to_string(),
358 paths: vec![],
359 },
360 denormalized_filters: vec![],
361 calendar_dimensions: vec![],
362 }
363 }
364
365 #[test]
366 fn test_validate_fact_table_with_valid_metadata() {
367 let validator = SchemaValidator::new();
368 let mut ir = AuthoringIR::new();
369 ir.fact_tables.insert(
370 "tf_sales".to_string(),
371 make_fact_table(
372 vec![MeasureColumn {
373 name: "revenue".to_string(),
374 sql_type: SqlType::Decimal,
375 nullable: false,
376 }],
377 "data",
378 ),
379 );
380 validator.validate(ir).unwrap_or_else(|e| {
381 panic!("validate fact table with valid metadata should succeed: {e}")
382 });
383 }
384
385 #[test]
386 fn test_validate_fact_table_invalid_prefix() {
387 let validator = SchemaValidator::new();
388 let mut ir = AuthoringIR::new();
389 ir.fact_tables.insert(
390 "sales".to_string(),
391 make_fact_table(
392 vec![MeasureColumn {
393 name: "revenue".to_string(),
394 sql_type: SqlType::Decimal,
395 nullable: false,
396 }],
397 "data",
398 ),
399 );
400 let result = validator.validate(ir);
401 assert!(
402 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("must start with 'tf_' prefix")),
403 "expected Validation error about tf_ prefix, got: {result:?}"
404 );
405 }
406
407 #[test]
408 fn test_validate_fact_table_empty_measures() {
409 let validator = SchemaValidator::new();
410 let mut ir = AuthoringIR::new();
411 ir.fact_tables.insert("tf_sales".to_string(), make_fact_table(vec![], "data"));
412 let result = validator.validate(ir);
413 assert!(
414 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("must have at least one measure")),
415 "expected Validation error about empty measures, got: {result:?}"
416 );
417 }
418
419 #[test]
420 fn test_validate_fact_table_dimensions_missing_name() {
421 let validator = SchemaValidator::new();
422 let mut ir = AuthoringIR::new();
423 ir.fact_tables.insert(
424 "tf_sales".to_string(),
425 make_fact_table(
426 vec![MeasureColumn {
427 name: "revenue".to_string(),
428 sql_type: SqlType::Decimal,
429 nullable: false,
430 }],
431 "",
432 ),
433 );
434 let result = validator.validate(ir);
435 assert!(
436 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("dimensions missing 'name' field")),
437 "expected Validation error about missing dimensions name, got: {result:?}"
438 );
439 }
440
441 #[test]
442 fn test_validate_aggregate_type_missing_count() {
443 let validator = SchemaValidator::new();
444 let mut ir = AuthoringIR::new();
445
446 ir.types.push(IRType {
447 name: "SalesAggregate".to_string(),
448 fields: vec![IRField {
449 name: "revenue_sum".to_string(),
450 field_type: "Float".to_string(),
451 nullable: true,
452 description: None,
453 sql_column: None,
454 }],
455 sql_source: None,
456 description: None,
457 });
458
459 let result = validator.validate(ir);
460 assert!(
461 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("must have a 'count' field")),
462 "expected Validation error about missing count field, got: {result:?}"
463 );
464 }
465
466 #[test]
467 fn test_validate_aggregate_type_with_count() {
468 let validator = SchemaValidator::new();
469 let mut ir = AuthoringIR::new();
470
471 ir.types.push(IRType {
472 name: "SalesAggregate".to_string(),
473 fields: vec![
474 IRField {
475 name: "count".to_string(),
476 field_type: "Int!".to_string(),
477 nullable: false,
478 description: None,
479 sql_column: None,
480 },
481 IRField {
482 name: "revenue_sum".to_string(),
483 field_type: "Float".to_string(),
484 nullable: true,
485 description: None,
486 sql_column: None,
487 },
488 ],
489 sql_source: None,
490 description: None,
491 });
492
493 validator
494 .validate(ir)
495 .unwrap_or_else(|e| panic!("validate aggregate type with count should succeed: {e}"));
496 }
497
498 #[test]
499 fn test_validate_group_by_input_invalid_field_type() {
500 let validator = SchemaValidator::new();
501 let mut ir = AuthoringIR::new();
502
503 ir.types.push(IRType {
504 name: "SalesGroupByInput".to_string(),
505 fields: vec![IRField {
506 name: "category".to_string(),
507 field_type: "String".to_string(), nullable: true,
509 description: None,
510 sql_column: None,
511 }],
512 sql_source: None,
513 description: None,
514 });
515
516 let result = validator.validate(ir);
517 assert!(
518 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("must be Boolean")),
519 "expected Validation error about Boolean requirement, got: {result:?}"
520 );
521 }
522
523 #[test]
524 fn test_validate_group_by_input_valid() {
525 let validator = SchemaValidator::new();
526 let mut ir = AuthoringIR::new();
527
528 ir.types.push(IRType {
529 name: "SalesGroupByInput".to_string(),
530 fields: vec![IRField {
531 name: "category".to_string(),
532 field_type: "Boolean".to_string(),
533 nullable: true,
534 description: None,
535 sql_column: None,
536 }],
537 sql_source: None,
538 description: None,
539 });
540
541 validator.validate(ir).unwrap_or_else(|e| {
542 panic!("validate group by input with Boolean fields should succeed: {e}")
543 });
544 }
545
546 #[test]
547 fn test_validate_having_input_invalid_suffix() {
548 let validator = SchemaValidator::new();
549 let mut ir = AuthoringIR::new();
550
551 ir.types.push(IRType {
552 name: "SalesHavingInput".to_string(),
553 fields: vec![IRField {
554 name: "count".to_string(), field_type: "Int".to_string(),
556 nullable: true,
557 description: None,
558 sql_column: None,
559 }],
560 sql_source: None,
561 description: None,
562 });
563
564 let result = validator.validate(ir);
565 assert!(
566 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("must have operator suffix")),
567 "expected Validation error about operator suffix, got: {result:?}"
568 );
569 }
570
571 #[test]
572 fn test_validate_having_input_valid() {
573 let validator = SchemaValidator::new();
574 let mut ir = AuthoringIR::new();
575
576 ir.types.push(IRType {
577 name: "SalesHavingInput".to_string(),
578 fields: vec![
579 IRField {
580 name: "count_gt".to_string(),
581 field_type: "Int".to_string(),
582 nullable: true,
583 description: None,
584 sql_column: None,
585 },
586 IRField {
587 name: "revenue_sum_gte".to_string(),
588 field_type: "Float".to_string(),
589 nullable: true,
590 description: None,
591 sql_column: None,
592 },
593 ],
594 sql_source: None,
595 description: None,
596 });
597
598 validator.validate(ir).unwrap_or_else(|e| {
599 panic!("validate having input with valid suffixes should succeed: {e}")
600 });
601 }
602
603 #[test]
608 fn test_extract_base_type() {
609 assert_eq!(extract_base_type("String"), "String");
610 assert_eq!(extract_base_type("String!"), "String");
611 assert_eq!(extract_base_type("[String]"), "String");
612 assert_eq!(extract_base_type("[String!]"), "String");
613 assert_eq!(extract_base_type("[String!]!"), "String");
614 assert_eq!(extract_base_type(" User "), "User");
615 }
616
617 #[test]
618 fn test_validate_type_with_valid_references() {
619 let validator = SchemaValidator::new();
620 let mut ir = AuthoringIR::new();
621
622 ir.types.push(IRType {
624 name: "User".to_string(),
625 fields: vec![
626 IRField {
627 name: "id".to_string(),
628 field_type: "ID!".to_string(),
629 nullable: false,
630 description: None,
631 sql_column: None,
632 },
633 IRField {
634 name: "name".to_string(),
635 field_type: "String!".to_string(),
636 nullable: false,
637 description: None,
638 sql_column: None,
639 },
640 ],
641 sql_source: Some("v_user".to_string()),
642 description: None,
643 });
644
645 ir.types.push(IRType {
647 name: "Post".to_string(),
648 fields: vec![
649 IRField {
650 name: "id".to_string(),
651 field_type: "ID!".to_string(),
652 nullable: false,
653 description: None,
654 sql_column: None,
655 },
656 IRField {
657 name: "author".to_string(),
658 field_type: "User".to_string(),
659 nullable: true,
660 description: None,
661 sql_column: None,
662 },
663 ],
664 sql_source: Some("v_post".to_string()),
665 description: None,
666 });
667
668 validator
669 .validate(ir)
670 .unwrap_or_else(|e| panic!("validate type with valid references should succeed: {e}"));
671 }
672
673 #[test]
674 fn test_validate_type_with_invalid_reference() {
675 let validator = SchemaValidator::new();
676 let mut ir = AuthoringIR::new();
677
678 ir.types.push(IRType {
679 name: "Post".to_string(),
680 fields: vec![IRField {
681 name: "author".to_string(),
682 field_type: "NonExistentType".to_string(),
683 nullable: true,
684 description: None,
685 sql_column: None,
686 }],
687 sql_source: None,
688 description: None,
689 });
690
691 let result = validator.validate(ir);
692 assert!(
693 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("references unknown type") && message.contains("NonExistentType")),
694 "expected Validation error about unknown type reference, got: {result:?}"
695 );
696 }
697
698 #[test]
699 fn test_validate_type_empty_name() {
700 let validator = SchemaValidator::new();
701 let mut ir = AuthoringIR::new();
702
703 ir.types.push(IRType {
704 name: String::new(),
705 fields: vec![],
706 sql_source: None,
707 description: None,
708 });
709
710 let result = validator.validate(ir);
711 assert!(
712 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("name cannot be empty")),
713 "expected Validation error about empty type name, got: {result:?}"
714 );
715 }
716
717 #[test]
718 fn test_validate_query_with_valid_return_type() {
719 use super::super::ir::{AutoParams, IRArgument, IRQuery};
720
721 let validator = SchemaValidator::new();
722 let mut ir = AuthoringIR::new();
723
724 ir.types.push(IRType {
726 name: "User".to_string(),
727 fields: vec![IRField {
728 name: "id".to_string(),
729 field_type: "ID!".to_string(),
730 nullable: false,
731 description: None,
732 sql_column: None,
733 }],
734 sql_source: Some("v_user".to_string()),
735 description: None,
736 });
737
738 ir.queries.push(IRQuery {
740 name: "user".to_string(),
741 return_type: "User".to_string(),
742 returns_list: false,
743 nullable: true,
744 arguments: vec![IRArgument {
745 name: "id".to_string(),
746 arg_type: "ID!".to_string(),
747 nullable: false,
748 default_value: None,
749 description: None,
750 }],
751 sql_source: Some("v_user".to_string()),
752 description: None,
753 auto_params: AutoParams::default(),
754 });
755
756 validator.validate(ir).unwrap_or_else(|e| {
757 panic!("validate query with valid return type should succeed: {e}")
758 });
759 }
760
761 #[test]
762 fn test_validate_query_with_invalid_return_type() {
763 use super::super::ir::{AutoParams, IRQuery};
764
765 let validator = SchemaValidator::new();
766 let mut ir = AuthoringIR::new();
767
768 ir.queries.push(IRQuery {
769 name: "unknownQuery".to_string(),
770 return_type: "NonExistentType".to_string(),
771 returns_list: false,
772 nullable: true,
773 arguments: vec![],
774 sql_source: None,
775 description: None,
776 auto_params: AutoParams::default(),
777 });
778
779 let result = validator.validate(ir);
780 assert!(
781 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("returns unknown type") && message.contains("NonExistentType")),
782 "expected Validation error about unknown return type, got: {result:?}"
783 );
784 }
785
786 #[test]
787 fn test_validate_query_with_scalar_return_type() {
788 use super::super::ir::{AutoParams, IRQuery};
789
790 let validator = SchemaValidator::new();
791 let mut ir = AuthoringIR::new();
792
793 ir.queries.push(IRQuery {
795 name: "serverTime".to_string(),
796 return_type: "DateTime".to_string(),
797 returns_list: false,
798 nullable: false,
799 arguments: vec![],
800 sql_source: None,
801 description: None,
802 auto_params: AutoParams::default(),
803 });
804
805 validator.validate(ir).unwrap_or_else(|e| {
806 panic!("validate query with scalar return type should succeed: {e}")
807 });
808 }
809
810 #[test]
811 fn test_validate_query_empty_name() {
812 use super::super::ir::{AutoParams, IRQuery};
813
814 let validator = SchemaValidator::new();
815 let mut ir = AuthoringIR::new();
816
817 ir.queries.push(IRQuery {
818 name: String::new(),
819 return_type: "String".to_string(),
820 returns_list: false,
821 nullable: true,
822 arguments: vec![],
823 sql_source: None,
824 description: None,
825 auto_params: AutoParams::default(),
826 });
827
828 let result = validator.validate(ir);
829 assert!(
830 matches!(&result, Err(FraiseQLError::Validation { message, .. }) if message.contains("Query name cannot be empty")),
831 "expected Validation error about empty query name, got: {result:?}"
832 );
833 }
834
835 #[test]
836 fn test_validate_list_type_references() {
837 let validator = SchemaValidator::new();
838 let mut ir = AuthoringIR::new();
839
840 ir.types.push(IRType {
842 name: "User".to_string(),
843 fields: vec![
844 IRField {
845 name: "id".to_string(),
846 field_type: "ID!".to_string(),
847 nullable: false,
848 description: None,
849 sql_column: None,
850 },
851 IRField {
852 name: "friends".to_string(),
853 field_type: "[User!]".to_string(), nullable: true,
855 description: None,
856 sql_column: None,
857 },
858 ],
859 sql_source: None,
860 description: None,
861 });
862
863 validator
864 .validate(ir)
865 .unwrap_or_else(|e| panic!("validate list type references should succeed: {e}"));
866 }
867
868 #[test]
869 fn test_validate_builtin_scalar_types() {
870 let validator = SchemaValidator::new();
871 let mut ir = AuthoringIR::new();
872
873 ir.types.push(IRType {
875 name: "TestType".to_string(),
876 fields: vec![
877 IRField {
878 name: "id".to_string(),
879 field_type: "ID".to_string(),
880 nullable: true,
881 description: None,
882 sql_column: None,
883 },
884 IRField {
885 name: "name".to_string(),
886 field_type: "String".to_string(),
887 nullable: true,
888 description: None,
889 sql_column: None,
890 },
891 IRField {
892 name: "age".to_string(),
893 field_type: "Int".to_string(),
894 nullable: true,
895 description: None,
896 sql_column: None,
897 },
898 IRField {
899 name: "rating".to_string(),
900 field_type: "Float".to_string(),
901 nullable: true,
902 description: None,
903 sql_column: None,
904 },
905 IRField {
906 name: "active".to_string(),
907 field_type: "Boolean".to_string(),
908 nullable: true,
909 description: None,
910 sql_column: None,
911 },
912 IRField {
913 name: "created".to_string(),
914 field_type: "DateTime".to_string(),
915 nullable: true,
916 description: None,
917 sql_column: None,
918 },
919 IRField {
920 name: "uid".to_string(),
921 field_type: "UUID".to_string(),
922 nullable: true,
923 description: None,
924 sql_column: None,
925 },
926 ],
927 sql_source: None,
928 description: None,
929 });
930
931 validator
932 .validate(ir)
933 .unwrap_or_else(|e| panic!("all builtin scalars should be recognized: {e}"));
934 }
935
936 #[test]
937 fn test_validate_rich_scalar_types() {
938 let validator = SchemaValidator::new();
939 let mut ir = AuthoringIR::new();
940
941 ir.types.push(IRType {
943 name: "Contact".to_string(),
944 fields: vec![
945 IRField {
946 name: "email".to_string(),
947 field_type: "Email".to_string(),
948 nullable: true,
949 description: None,
950 sql_column: None,
951 },
952 IRField {
953 name: "phone".to_string(),
954 field_type: "PhoneNumber".to_string(),
955 nullable: true,
956 description: None,
957 sql_column: None,
958 },
959 IRField {
960 name: "url".to_string(),
961 field_type: "URL".to_string(),
962 nullable: true,
963 description: None,
964 sql_column: None,
965 },
966 IRField {
967 name: "ip".to_string(),
968 field_type: "IPAddress".to_string(),
969 nullable: true,
970 description: None,
971 sql_column: None,
972 },
973 ],
974 sql_source: None,
975 description: None,
976 });
977
978 validator
979 .validate(ir)
980 .unwrap_or_else(|e| panic!("rich scalars should be recognized: {e}"));
981 }
982}