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