1use std::collections::BTreeMap;
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_entity(&self, entity: &Entity) -> Result<(), EntitySchemaConformanceError> {
53 let uid = entity.uid();
54 let etype = uid.entity_type();
55 if etype.is_action() {
56 let schema_action = self
57 .schema
58 .action(uid)
59 .ok_or_else(|| EntitySchemaConformanceError::undeclared_action(uid.clone()))?;
60 if !entity.deep_eq(&schema_action) {
62 return Err(EntitySchemaConformanceError::action_declaration_mismatch(
63 uid.clone(),
64 ));
65 }
66 } else {
67 validate_euid(self.schema, uid)
68 .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
69 let schema_etype = self.schema.entity_type(etype).ok_or_else(|| {
70 let suggested_types = self
71 .schema
72 .entity_types_with_basename(&etype.name().basename())
73 .collect();
74 UnexpectedEntityTypeError {
75 uid: uid.clone(),
76 suggested_types,
77 }
78 })?;
79 for required_attr in schema_etype.required_attrs() {
82 if entity.get(&required_attr).is_none() {
83 return Err(EntitySchemaConformanceError::missing_entity_attr(
84 uid.clone(),
85 required_attr,
86 ));
87 }
88 }
89 for (attr, val) in entity.attrs() {
92 match schema_etype.attr_type(attr) {
93 None => {
94 if !schema_etype.open_attributes() {
97 return Err(EntitySchemaConformanceError::unexpected_entity_attr(
98 uid.clone(),
99 attr.clone(),
100 ));
101 }
102 }
103 Some(expected_ty) => {
104 match typecheck_value_against_schematype(val, &expected_ty, self.extensions)
107 {
108 Ok(()) => {} Err(TypecheckError::TypeMismatch(err)) => {
110 return Err(EntitySchemaConformanceError::type_mismatch(
111 uid.clone(),
112 attr.clone(),
113 err,
114 ));
115 }
116 Err(TypecheckError::ExtensionFunctionLookup(err)) => {
117 return Err(
118 EntitySchemaConformanceError::extension_function_lookup(
119 uid.clone(),
120 attr.clone(),
121 err,
122 ),
123 );
124 }
125 }
126 }
127 }
128 validate_euids_in_partial_value(self.schema, val)
129 .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
130 }
131 for ancestor_euid in entity.ancestors() {
134 validate_euid(self.schema, ancestor_euid)
135 .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
136 let ancestor_type = ancestor_euid.entity_type();
137 if schema_etype.allowed_parent_types().contains(ancestor_type) {
138 } else {
143 return Err(EntitySchemaConformanceError::invalid_ancestor_type(
144 uid.clone(),
145 ancestor_type.clone(),
146 ));
147 }
148 }
149
150 for (_, val) in entity.tags() {
151 validate_euids_in_partial_value(self.schema, val)
152 .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
153 }
154 }
155 Ok(())
156 }
157}
158
159pub fn is_valid_enumerated_entity(
161 choices: &[Eid],
162 uid: &EntityUID,
163) -> Result<(), InvalidEnumEntityError> {
164 choices
165 .iter()
166 .find(|id| uid.eid() == *id)
167 .ok_or_else(|| InvalidEnumEntityError {
168 uid: uid.clone(),
169 choices: choices.to_vec(),
170 })
171 .map(|_| ())
172}
173
174pub(crate) fn validate_euid(
176 schema: &impl Schema,
177 euid: &EntityUID,
178) -> Result<(), InvalidEnumEntityError> {
179 if let Some(desc) = schema.entity_type(euid.entity_type()) {
180 if let Some(choices) = desc.enum_entity_eids() {
181 is_valid_enumerated_entity(&Vec::from(choices), euid)?;
182 }
183 }
184 Ok(())
185}
186
187fn validate_euids_in_subexpressions<'a>(
188 exprs: impl IntoIterator<Item = &'a crate::ast::Expr>,
189 schema: &impl Schema,
190) -> std::result::Result<(), InvalidEnumEntityError> {
191 exprs.into_iter().try_for_each(|e| match e.expr_kind() {
192 ExprKind::Lit(Literal::EntityUID(euid)) => validate_euid(schema, euid.as_ref()),
193 _ => Ok(()),
194 })
195}
196
197pub fn validate_euids_in_partial_value(
199 schema: &impl Schema,
200 val: &PartialValue,
201) -> Result<(), InvalidEnumEntityError> {
202 match val {
203 PartialValue::Value(val) => validate_euids_in_subexpressions(
204 RestrictedExpr::from(val.clone()).subexpressions(),
205 schema,
206 ),
207 PartialValue::Residual(e) => validate_euids_in_subexpressions(e.subexpressions(), schema),
208 }
209}
210
211pub fn typecheck_value_against_schematype(
215 value: &PartialValue,
216 expected_ty: &SchemaType,
217 extensions: &Extensions<'_>,
218) -> Result<(), TypecheckError> {
219 match RestrictedExpr::try_from(value.clone()) {
220 Ok(expr) => typecheck_restricted_expr_against_schematype(
221 expr.as_borrowed(),
222 expected_ty,
223 extensions,
224 ),
225 Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
226 Ok(())
234 }
235 }
236}
237
238pub fn typecheck_restricted_expr_against_schematype(
244 expr: BorrowedRestrictedExpr<'_>,
245 expected_ty: &SchemaType,
246 extensions: &Extensions<'_>,
247) -> Result<(), TypecheckError> {
248 use SchemaType::*;
249 let type_mismatch_err = || {
250 Err(TypeMismatchError::type_mismatch(
251 expected_ty.clone(),
252 expr.try_type_of(extensions),
253 expr.to_owned(),
254 )
255 .into())
256 };
257
258 match expr.expr_kind() {
259 ExprKind::Unknown(u) => match u.type_annotation.clone().and_then(SchemaType::from_ty) {
264 Some(ty) => {
265 if &ty == expected_ty {
266 return Ok(());
267 } else {
268 return type_mismatch_err();
269 }
270 }
271 None => return Ok(()),
272 },
273 ExprKind::ExtensionFunctionApp { fn_name, .. } => {
278 return match extensions.func(fn_name)?.return_type() {
279 None => {
280 Ok(())
283 }
284 Some(rty) => {
285 if rty == expected_ty {
286 Ok(())
287 } else {
288 type_mismatch_err()
289 }
290 }
291 };
292 }
293 _ => (),
294 };
295
296 match expected_ty {
304 Bool => {
305 if expr.as_bool().is_some() {
306 Ok(())
307 } else {
308 type_mismatch_err()
309 }
310 }
311 Long => {
312 if expr.as_long().is_some() {
313 Ok(())
314 } else {
315 type_mismatch_err()
316 }
317 }
318 String => {
319 if expr.as_string().is_some() {
320 Ok(())
321 } else {
322 type_mismatch_err()
323 }
324 }
325 EmptySet => {
326 if expr.as_set_elements().is_some_and(|e| e.count() == 0) {
327 Ok(())
328 } else {
329 type_mismatch_err()
330 }
331 }
332 Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => Ok(()),
333 Set { element_ty: elty } => match expr.as_set_elements() {
334 Some(mut els) => els.try_for_each(|e| {
335 typecheck_restricted_expr_against_schematype(e, elty, extensions)
336 }),
337 None => type_mismatch_err(),
338 },
339 Record { attrs, open_attrs } => match expr.as_record_pairs() {
340 Some(pairs) => {
341 let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
342 attrs.iter().try_for_each(|(k, v)| {
345 if !v.required {
346 Ok(())
347 } else {
348 match pairs_map.get(k) {
349 Some(inner_e) => typecheck_restricted_expr_against_schematype(
350 *inner_e,
351 &v.attr_type,
352 extensions,
353 ),
354 None => Err(TypeMismatchError::missing_required_attr(
355 expected_ty.clone(),
356 k.clone(),
357 expr.to_owned(),
358 )
359 .into()),
360 }
361 }
362 })?;
363 pairs_map
366 .iter()
367 .try_for_each(|(k, inner_e)| match attrs.get(*k) {
368 Some(sch_ty) => typecheck_restricted_expr_against_schematype(
369 *inner_e,
370 &sch_ty.attr_type,
371 extensions,
372 ),
373 None => {
374 if *open_attrs {
375 Ok(())
376 } else {
377 Err(TypeMismatchError::unexpected_attr(
378 expected_ty.clone(),
379 (*k).clone(),
380 expr.to_owned(),
381 )
382 .into())
383 }
384 }
385 })?;
386 Ok(())
387 }
388 None => type_mismatch_err(),
389 },
390 Extension { .. } => type_mismatch_err(),
392 Entity { ty } => match expr.as_euid() {
393 Some(actual_euid) if actual_euid.entity_type() == ty => Ok(()),
394 _ => type_mismatch_err(),
395 },
396 }
397}
398
399#[derive(Debug, Diagnostic, Error)]
402pub enum TypecheckError {
403 #[error(transparent)]
405 #[diagnostic(transparent)]
406 TypeMismatch(#[from] TypeMismatchError),
407 #[error(transparent)]
414 #[diagnostic(transparent)]
415 ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
416}
417
418#[cfg(test)]
419mod test_typecheck {
420 use std::collections::BTreeMap;
421
422 use cool_asserts::assert_matches;
423 use miette::Report;
424 use smol_str::ToSmolStr;
425
426 use crate::{
427 entities::{
428 conformance::TypecheckError, AttributeType, BorrowedRestrictedExpr, Expr, SchemaType,
429 Unknown,
430 },
431 extensions::Extensions,
432 test_utils::{expect_err, ExpectedErrorMessageBuilder},
433 };
434
435 use super::typecheck_restricted_expr_against_schematype;
436
437 #[test]
438 fn unknown() {
439 typecheck_restricted_expr_against_schematype(
440 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
441 &SchemaType::Bool,
442 Extensions::all_available(),
443 )
444 .unwrap();
445 typecheck_restricted_expr_against_schematype(
446 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
447 &SchemaType::String,
448 Extensions::all_available(),
449 )
450 .unwrap();
451 typecheck_restricted_expr_against_schematype(
452 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
453 &SchemaType::Set {
454 element_ty: Box::new(SchemaType::Extension {
455 name: "decimal".parse().unwrap(),
456 }),
457 },
458 Extensions::all_available(),
459 )
460 .unwrap();
461 }
462
463 #[test]
464 fn bool() {
465 typecheck_restricted_expr_against_schematype(
466 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
467 &SchemaType::Bool,
468 Extensions::all_available(),
469 )
470 .unwrap();
471 }
472
473 #[test]
474 fn bool_fails() {
475 assert_matches!(
476 typecheck_restricted_expr_against_schematype(
477 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
478 &SchemaType::Bool,
479 Extensions::all_available(),
480 ),
481 Err(e@TypecheckError::TypeMismatch(_)) => {
482 expect_err(
483 "",
484 &Report::new(e),
485 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type bool, but it actually has type long: `1`").build()
486 );
487 }
488 )
489 }
490
491 #[test]
492 fn long() {
493 typecheck_restricted_expr_against_schematype(
494 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
495 &SchemaType::Long,
496 Extensions::all_available(),
497 )
498 .unwrap();
499 }
500
501 #[test]
502 fn long_fails() {
503 assert_matches!(
504 typecheck_restricted_expr_against_schematype(
505 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
506 &SchemaType::Long,
507 Extensions::all_available(),
508 ),
509 Err(e@TypecheckError::TypeMismatch(_)) => {
510 expect_err(
511 "",
512 &Report::new(e),
513 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
514 );
515 }
516 )
517 }
518
519 #[test]
520 fn string() {
521 typecheck_restricted_expr_against_schematype(
522 BorrowedRestrictedExpr::new(&r#""foo""#.parse().unwrap()).unwrap(),
523 &SchemaType::String,
524 Extensions::all_available(),
525 )
526 .unwrap();
527 }
528
529 #[test]
530 fn string_fails() {
531 assert_matches!(
532 typecheck_restricted_expr_against_schematype(
533 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
534 &SchemaType::String,
535 Extensions::all_available(),
536 ),
537 Err(e@TypecheckError::TypeMismatch(_)) => {
538 expect_err(
539 "",
540 &Report::new(e),
541 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type bool: `false`").build()
542 );
543 }
544 )
545 }
546
547 #[test]
548 fn test_typecheck_set() {
549 typecheck_restricted_expr_against_schematype(
550 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
551 &SchemaType::Set {
552 element_ty: Box::new(SchemaType::Long),
553 },
554 Extensions::all_available(),
555 )
556 .unwrap();
557 typecheck_restricted_expr_against_schematype(
558 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
559 &SchemaType::Set {
560 element_ty: Box::new(SchemaType::Bool),
561 },
562 Extensions::all_available(),
563 )
564 .unwrap();
565 }
566
567 #[test]
568 fn test_typecheck_set_fails() {
569 assert_matches!(
570 typecheck_restricted_expr_against_schematype(
571 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
572 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
573 Extensions::all_available(),
574 ),
575 Err(e@TypecheckError::TypeMismatch(_)) => {
576 expect_err(
577 "",
578 &Report::new(e),
579 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type [string], but it actually has type record: `{}`").build()
580 );
581 }
582 );
583 assert_matches!(
584 typecheck_restricted_expr_against_schematype(
585 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
586 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
587 Extensions::all_available(),
588 ),
589 Err(e@TypecheckError::TypeMismatch(_)) => {
590 expect_err(
591 "",
592 &Report::new(e),
593 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type long: `1`").build()
594 );
595 }
596 );
597 assert_matches!(
598 typecheck_restricted_expr_against_schematype(
599 BorrowedRestrictedExpr::new(&"[1, true]".parse().unwrap()).unwrap(),
600 &SchemaType::Set { element_ty: Box::new(SchemaType::Long) },
601 Extensions::all_available(),
602 ),
603 Err(e@TypecheckError::TypeMismatch(_)) => {
604 expect_err(
605 "",
606 &Report::new(e),
607 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `true`").build()
608 );
609 }
610 )
611 }
612
613 #[test]
614 fn test_typecheck_record() {
615 typecheck_restricted_expr_against_schematype(
616 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
617 &SchemaType::Record {
618 attrs: BTreeMap::new(),
619 open_attrs: false,
620 },
621 Extensions::all_available(),
622 )
623 .unwrap();
624 typecheck_restricted_expr_against_schematype(
625 BorrowedRestrictedExpr::new(&"{a: 1}".parse().unwrap()).unwrap(),
626 &SchemaType::Record {
627 attrs: BTreeMap::from([(
628 "a".to_smolstr(),
629 AttributeType {
630 attr_type: SchemaType::Long,
631 required: true,
632 },
633 )]),
634 open_attrs: false,
635 },
636 Extensions::all_available(),
637 )
638 .unwrap();
639 typecheck_restricted_expr_against_schematype(
640 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
641 &SchemaType::Record {
642 attrs: BTreeMap::from([(
643 "a".to_smolstr(),
644 AttributeType {
645 attr_type: SchemaType::Long,
646 required: false,
647 },
648 )]),
649 open_attrs: false,
650 },
651 Extensions::all_available(),
652 )
653 .unwrap();
654 }
655
656 #[test]
657 fn test_typecheck_record_fails() {
658 assert_matches!(
659 typecheck_restricted_expr_against_schematype(
660 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
661 &SchemaType::Record { attrs: BTreeMap::from([]), open_attrs: false },
662 Extensions::all_available(),
663 ),
664 Err(e@TypecheckError::TypeMismatch(_)) => {
665 expect_err(
666 "",
667 &Report::new(e),
668 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type { }, but it actually has type set: `[]`").build()
669 );
670 }
671 );
672 assert_matches!(
673 typecheck_restricted_expr_against_schematype(
674 BorrowedRestrictedExpr::new(&"{a: false}".parse().unwrap()).unwrap(),
675 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
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 long, but it actually has type bool: `false`").build()
683 );
684 }
685 );
686 assert_matches!(
687 typecheck_restricted_expr_against_schematype(
688 BorrowedRestrictedExpr::new(&"{a: {}}".parse().unwrap()).unwrap(),
689 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
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 long, but it actually has type record: `{}`").build()
697 );
698 }
699 );
700 assert_matches!(
701 typecheck_restricted_expr_against_schematype(
702 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
703 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
704 Extensions::all_available(),
705 ),
706 Err(e@TypecheckError::TypeMismatch(_)) => {
707 expect_err(
708 "",
709 &Report::new(e),
710 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it is missing the required attribute `a`: `{}`"#).build()
711 );
712 }
713 );
714 assert_matches!(
715 typecheck_restricted_expr_against_schematype(
716 BorrowedRestrictedExpr::new(&"{a: 1, b: 1}".parse().unwrap()).unwrap(),
717 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
718 Extensions::all_available(),
719 ),
720 Err(e@TypecheckError::TypeMismatch(_)) => {
721 expect_err(
722 "",
723 &Report::new(e),
724 &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()
725 );
726 }
727 );
728 assert_matches!(
729 typecheck_restricted_expr_against_schematype(
730 BorrowedRestrictedExpr::new(&"{b: 1}".parse().unwrap()).unwrap(),
731 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
732 Extensions::all_available(),
733 ),
734 Err(e@TypecheckError::TypeMismatch(_)) => {
735 expect_err(
736 "",
737 &Report::new(e),
738 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (optional) long }, but it contains an unexpected attribute `b`: `{"b": 1}`"#).build()
739 );
740 }
741 );
742 }
743
744 #[test]
745 fn extension() {
746 typecheck_restricted_expr_against_schematype(
747 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
748 &SchemaType::Extension {
749 name: "decimal".parse().unwrap(),
750 },
751 Extensions::all_available(),
752 )
753 .unwrap();
754 }
755
756 #[test]
757 fn non_constructor_extension_function() {
758 typecheck_restricted_expr_against_schematype(
759 BorrowedRestrictedExpr::new(&r#"ip("127.0.0.1").isLoopback()"#.parse().unwrap())
760 .unwrap(),
761 &SchemaType::Bool,
762 Extensions::all_available(),
763 )
764 .unwrap();
765 }
766
767 #[test]
768 fn extension_fails() {
769 assert_matches!(
770 typecheck_restricted_expr_against_schematype(
771 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
772 &SchemaType::Extension { name: "ipaddr".parse().unwrap() },
773 Extensions::all_available(),
774 ),
775 Err(e@TypecheckError::TypeMismatch(_)) => {
776 expect_err(
777 "",
778 &Report::new(e),
779 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("1.1")`"#).build()
780 );
781 }
782 )
783 }
784
785 #[test]
786 fn entity() {
787 typecheck_restricted_expr_against_schematype(
788 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
789 &SchemaType::Entity {
790 ty: "User".parse().unwrap(),
791 },
792 Extensions::all_available(),
793 )
794 .unwrap();
795 }
796
797 #[test]
798 fn entity_fails() {
799 assert_matches!(
800 typecheck_restricted_expr_against_schematype(
801 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
802 &SchemaType::Entity { ty: "Photo".parse().unwrap() },
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 `Photo`, but it actually has type (entity of type `User`): `User::"alice"`"#).build()
810 );
811 }
812 )
813 }
814}