1pub mod err;
18
19use super::{json::err::TypeMismatchError, EntityTypeDescription, Schema, SchemaType};
20use super::{Eid, EntityUID, ExprKind, Literal};
21use crate::ast::{
22 BorrowedRestrictedExpr, Entity, PartialValue, PartialValueToRestrictedExprError, RestrictedExpr,
23};
24use crate::extensions::{ExtensionFunctionLookupError, Extensions};
25use err::{
26 EntitySchemaConformanceError, InvalidEnumEntity, InvalidEnumEntityError, UndeclaredAction,
27};
28use miette::Diagnostic;
29use nonempty::NonEmpty;
30use smol_str::SmolStr;
31use std::collections::{BTreeMap, HashMap};
32use thiserror::Error;
33
34#[derive(Debug, Clone)]
36pub struct EntitySchemaConformanceChecker<'a, S> {
37 schema: &'a S,
39 extensions: &'a Extensions<'a>,
41}
42
43impl<'a, S> EntitySchemaConformanceChecker<'a, S> {
44 pub fn new(schema: &'a S, extensions: &'a Extensions<'a>) -> Self {
46 Self { schema, extensions }
47 }
48}
49
50impl<S: Schema> EntitySchemaConformanceChecker<'_, S> {
51 pub fn validate_action(&self, action: &Entity) -> Result<(), EntitySchemaConformanceError> {
53 let uid = action.uid();
54 let schema_action = self
55 .schema
56 .action(uid)
57 .ok_or_else(|| EntitySchemaConformanceError::undeclared_action(uid.clone()))?;
58 if !action.deep_eq(&schema_action) {
60 return Err(EntitySchemaConformanceError::action_declaration_mismatch(
61 uid.clone(),
62 ));
63 }
64 Ok(())
65 }
66
67 pub fn validate_entity_ancestors<'a>(
69 &self,
70 uid: &EntityUID,
71 ancestors: impl Iterator<Item = &'a EntityUID>,
72 schema_etype: &impl EntityTypeDescription,
73 ) -> Result<(), EntitySchemaConformanceError> {
74 for ancestor_euid in ancestors {
77 validate_euid(self.schema, ancestor_euid)?;
78 let ancestor_type = ancestor_euid.entity_type();
79 if schema_etype.allowed_parent_types().contains(ancestor_type) {
80 } else {
85 return Err(EntitySchemaConformanceError::invalid_ancestor_type(
86 uid.clone(),
87 ancestor_type.clone(),
88 ));
89 }
90 }
91 Ok(())
92 }
93
94 pub fn validate_entity_attributes<'a>(
96 &self,
97 uid: &EntityUID,
98 attrs: impl Iterator<Item = (&'a SmolStr, &'a PartialValue)>,
99 schema_etype: &impl EntityTypeDescription,
100 ) -> Result<(), EntitySchemaConformanceError> {
101 let attrs: HashMap<&SmolStr, &PartialValue> = attrs.collect();
102 for required_attr in schema_etype.required_attrs() {
105 if !attrs.contains_key(&required_attr) {
106 return Err(EntitySchemaConformanceError::missing_entity_attr(
107 uid.clone(),
108 required_attr,
109 ));
110 }
111 }
112 for (attr, val) in attrs {
115 match schema_etype.attr_type(attr) {
116 None => {
117 if !schema_etype.open_attributes() {
120 return Err(EntitySchemaConformanceError::unexpected_entity_attr(
121 uid.clone(),
122 attr.clone(),
123 ));
124 }
125 }
126 Some(expected_ty) => {
127 match typecheck_value_against_schematype(val, &expected_ty, self.extensions) {
130 Ok(()) => {} Err(TypecheckError::TypeMismatch(err)) => {
132 return Err(EntitySchemaConformanceError::type_mismatch(
133 uid.clone(),
134 attr.clone(),
135 err::AttrOrTag::Attr,
136 err,
137 ));
138 }
139 Err(TypecheckError::ExtensionFunctionLookup(err)) => {
140 return Err(EntitySchemaConformanceError::extension_function_lookup(
141 uid.clone(),
142 attr.clone(),
143 err::AttrOrTag::Attr,
144 err,
145 ));
146 }
147 };
148 }
149 }
150 validate_euids_in_partial_value(self.schema, val)?;
151 }
152 Ok(())
153 }
154
155 pub fn validate_tags<'a>(
157 &self,
158 uid: &EntityUID,
159 tags: impl Iterator<Item = (&'a SmolStr, &'a PartialValue)>,
160 schema_etype: &impl EntityTypeDescription,
161 ) -> Result<(), EntitySchemaConformanceError> {
162 let tags: HashMap<&SmolStr, &PartialValue> = tags.collect();
163 match schema_etype.tag_type() {
164 None => {
165 if let Some((k, _)) = tags.iter().next() {
166 return Err(EntitySchemaConformanceError::unexpected_entity_tag(
167 uid.clone(),
168 k.to_string(),
169 ));
170 }
171 }
172 Some(expected_ty) => {
173 for (tag, val) in &tags {
174 match typecheck_value_against_schematype(val, &expected_ty, self.extensions) {
175 Ok(()) => {} Err(TypecheckError::TypeMismatch(err)) => {
177 return Err(EntitySchemaConformanceError::type_mismatch(
178 uid.clone(),
179 tag.to_string(),
180 err::AttrOrTag::Tag,
181 err,
182 ));
183 }
184 Err(TypecheckError::ExtensionFunctionLookup(err)) => {
185 return Err(EntitySchemaConformanceError::extension_function_lookup(
186 uid.clone(),
187 tag.to_string(),
188 err::AttrOrTag::Tag,
189 err,
190 ));
191 }
192 }
193 }
194 }
195 }
196 for val in tags.values() {
197 validate_euids_in_partial_value(self.schema, val)?;
198 }
199 Ok(())
200 }
201
202 pub fn validate_entity(&self, entity: &Entity) -> Result<(), EntitySchemaConformanceError> {
205 let uid = entity.uid();
206 let etype = uid.entity_type();
207 if etype.is_action() {
208 self.validate_action(entity)?;
209 } else {
210 let schema_etype = self.schema.entity_type(etype).ok_or_else(|| {
211 EntitySchemaConformanceError::unexpected_entity_type(self.schema, uid.clone())
212 })?;
213
214 validate_euid(self.schema, uid)?;
215 self.validate_entity_attributes(uid, entity.attrs(), &schema_etype)?;
216 self.validate_entity_ancestors(uid, entity.ancestors(), &schema_etype)?;
217 self.validate_tags(uid, entity.tags(), &schema_etype)?;
218 }
219 Ok(())
220 }
221}
222
223pub fn is_valid_enumerated_entity(
225 choices: &NonEmpty<Eid>,
226 uid: &EntityUID,
227) -> Result<(), InvalidEnumEntityError> {
228 choices
229 .iter()
230 .any(|id| uid.eid() == id)
231 .then_some(())
232 .ok_or_else(|| InvalidEnumEntityError {
233 uid: uid.clone(),
234 choices: choices.clone(),
235 })
236}
237
238#[derive(Debug, Error, Diagnostic)]
242pub enum ValidateEuidError {
243 #[error(transparent)]
245 #[diagnostic(transparent)]
246 InvalidEnumEntity(#[from] InvalidEnumEntityError),
247 #[error(transparent)]
249 #[diagnostic(transparent)]
250 UndeclaredAction(#[from] UndeclaredAction),
251}
252
253impl From<ValidateEuidError> for EntitySchemaConformanceError {
254 fn from(e: ValidateEuidError) -> Self {
255 match e {
256 ValidateEuidError::InvalidEnumEntity(e) => InvalidEnumEntity::from(e).into(),
257 ValidateEuidError::UndeclaredAction(e) => e.into(),
258 }
259 }
260}
261
262pub fn validate_euid(schema: &impl Schema, euid: &EntityUID) -> Result<(), ValidateEuidError> {
268 let entity_type = euid.entity_type();
269 if let Some(desc) = schema.entity_type(entity_type) {
270 if let Some(choices) = desc.enum_entity_eids() {
271 is_valid_enumerated_entity(choices, euid)?;
272 }
273 }
274 if entity_type.is_action() && schema.action(euid).is_none() {
275 return Err(ValidateEuidError::UndeclaredAction(UndeclaredAction {
276 uid: euid.clone(),
277 }));
278 }
279 Ok(())
280}
281
282fn validate_euids_in_subexpressions<'a>(
283 exprs: impl IntoIterator<Item = &'a crate::ast::Expr>,
284 schema: &impl Schema,
285) -> std::result::Result<(), ValidateEuidError> {
286 exprs.into_iter().try_for_each(|e| match e.expr_kind() {
287 ExprKind::Lit(Literal::EntityUID(euid)) => validate_euid(schema, euid.as_ref()),
288 _ => Ok(()),
289 })
290}
291
292pub fn validate_euids_in_partial_value(
294 schema: &impl Schema,
295 val: &PartialValue,
296) -> Result<(), ValidateEuidError> {
297 match val {
298 PartialValue::Value(val) => validate_euids_in_subexpressions(
299 RestrictedExpr::from(val.clone()).subexpressions(),
300 schema,
301 ),
302 PartialValue::Residual(e) => validate_euids_in_subexpressions(e.subexpressions(), schema),
303 }
304}
305
306pub fn typecheck_value_against_schematype(
310 value: &PartialValue,
311 expected_ty: &SchemaType,
312 extensions: &Extensions<'_>,
313) -> Result<(), TypecheckError> {
314 match RestrictedExpr::try_from(value.clone()) {
315 Ok(expr) => typecheck_restricted_expr_against_schematype(
316 expr.as_borrowed(),
317 expected_ty,
318 extensions,
319 ),
320 Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
321 Ok(())
329 }
330 }
331}
332
333pub fn typecheck_restricted_expr_against_schematype(
339 expr: BorrowedRestrictedExpr<'_>,
340 expected_ty: &SchemaType,
341 extensions: &Extensions<'_>,
342) -> Result<(), TypecheckError> {
343 use SchemaType::*;
344 let type_mismatch_err = || {
345 Err(TypeMismatchError::type_mismatch(
346 expected_ty.clone(),
347 expr.try_type_of(extensions),
348 expr.to_owned(),
349 )
350 .into())
351 };
352
353 match expr.expr_kind() {
354 ExprKind::Unknown(u) => match u.type_annotation.clone().and_then(SchemaType::from_ty) {
359 Some(ty) => {
360 if &ty == expected_ty {
361 return Ok(());
362 } else {
363 return type_mismatch_err();
364 }
365 }
366 None => return Ok(()),
367 },
368 ExprKind::ExtensionFunctionApp { fn_name, .. } => {
373 return match extensions.func(fn_name)?.return_type() {
374 None => {
375 Ok(())
378 }
379 Some(rty) => {
380 if rty == expected_ty {
381 Ok(())
382 } else {
383 type_mismatch_err()
384 }
385 }
386 };
387 }
388 _ => (),
389 };
390
391 match expected_ty {
399 Bool => {
400 if expr.as_bool().is_some() {
401 Ok(())
402 } else {
403 type_mismatch_err()
404 }
405 }
406 Long => {
407 if expr.as_long().is_some() {
408 Ok(())
409 } else {
410 type_mismatch_err()
411 }
412 }
413 String => {
414 if expr.as_string().is_some() {
415 Ok(())
416 } else {
417 type_mismatch_err()
418 }
419 }
420 EmptySet => {
421 if expr.as_set_elements().is_some_and(|e| e.count() == 0) {
422 Ok(())
423 } else {
424 type_mismatch_err()
425 }
426 }
427 Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => Ok(()),
428 Set { element_ty: elty } => match expr.as_set_elements() {
429 Some(mut els) => els.try_for_each(|e| {
430 typecheck_restricted_expr_against_schematype(e, elty, extensions)
431 }),
432 None => type_mismatch_err(),
433 },
434 Record { attrs, open_attrs } => match expr.as_record_pairs() {
435 Some(pairs) => {
436 let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
437 attrs.iter().try_for_each(|(k, v)| {
440 if !v.required {
441 Ok(())
442 } else {
443 match pairs_map.get(k) {
444 Some(inner_e) => typecheck_restricted_expr_against_schematype(
445 *inner_e,
446 &v.attr_type,
447 extensions,
448 ),
449 None => Err(TypeMismatchError::missing_required_attr(
450 expected_ty.clone(),
451 k.clone(),
452 expr.to_owned(),
453 )
454 .into()),
455 }
456 }
457 })?;
458 pairs_map
461 .iter()
462 .try_for_each(|(k, inner_e)| match attrs.get(*k) {
463 Some(sch_ty) => typecheck_restricted_expr_against_schematype(
464 *inner_e,
465 &sch_ty.attr_type,
466 extensions,
467 ),
468 None => {
469 if *open_attrs {
470 Ok(())
471 } else {
472 Err(TypeMismatchError::unexpected_attr(
473 expected_ty.clone(),
474 (*k).clone(),
475 expr.to_owned(),
476 )
477 .into())
478 }
479 }
480 })?;
481 Ok(())
482 }
483 None => type_mismatch_err(),
484 },
485 Extension { .. } => type_mismatch_err(),
487 Entity { ty } => match expr.as_euid() {
488 Some(actual_euid) if actual_euid.entity_type() == ty => Ok(()),
489 _ => type_mismatch_err(),
490 },
491 }
492}
493
494#[derive(Debug, Diagnostic, Error)]
497pub enum TypecheckError {
498 #[error(transparent)]
500 #[diagnostic(transparent)]
501 TypeMismatch(#[from] TypeMismatchError),
502 #[error(transparent)]
509 #[diagnostic(transparent)]
510 ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
511}
512
513#[cfg(test)]
514mod test_typecheck {
515 use std::collections::BTreeMap;
516
517 use cool_asserts::assert_matches;
518 use miette::Report;
519 use smol_str::ToSmolStr;
520
521 use crate::{
522 entities::{
523 conformance::TypecheckError, AttributeType, BorrowedRestrictedExpr, Expr, SchemaType,
524 Unknown,
525 },
526 extensions::Extensions,
527 test_utils::{expect_err, ExpectedErrorMessageBuilder},
528 };
529
530 use super::typecheck_restricted_expr_against_schematype;
531
532 #[test]
533 fn unknown() {
534 typecheck_restricted_expr_against_schematype(
535 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
536 &SchemaType::Bool,
537 Extensions::all_available(),
538 )
539 .unwrap();
540 typecheck_restricted_expr_against_schematype(
541 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
542 &SchemaType::String,
543 Extensions::all_available(),
544 )
545 .unwrap();
546 typecheck_restricted_expr_against_schematype(
547 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
548 &SchemaType::Set {
549 element_ty: Box::new(SchemaType::Extension {
550 name: "decimal".parse().unwrap(),
551 }),
552 },
553 Extensions::all_available(),
554 )
555 .unwrap();
556 }
557
558 #[test]
559 fn bool() {
560 typecheck_restricted_expr_against_schematype(
561 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
562 &SchemaType::Bool,
563 Extensions::all_available(),
564 )
565 .unwrap();
566 }
567
568 #[test]
569 fn bool_fails() {
570 assert_matches!(
571 typecheck_restricted_expr_against_schematype(
572 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
573 &SchemaType::Bool,
574 Extensions::all_available(),
575 ),
576 Err(e@TypecheckError::TypeMismatch(_)) => {
577 expect_err(
578 "",
579 &Report::new(e),
580 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type bool, but it actually has type long: `1`").build()
581 );
582 }
583 )
584 }
585
586 #[test]
587 fn long() {
588 typecheck_restricted_expr_against_schematype(
589 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
590 &SchemaType::Long,
591 Extensions::all_available(),
592 )
593 .unwrap();
594 }
595
596 #[test]
597 fn long_fails() {
598 assert_matches!(
599 typecheck_restricted_expr_against_schematype(
600 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
601 &SchemaType::Long,
602 Extensions::all_available(),
603 ),
604 Err(e@TypecheckError::TypeMismatch(_)) => {
605 expect_err(
606 "",
607 &Report::new(e),
608 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
609 );
610 }
611 )
612 }
613
614 #[test]
615 fn string() {
616 typecheck_restricted_expr_against_schematype(
617 BorrowedRestrictedExpr::new(&r#""foo""#.parse().unwrap()).unwrap(),
618 &SchemaType::String,
619 Extensions::all_available(),
620 )
621 .unwrap();
622 }
623
624 #[test]
625 fn string_fails() {
626 assert_matches!(
627 typecheck_restricted_expr_against_schematype(
628 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
629 &SchemaType::String,
630 Extensions::all_available(),
631 ),
632 Err(e@TypecheckError::TypeMismatch(_)) => {
633 expect_err(
634 "",
635 &Report::new(e),
636 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type bool: `false`").build()
637 );
638 }
639 )
640 }
641
642 #[test]
643 fn test_typecheck_set() {
644 typecheck_restricted_expr_against_schematype(
645 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
646 &SchemaType::Set {
647 element_ty: Box::new(SchemaType::Long),
648 },
649 Extensions::all_available(),
650 )
651 .unwrap();
652 typecheck_restricted_expr_against_schematype(
653 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
654 &SchemaType::Set {
655 element_ty: Box::new(SchemaType::Bool),
656 },
657 Extensions::all_available(),
658 )
659 .unwrap();
660 }
661
662 #[test]
663 fn test_typecheck_set_fails() {
664 assert_matches!(
665 typecheck_restricted_expr_against_schematype(
666 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
667 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
668 Extensions::all_available(),
669 ),
670 Err(e@TypecheckError::TypeMismatch(_)) => {
671 expect_err(
672 "",
673 &Report::new(e),
674 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type [string], but it actually has type record: `{}`").build()
675 );
676 }
677 );
678 assert_matches!(
679 typecheck_restricted_expr_against_schematype(
680 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
681 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
682 Extensions::all_available(),
683 ),
684 Err(e@TypecheckError::TypeMismatch(_)) => {
685 expect_err(
686 "",
687 &Report::new(e),
688 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type long: `1`").build()
689 );
690 }
691 );
692 assert_matches!(
693 typecheck_restricted_expr_against_schematype(
694 BorrowedRestrictedExpr::new(&"[1, true]".parse().unwrap()).unwrap(),
695 &SchemaType::Set { element_ty: Box::new(SchemaType::Long) },
696 Extensions::all_available(),
697 ),
698 Err(e@TypecheckError::TypeMismatch(_)) => {
699 expect_err(
700 "",
701 &Report::new(e),
702 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `true`").build()
703 );
704 }
705 )
706 }
707
708 #[test]
709 fn test_typecheck_record() {
710 typecheck_restricted_expr_against_schematype(
711 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
712 &SchemaType::Record {
713 attrs: BTreeMap::new(),
714 open_attrs: false,
715 },
716 Extensions::all_available(),
717 )
718 .unwrap();
719 typecheck_restricted_expr_against_schematype(
720 BorrowedRestrictedExpr::new(&"{a: 1}".parse().unwrap()).unwrap(),
721 &SchemaType::Record {
722 attrs: BTreeMap::from([(
723 "a".to_smolstr(),
724 AttributeType {
725 attr_type: SchemaType::Long,
726 required: true,
727 },
728 )]),
729 open_attrs: false,
730 },
731 Extensions::all_available(),
732 )
733 .unwrap();
734 typecheck_restricted_expr_against_schematype(
735 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
736 &SchemaType::Record {
737 attrs: BTreeMap::from([(
738 "a".to_smolstr(),
739 AttributeType {
740 attr_type: SchemaType::Long,
741 required: false,
742 },
743 )]),
744 open_attrs: false,
745 },
746 Extensions::all_available(),
747 )
748 .unwrap();
749 }
750
751 #[test]
752 fn test_typecheck_record_fails() {
753 assert_matches!(
754 typecheck_restricted_expr_against_schematype(
755 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
756 &SchemaType::Record { attrs: BTreeMap::from([]), open_attrs: false },
757 Extensions::all_available(),
758 ),
759 Err(e@TypecheckError::TypeMismatch(_)) => {
760 expect_err(
761 "",
762 &Report::new(e),
763 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type { }, but it actually has type set: `[]`").build()
764 );
765 }
766 );
767 assert_matches!(
768 typecheck_restricted_expr_against_schematype(
769 BorrowedRestrictedExpr::new(&"{a: false}".parse().unwrap()).unwrap(),
770 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
771 Extensions::all_available(),
772 ),
773 Err(e@TypecheckError::TypeMismatch(_)) => {
774 expect_err(
775 "",
776 &Report::new(e),
777 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
778 );
779 }
780 );
781 assert_matches!(
782 typecheck_restricted_expr_against_schematype(
783 BorrowedRestrictedExpr::new(&"{a: {}}".parse().unwrap()).unwrap(),
784 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
785 Extensions::all_available(),
786 ),
787 Err(e@TypecheckError::TypeMismatch(_)) => {
788 expect_err(
789 "",
790 &Report::new(e),
791 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type record: `{}`").build()
792 );
793 }
794 );
795 assert_matches!(
796 typecheck_restricted_expr_against_schematype(
797 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
798 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
799 Extensions::all_available(),
800 ),
801 Err(e@TypecheckError::TypeMismatch(_)) => {
802 expect_err(
803 "",
804 &Report::new(e),
805 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it is missing the required attribute `a`: `{}`"#).build()
806 );
807 }
808 );
809 assert_matches!(
810 typecheck_restricted_expr_against_schematype(
811 BorrowedRestrictedExpr::new(&"{a: 1, b: 1}".parse().unwrap()).unwrap(),
812 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
813 Extensions::all_available(),
814 ),
815 Err(e@TypecheckError::TypeMismatch(_)) => {
816 expect_err(
817 "",
818 &Report::new(e),
819 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it contains an unexpected attribute `b`: `{"a": 1, "b": 1}`"#).build()
820 );
821 }
822 );
823 assert_matches!(
824 typecheck_restricted_expr_against_schematype(
825 BorrowedRestrictedExpr::new(&"{b: 1}".parse().unwrap()).unwrap(),
826 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
827 Extensions::all_available(),
828 ),
829 Err(e@TypecheckError::TypeMismatch(_)) => {
830 expect_err(
831 "",
832 &Report::new(e),
833 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (optional) long }, but it contains an unexpected attribute `b`: `{"b": 1}`"#).build()
834 );
835 }
836 );
837 }
838
839 #[test]
840 fn extension() {
841 typecheck_restricted_expr_against_schematype(
842 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
843 &SchemaType::Extension {
844 name: "decimal".parse().unwrap(),
845 },
846 Extensions::all_available(),
847 )
848 .unwrap();
849 }
850
851 #[test]
852 fn non_constructor_extension_function() {
853 typecheck_restricted_expr_against_schematype(
854 BorrowedRestrictedExpr::new(&r#"ip("127.0.0.1").isLoopback()"#.parse().unwrap())
855 .unwrap(),
856 &SchemaType::Bool,
857 Extensions::all_available(),
858 )
859 .unwrap();
860 }
861
862 #[test]
863 fn extension_fails() {
864 assert_matches!(
865 typecheck_restricted_expr_against_schematype(
866 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
867 &SchemaType::Extension { name: "ipaddr".parse().unwrap() },
868 Extensions::all_available(),
869 ),
870 Err(e@TypecheckError::TypeMismatch(_)) => {
871 expect_err(
872 "",
873 &Report::new(e),
874 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("1.1")`"#).build()
875 );
876 }
877 )
878 }
879
880 #[test]
881 fn entity() {
882 typecheck_restricted_expr_against_schematype(
883 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
884 &SchemaType::Entity {
885 ty: "User".parse().unwrap(),
886 },
887 Extensions::all_available(),
888 )
889 .unwrap();
890 }
891
892 #[test]
893 fn entity_fails() {
894 assert_matches!(
895 typecheck_restricted_expr_against_schematype(
896 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
897 &SchemaType::Entity { ty: "Photo".parse().unwrap() },
898 Extensions::all_available(),
899 ),
900 Err(e@TypecheckError::TypeMismatch(_)) => {
901 expect_err(
902 "",
903 &Report::new(e),
904 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type `Photo`, but it actually has type (entity of type `User`): `User::"alice"`"#).build()
905 );
906 }
907 )
908 }
909}