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