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 UnexpectedEntityTypeError,
28};
29use miette::Diagnostic;
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 let suggested_types = self
212 .schema
213 .entity_types_with_basename(&etype.name().basename())
214 .collect();
215 UnexpectedEntityTypeError {
216 uid: uid.clone(),
217 suggested_types,
218 }
219 })?;
220
221 validate_euid(self.schema, uid)?;
222 self.validate_entity_attributes(uid, entity.attrs(), &schema_etype)?;
223 self.validate_entity_ancestors(uid, entity.ancestors(), &schema_etype)?;
224 self.validate_tags(uid, entity.tags(), &schema_etype)?;
225 }
226 Ok(())
227 }
228}
229
230pub fn is_valid_enumerated_entity(
232 choices: &[Eid],
233 uid: &EntityUID,
234) -> Result<(), InvalidEnumEntityError> {
235 choices
236 .iter()
237 .find(|id| uid.eid() == *id)
238 .ok_or_else(|| InvalidEnumEntityError {
239 uid: uid.clone(),
240 choices: choices.to_vec(),
241 })
242 .map(|_| ())
243}
244
245#[derive(Debug, Error, Diagnostic)]
249pub enum ValidateEuidError {
250 #[error(transparent)]
252 #[diagnostic(transparent)]
253 InvalidEnumEntity(#[from] InvalidEnumEntityError),
254 #[error(transparent)]
256 #[diagnostic(transparent)]
257 UndeclaredAction(#[from] UndeclaredAction),
258}
259
260impl From<ValidateEuidError> for EntitySchemaConformanceError {
261 fn from(e: ValidateEuidError) -> Self {
262 match e {
263 ValidateEuidError::InvalidEnumEntity(e) => InvalidEnumEntity::from(e).into(),
264 ValidateEuidError::UndeclaredAction(e) => e.into(),
265 }
266 }
267}
268
269pub fn validate_euid(schema: &impl Schema, euid: &EntityUID) -> Result<(), ValidateEuidError> {
275 let entity_type = euid.entity_type();
276 if let Some(desc) = schema.entity_type(entity_type) {
277 if let Some(choices) = desc.enum_entity_eids() {
278 is_valid_enumerated_entity(&Vec::from(choices), euid)?;
279 }
280 }
281 if entity_type.is_action() && schema.action(euid).is_none() {
282 return Err(ValidateEuidError::UndeclaredAction(UndeclaredAction {
283 uid: euid.clone(),
284 }));
285 }
286 Ok(())
287}
288
289fn validate_euids_in_subexpressions<'a>(
290 exprs: impl IntoIterator<Item = &'a crate::ast::Expr>,
291 schema: &impl Schema,
292) -> std::result::Result<(), ValidateEuidError> {
293 exprs.into_iter().try_for_each(|e| match e.expr_kind() {
294 ExprKind::Lit(Literal::EntityUID(euid)) => validate_euid(schema, euid.as_ref()),
295 _ => Ok(()),
296 })
297}
298
299pub fn validate_euids_in_partial_value(
301 schema: &impl Schema,
302 val: &PartialValue,
303) -> Result<(), ValidateEuidError> {
304 match val {
305 PartialValue::Value(val) => validate_euids_in_subexpressions(
306 RestrictedExpr::from(val.clone()).subexpressions(),
307 schema,
308 ),
309 PartialValue::Residual(e) => validate_euids_in_subexpressions(e.subexpressions(), schema),
310 }
311}
312
313pub fn typecheck_value_against_schematype(
317 value: &PartialValue,
318 expected_ty: &SchemaType,
319 extensions: &Extensions<'_>,
320) -> Result<(), TypecheckError> {
321 match RestrictedExpr::try_from(value.clone()) {
322 Ok(expr) => typecheck_restricted_expr_against_schematype(
323 expr.as_borrowed(),
324 expected_ty,
325 extensions,
326 ),
327 Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
328 Ok(())
336 }
337 }
338}
339
340pub fn typecheck_restricted_expr_against_schematype(
346 expr: BorrowedRestrictedExpr<'_>,
347 expected_ty: &SchemaType,
348 extensions: &Extensions<'_>,
349) -> Result<(), TypecheckError> {
350 use SchemaType::*;
351 let type_mismatch_err = || {
352 Err(TypeMismatchError::type_mismatch(
353 expected_ty.clone(),
354 expr.try_type_of(extensions),
355 expr.to_owned(),
356 )
357 .into())
358 };
359
360 match expr.expr_kind() {
361 ExprKind::Unknown(u) => match u.type_annotation.clone().and_then(SchemaType::from_ty) {
366 Some(ty) => {
367 if &ty == expected_ty {
368 return Ok(());
369 } else {
370 return type_mismatch_err();
371 }
372 }
373 None => return Ok(()),
374 },
375 ExprKind::ExtensionFunctionApp { fn_name, .. } => {
380 return match extensions.func(fn_name)?.return_type() {
381 None => {
382 Ok(())
385 }
386 Some(rty) => {
387 if rty == expected_ty {
388 Ok(())
389 } else {
390 type_mismatch_err()
391 }
392 }
393 };
394 }
395 _ => (),
396 };
397
398 match expected_ty {
406 Bool => {
407 if expr.as_bool().is_some() {
408 Ok(())
409 } else {
410 type_mismatch_err()
411 }
412 }
413 Long => {
414 if expr.as_long().is_some() {
415 Ok(())
416 } else {
417 type_mismatch_err()
418 }
419 }
420 String => {
421 if expr.as_string().is_some() {
422 Ok(())
423 } else {
424 type_mismatch_err()
425 }
426 }
427 EmptySet => {
428 if expr.as_set_elements().is_some_and(|e| e.count() == 0) {
429 Ok(())
430 } else {
431 type_mismatch_err()
432 }
433 }
434 Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => Ok(()),
435 Set { element_ty: elty } => match expr.as_set_elements() {
436 Some(mut els) => els.try_for_each(|e| {
437 typecheck_restricted_expr_against_schematype(e, elty, extensions)
438 }),
439 None => type_mismatch_err(),
440 },
441 Record { attrs, open_attrs } => match expr.as_record_pairs() {
442 Some(pairs) => {
443 let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
444 attrs.iter().try_for_each(|(k, v)| {
447 if !v.required {
448 Ok(())
449 } else {
450 match pairs_map.get(k) {
451 Some(inner_e) => typecheck_restricted_expr_against_schematype(
452 *inner_e,
453 &v.attr_type,
454 extensions,
455 ),
456 None => Err(TypeMismatchError::missing_required_attr(
457 expected_ty.clone(),
458 k.clone(),
459 expr.to_owned(),
460 )
461 .into()),
462 }
463 }
464 })?;
465 pairs_map
468 .iter()
469 .try_for_each(|(k, inner_e)| match attrs.get(*k) {
470 Some(sch_ty) => typecheck_restricted_expr_against_schematype(
471 *inner_e,
472 &sch_ty.attr_type,
473 extensions,
474 ),
475 None => {
476 if *open_attrs {
477 Ok(())
478 } else {
479 Err(TypeMismatchError::unexpected_attr(
480 expected_ty.clone(),
481 (*k).clone(),
482 expr.to_owned(),
483 )
484 .into())
485 }
486 }
487 })?;
488 Ok(())
489 }
490 None => type_mismatch_err(),
491 },
492 Extension { .. } => type_mismatch_err(),
494 Entity { ty } => match expr.as_euid() {
495 Some(actual_euid) if actual_euid.entity_type() == ty => Ok(()),
496 _ => type_mismatch_err(),
497 },
498 }
499}
500
501#[derive(Debug, Diagnostic, Error)]
504pub enum TypecheckError {
505 #[error(transparent)]
507 #[diagnostic(transparent)]
508 TypeMismatch(#[from] TypeMismatchError),
509 #[error(transparent)]
516 #[diagnostic(transparent)]
517 ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
518}
519
520#[cfg(test)]
521mod test_typecheck {
522 use std::collections::BTreeMap;
523
524 use cool_asserts::assert_matches;
525 use miette::Report;
526 use smol_str::ToSmolStr;
527
528 use crate::{
529 entities::{
530 conformance::TypecheckError, AttributeType, BorrowedRestrictedExpr, Expr, SchemaType,
531 Unknown,
532 },
533 extensions::Extensions,
534 test_utils::{expect_err, ExpectedErrorMessageBuilder},
535 };
536
537 use super::typecheck_restricted_expr_against_schematype;
538
539 #[test]
540 fn unknown() {
541 typecheck_restricted_expr_against_schematype(
542 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
543 &SchemaType::Bool,
544 Extensions::all_available(),
545 )
546 .unwrap();
547 typecheck_restricted_expr_against_schematype(
548 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
549 &SchemaType::String,
550 Extensions::all_available(),
551 )
552 .unwrap();
553 typecheck_restricted_expr_against_schematype(
554 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
555 &SchemaType::Set {
556 element_ty: Box::new(SchemaType::Extension {
557 name: "decimal".parse().unwrap(),
558 }),
559 },
560 Extensions::all_available(),
561 )
562 .unwrap();
563 }
564
565 #[test]
566 fn bool() {
567 typecheck_restricted_expr_against_schematype(
568 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
569 &SchemaType::Bool,
570 Extensions::all_available(),
571 )
572 .unwrap();
573 }
574
575 #[test]
576 fn bool_fails() {
577 assert_matches!(
578 typecheck_restricted_expr_against_schematype(
579 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
580 &SchemaType::Bool,
581 Extensions::all_available(),
582 ),
583 Err(e@TypecheckError::TypeMismatch(_)) => {
584 expect_err(
585 "",
586 &Report::new(e),
587 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type bool, but it actually has type long: `1`").build()
588 );
589 }
590 )
591 }
592
593 #[test]
594 fn long() {
595 typecheck_restricted_expr_against_schematype(
596 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
597 &SchemaType::Long,
598 Extensions::all_available(),
599 )
600 .unwrap();
601 }
602
603 #[test]
604 fn long_fails() {
605 assert_matches!(
606 typecheck_restricted_expr_against_schematype(
607 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
608 &SchemaType::Long,
609 Extensions::all_available(),
610 ),
611 Err(e@TypecheckError::TypeMismatch(_)) => {
612 expect_err(
613 "",
614 &Report::new(e),
615 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
616 );
617 }
618 )
619 }
620
621 #[test]
622 fn string() {
623 typecheck_restricted_expr_against_schematype(
624 BorrowedRestrictedExpr::new(&r#""foo""#.parse().unwrap()).unwrap(),
625 &SchemaType::String,
626 Extensions::all_available(),
627 )
628 .unwrap();
629 }
630
631 #[test]
632 fn string_fails() {
633 assert_matches!(
634 typecheck_restricted_expr_against_schematype(
635 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
636 &SchemaType::String,
637 Extensions::all_available(),
638 ),
639 Err(e@TypecheckError::TypeMismatch(_)) => {
640 expect_err(
641 "",
642 &Report::new(e),
643 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type bool: `false`").build()
644 );
645 }
646 )
647 }
648
649 #[test]
650 fn test_typecheck_set() {
651 typecheck_restricted_expr_against_schematype(
652 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
653 &SchemaType::Set {
654 element_ty: Box::new(SchemaType::Long),
655 },
656 Extensions::all_available(),
657 )
658 .unwrap();
659 typecheck_restricted_expr_against_schematype(
660 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
661 &SchemaType::Set {
662 element_ty: Box::new(SchemaType::Bool),
663 },
664 Extensions::all_available(),
665 )
666 .unwrap();
667 }
668
669 #[test]
670 fn test_typecheck_set_fails() {
671 assert_matches!(
672 typecheck_restricted_expr_against_schematype(
673 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
674 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
675 Extensions::all_available(),
676 ),
677 Err(e@TypecheckError::TypeMismatch(_)) => {
678 expect_err(
679 "",
680 &Report::new(e),
681 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type [string], but it actually has type record: `{}`").build()
682 );
683 }
684 );
685 assert_matches!(
686 typecheck_restricted_expr_against_schematype(
687 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
688 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
689 Extensions::all_available(),
690 ),
691 Err(e@TypecheckError::TypeMismatch(_)) => {
692 expect_err(
693 "",
694 &Report::new(e),
695 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type long: `1`").build()
696 );
697 }
698 );
699 assert_matches!(
700 typecheck_restricted_expr_against_schematype(
701 BorrowedRestrictedExpr::new(&"[1, true]".parse().unwrap()).unwrap(),
702 &SchemaType::Set { element_ty: Box::new(SchemaType::Long) },
703 Extensions::all_available(),
704 ),
705 Err(e@TypecheckError::TypeMismatch(_)) => {
706 expect_err(
707 "",
708 &Report::new(e),
709 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `true`").build()
710 );
711 }
712 )
713 }
714
715 #[test]
716 fn test_typecheck_record() {
717 typecheck_restricted_expr_against_schematype(
718 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
719 &SchemaType::Record {
720 attrs: BTreeMap::new(),
721 open_attrs: false,
722 },
723 Extensions::all_available(),
724 )
725 .unwrap();
726 typecheck_restricted_expr_against_schematype(
727 BorrowedRestrictedExpr::new(&"{a: 1}".parse().unwrap()).unwrap(),
728 &SchemaType::Record {
729 attrs: BTreeMap::from([(
730 "a".to_smolstr(),
731 AttributeType {
732 attr_type: SchemaType::Long,
733 required: true,
734 },
735 )]),
736 open_attrs: false,
737 },
738 Extensions::all_available(),
739 )
740 .unwrap();
741 typecheck_restricted_expr_against_schematype(
742 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
743 &SchemaType::Record {
744 attrs: BTreeMap::from([(
745 "a".to_smolstr(),
746 AttributeType {
747 attr_type: SchemaType::Long,
748 required: false,
749 },
750 )]),
751 open_attrs: false,
752 },
753 Extensions::all_available(),
754 )
755 .unwrap();
756 }
757
758 #[test]
759 fn test_typecheck_record_fails() {
760 assert_matches!(
761 typecheck_restricted_expr_against_schematype(
762 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
763 &SchemaType::Record { attrs: BTreeMap::from([]), open_attrs: false },
764 Extensions::all_available(),
765 ),
766 Err(e@TypecheckError::TypeMismatch(_)) => {
767 expect_err(
768 "",
769 &Report::new(e),
770 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type { }, but it actually has type set: `[]`").build()
771 );
772 }
773 );
774 assert_matches!(
775 typecheck_restricted_expr_against_schematype(
776 BorrowedRestrictedExpr::new(&"{a: false}".parse().unwrap()).unwrap(),
777 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
778 Extensions::all_available(),
779 ),
780 Err(e@TypecheckError::TypeMismatch(_)) => {
781 expect_err(
782 "",
783 &Report::new(e),
784 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
785 );
786 }
787 );
788 assert_matches!(
789 typecheck_restricted_expr_against_schematype(
790 BorrowedRestrictedExpr::new(&"{a: {}}".parse().unwrap()).unwrap(),
791 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
792 Extensions::all_available(),
793 ),
794 Err(e@TypecheckError::TypeMismatch(_)) => {
795 expect_err(
796 "",
797 &Report::new(e),
798 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type record: `{}`").build()
799 );
800 }
801 );
802 assert_matches!(
803 typecheck_restricted_expr_against_schematype(
804 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
805 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
806 Extensions::all_available(),
807 ),
808 Err(e@TypecheckError::TypeMismatch(_)) => {
809 expect_err(
810 "",
811 &Report::new(e),
812 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it is missing the required attribute `a`: `{}`"#).build()
813 );
814 }
815 );
816 assert_matches!(
817 typecheck_restricted_expr_against_schematype(
818 BorrowedRestrictedExpr::new(&"{a: 1, b: 1}".parse().unwrap()).unwrap(),
819 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
820 Extensions::all_available(),
821 ),
822 Err(e@TypecheckError::TypeMismatch(_)) => {
823 expect_err(
824 "",
825 &Report::new(e),
826 &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()
827 );
828 }
829 );
830 assert_matches!(
831 typecheck_restricted_expr_against_schematype(
832 BorrowedRestrictedExpr::new(&"{b: 1}".parse().unwrap()).unwrap(),
833 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
834 Extensions::all_available(),
835 ),
836 Err(e@TypecheckError::TypeMismatch(_)) => {
837 expect_err(
838 "",
839 &Report::new(e),
840 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (optional) long }, but it contains an unexpected attribute `b`: `{"b": 1}`"#).build()
841 );
842 }
843 );
844 }
845
846 #[test]
847 fn extension() {
848 typecheck_restricted_expr_against_schematype(
849 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
850 &SchemaType::Extension {
851 name: "decimal".parse().unwrap(),
852 },
853 Extensions::all_available(),
854 )
855 .unwrap();
856 }
857
858 #[test]
859 fn non_constructor_extension_function() {
860 typecheck_restricted_expr_against_schematype(
861 BorrowedRestrictedExpr::new(&r#"ip("127.0.0.1").isLoopback()"#.parse().unwrap())
862 .unwrap(),
863 &SchemaType::Bool,
864 Extensions::all_available(),
865 )
866 .unwrap();
867 }
868
869 #[test]
870 fn extension_fails() {
871 assert_matches!(
872 typecheck_restricted_expr_against_schematype(
873 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
874 &SchemaType::Extension { name: "ipaddr".parse().unwrap() },
875 Extensions::all_available(),
876 ),
877 Err(e@TypecheckError::TypeMismatch(_)) => {
878 expect_err(
879 "",
880 &Report::new(e),
881 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("1.1")`"#).build()
882 );
883 }
884 )
885 }
886
887 #[test]
888 fn entity() {
889 typecheck_restricted_expr_against_schematype(
890 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
891 &SchemaType::Entity {
892 ty: "User".parse().unwrap(),
893 },
894 Extensions::all_available(),
895 )
896 .unwrap();
897 }
898
899 #[test]
900 fn entity_fails() {
901 assert_matches!(
902 typecheck_restricted_expr_against_schematype(
903 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
904 &SchemaType::Entity { ty: "Photo".parse().unwrap() },
905 Extensions::all_available(),
906 ),
907 Err(e@TypecheckError::TypeMismatch(_)) => {
908 expect_err(
909 "",
910 &Report::new(e),
911 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type `Photo`, but it actually has type (entity of type `User`): `User::"alice"`"#).build()
912 );
913 }
914 )
915 }
916}