1use std::collections::BTreeMap;
18
19use super::{json::err::TypeMismatchError, EntityTypeDescription, Schema, SchemaType};
20use crate::ast::{
21 BorrowedRestrictedExpr, Entity, PartialValue, PartialValueToRestrictedExprError, RestrictedExpr,
22};
23use crate::entities::ExprKind;
24use crate::extensions::{ExtensionFunctionLookupError, Extensions};
25use miette::Diagnostic;
26use smol_str::SmolStr;
27use thiserror::Error;
28pub mod err;
29
30use err::{EntitySchemaConformanceError, UnexpectedEntityTypeError};
31
32#[derive(Debug, Clone)]
34pub struct EntitySchemaConformanceChecker<'a, S: Schema> {
35 schema: &'a S,
37 extensions: &'a Extensions<'a>,
39}
40
41impl<'a, S: Schema> EntitySchemaConformanceChecker<'a, S> {
42 pub fn new(schema: &'a S, extensions: &'a Extensions<'a>) -> Self {
44 Self { schema, extensions }
45 }
46
47 pub fn validate_entity(&self, entity: &Entity) -> Result<(), EntitySchemaConformanceError> {
50 let uid = entity.uid();
51 let etype = uid.entity_type();
52 if etype.is_action() {
53 let schema_action = self
54 .schema
55 .action(uid)
56 .ok_or_else(|| EntitySchemaConformanceError::undeclared_action(uid.clone()))?;
57 if !entity.deep_eq(&schema_action) {
59 return Err(EntitySchemaConformanceError::action_declaration_mismatch(
60 uid.clone(),
61 ));
62 }
63 } else {
64 let schema_etype = self.schema.entity_type(etype).ok_or_else(|| {
65 let suggested_types = self
66 .schema
67 .entity_types_with_basename(&etype.name().basename())
68 .collect();
69 UnexpectedEntityTypeError {
70 uid: uid.clone(),
71 suggested_types,
72 }
73 })?;
74 for required_attr in schema_etype.required_attrs() {
77 if entity.get(&required_attr).is_none() {
78 return Err(EntitySchemaConformanceError::missing_entity_attr(
79 uid.clone(),
80 required_attr,
81 ));
82 }
83 }
84 for (attr, val) in entity.attrs() {
87 match schema_etype.attr_type(attr) {
88 None => {
89 if !schema_etype.open_attributes() {
92 return Err(EntitySchemaConformanceError::unexpected_entity_attr(
93 uid.clone(),
94 attr.clone(),
95 ));
96 }
97 }
98 Some(expected_ty) => {
99 match typecheck_value_against_schematype(val, &expected_ty, self.extensions)
102 {
103 Ok(()) => {} Err(TypecheckError::TypeMismatch(err)) => {
105 return Err(EntitySchemaConformanceError::type_mismatch(
106 uid.clone(),
107 attr.clone(),
108 err,
109 ));
110 }
111 Err(TypecheckError::ExtensionFunctionLookup(err)) => {
112 return Err(
113 EntitySchemaConformanceError::extension_function_lookup(
114 uid.clone(),
115 attr.clone(),
116 err,
117 ),
118 );
119 }
120 }
121 }
122 }
123 }
124 for ancestor_euid in entity.ancestors() {
127 let ancestor_type = ancestor_euid.entity_type();
128 if schema_etype.allowed_parent_types().contains(ancestor_type) {
129 } else {
134 return Err(EntitySchemaConformanceError::invalid_ancestor_type(
135 uid.clone(),
136 ancestor_type.clone(),
137 ));
138 }
139 }
140 }
141 Ok(())
142 }
143}
144
145pub fn typecheck_value_against_schematype(
149 value: &PartialValue,
150 expected_ty: &SchemaType,
151 extensions: &Extensions<'_>,
152) -> Result<(), TypecheckError> {
153 match RestrictedExpr::try_from(value.clone()) {
154 Ok(expr) => typecheck_restricted_expr_against_schematype(
155 expr.as_borrowed(),
156 expected_ty,
157 extensions,
158 ),
159 Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
160 Ok(())
168 }
169 }
170}
171
172pub fn typecheck_restricted_expr_against_schematype(
178 expr: BorrowedRestrictedExpr<'_>,
179 expected_ty: &SchemaType,
180 extensions: &Extensions<'_>,
181) -> Result<(), TypecheckError> {
182 use SchemaType::*;
183 let type_mismatch_err = || {
184 Err(TypeMismatchError::type_mismatch(
185 expected_ty.clone(),
186 expr.try_type_of(extensions),
187 expr.to_owned(),
188 )
189 .into())
190 };
191
192 match expr.expr_kind() {
193 ExprKind::Unknown(u) => match u.type_annotation.clone().and_then(SchemaType::from_ty) {
198 Some(ty) => {
199 if &ty == expected_ty {
200 return Ok(());
201 } else {
202 return type_mismatch_err();
203 }
204 }
205 None => return Ok(()),
206 },
207 ExprKind::ExtensionFunctionApp { fn_name, .. } => {
212 return match extensions.func(fn_name)?.return_type() {
213 None => {
214 Ok(())
217 }
218 Some(rty) => {
219 if rty == expected_ty {
220 Ok(())
221 } else {
222 type_mismatch_err()
223 }
224 }
225 };
226 }
227 _ => (),
228 };
229
230 match expected_ty {
238 Bool => {
239 if expr.as_bool().is_some() {
240 Ok(())
241 } else {
242 type_mismatch_err()
243 }
244 }
245 Long => {
246 if expr.as_long().is_some() {
247 Ok(())
248 } else {
249 type_mismatch_err()
250 }
251 }
252 String => {
253 if expr.as_string().is_some() {
254 Ok(())
255 } else {
256 type_mismatch_err()
257 }
258 }
259 EmptySet => {
260 if expr.as_set_elements().is_some_and(|e| e.count() == 0) {
261 Ok(())
262 } else {
263 type_mismatch_err()
264 }
265 }
266 Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => Ok(()),
267 Set { element_ty: elty } => match expr.as_set_elements() {
268 Some(mut els) => els.try_for_each(|e| {
269 typecheck_restricted_expr_against_schematype(e, elty, extensions)
270 }),
271 None => type_mismatch_err(),
272 },
273 Record { attrs, open_attrs } => match expr.as_record_pairs() {
274 Some(pairs) => {
275 let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
276 attrs.iter().try_for_each(|(k, v)| {
279 if !v.required {
280 Ok(())
281 } else {
282 match pairs_map.get(k) {
283 Some(inner_e) => typecheck_restricted_expr_against_schematype(
284 *inner_e,
285 &v.attr_type,
286 extensions,
287 ),
288 None => Err(TypeMismatchError::missing_required_attr(
289 expected_ty.clone(),
290 k.clone(),
291 expr.to_owned(),
292 )
293 .into()),
294 }
295 }
296 })?;
297 pairs_map
300 .iter()
301 .try_for_each(|(k, inner_e)| match attrs.get(*k) {
302 Some(sch_ty) => typecheck_restricted_expr_against_schematype(
303 *inner_e,
304 &sch_ty.attr_type,
305 extensions,
306 ),
307 None => {
308 if *open_attrs {
309 Ok(())
310 } else {
311 Err(TypeMismatchError::unexpected_attr(
312 expected_ty.clone(),
313 (*k).clone(),
314 expr.to_owned(),
315 )
316 .into())
317 }
318 }
319 })?;
320 Ok(())
321 }
322 None => type_mismatch_err(),
323 },
324 Extension { .. } => type_mismatch_err(),
326 Entity { ty } => match expr.as_euid() {
327 Some(actual_euid) if actual_euid.entity_type() == ty => Ok(()),
328 _ => type_mismatch_err(),
329 },
330 }
331}
332
333#[derive(Debug, Diagnostic, Error)]
336pub enum TypecheckError {
337 #[error(transparent)]
339 #[diagnostic(transparent)]
340 TypeMismatch(#[from] TypeMismatchError),
341 #[error(transparent)]
348 #[diagnostic(transparent)]
349 ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
350}
351
352#[cfg(test)]
353mod test_typecheck {
354 use std::collections::BTreeMap;
355
356 use cool_asserts::assert_matches;
357 use miette::Report;
358 use smol_str::ToSmolStr;
359
360 use crate::{
361 entities::{
362 conformance::TypecheckError, AttributeType, BorrowedRestrictedExpr, Expr, SchemaType,
363 Unknown,
364 },
365 extensions::Extensions,
366 test_utils::{expect_err, ExpectedErrorMessageBuilder},
367 };
368
369 use super::typecheck_restricted_expr_against_schematype;
370
371 #[test]
372 fn unknown() {
373 typecheck_restricted_expr_against_schematype(
374 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
375 &SchemaType::Bool,
376 Extensions::all_available(),
377 )
378 .unwrap();
379 typecheck_restricted_expr_against_schematype(
380 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
381 &SchemaType::String,
382 Extensions::all_available(),
383 )
384 .unwrap();
385 typecheck_restricted_expr_against_schematype(
386 BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
387 &SchemaType::Set {
388 element_ty: Box::new(SchemaType::Extension {
389 name: "decimal".parse().unwrap(),
390 }),
391 },
392 Extensions::all_available(),
393 )
394 .unwrap();
395 }
396
397 #[test]
398 fn bool() {
399 typecheck_restricted_expr_against_schematype(
400 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
401 &SchemaType::Bool,
402 Extensions::all_available(),
403 )
404 .unwrap();
405 }
406
407 #[test]
408 fn bool_fails() {
409 assert_matches!(
410 typecheck_restricted_expr_against_schematype(
411 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
412 &SchemaType::Bool,
413 Extensions::all_available(),
414 ),
415 Err(e@TypecheckError::TypeMismatch(_)) => {
416 expect_err(
417 "",
418 &Report::new(e),
419 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type bool, but it actually has type long: `1`").build()
420 );
421 }
422 )
423 }
424
425 #[test]
426 fn long() {
427 typecheck_restricted_expr_against_schematype(
428 BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
429 &SchemaType::Long,
430 Extensions::all_available(),
431 )
432 .unwrap();
433 }
434
435 #[test]
436 fn long_fails() {
437 assert_matches!(
438 typecheck_restricted_expr_against_schematype(
439 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
440 &SchemaType::Long,
441 Extensions::all_available(),
442 ),
443 Err(e@TypecheckError::TypeMismatch(_)) => {
444 expect_err(
445 "",
446 &Report::new(e),
447 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
448 );
449 }
450 )
451 }
452
453 #[test]
454 fn string() {
455 typecheck_restricted_expr_against_schematype(
456 BorrowedRestrictedExpr::new(&r#""foo""#.parse().unwrap()).unwrap(),
457 &SchemaType::String,
458 Extensions::all_available(),
459 )
460 .unwrap();
461 }
462
463 #[test]
464 fn string_fails() {
465 assert_matches!(
466 typecheck_restricted_expr_against_schematype(
467 BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
468 &SchemaType::String,
469 Extensions::all_available(),
470 ),
471 Err(e@TypecheckError::TypeMismatch(_)) => {
472 expect_err(
473 "",
474 &Report::new(e),
475 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type bool: `false`").build()
476 );
477 }
478 )
479 }
480
481 #[test]
482 fn test_typecheck_set() {
483 typecheck_restricted_expr_against_schematype(
484 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
485 &SchemaType::Set {
486 element_ty: Box::new(SchemaType::Long),
487 },
488 Extensions::all_available(),
489 )
490 .unwrap();
491 typecheck_restricted_expr_against_schematype(
492 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
493 &SchemaType::Set {
494 element_ty: Box::new(SchemaType::Bool),
495 },
496 Extensions::all_available(),
497 )
498 .unwrap();
499 }
500
501 #[test]
502 fn test_typecheck_set_fails() {
503 assert_matches!(
504 typecheck_restricted_expr_against_schematype(
505 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
506 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
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 [string], but it actually has type record: `{}`").build()
514 );
515 }
516 );
517 assert_matches!(
518 typecheck_restricted_expr_against_schematype(
519 BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
520 &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
521 Extensions::all_available(),
522 ),
523 Err(e@TypecheckError::TypeMismatch(_)) => {
524 expect_err(
525 "",
526 &Report::new(e),
527 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type long: `1`").build()
528 );
529 }
530 );
531 assert_matches!(
532 typecheck_restricted_expr_against_schematype(
533 BorrowedRestrictedExpr::new(&"[1, true]".parse().unwrap()).unwrap(),
534 &SchemaType::Set { element_ty: Box::new(SchemaType::Long) },
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 long, but it actually has type bool: `true`").build()
542 );
543 }
544 )
545 }
546
547 #[test]
548 fn test_typecheck_record() {
549 typecheck_restricted_expr_against_schematype(
550 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
551 &SchemaType::Record {
552 attrs: BTreeMap::new(),
553 open_attrs: false,
554 },
555 Extensions::all_available(),
556 )
557 .unwrap();
558 typecheck_restricted_expr_against_schematype(
559 BorrowedRestrictedExpr::new(&"{a: 1}".parse().unwrap()).unwrap(),
560 &SchemaType::Record {
561 attrs: BTreeMap::from([(
562 "a".to_smolstr(),
563 AttributeType {
564 attr_type: SchemaType::Long,
565 required: true,
566 },
567 )]),
568 open_attrs: false,
569 },
570 Extensions::all_available(),
571 )
572 .unwrap();
573 typecheck_restricted_expr_against_schematype(
574 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
575 &SchemaType::Record {
576 attrs: BTreeMap::from([(
577 "a".to_smolstr(),
578 AttributeType {
579 attr_type: SchemaType::Long,
580 required: false,
581 },
582 )]),
583 open_attrs: false,
584 },
585 Extensions::all_available(),
586 )
587 .unwrap();
588 }
589
590 #[test]
591 fn test_typecheck_record_fails() {
592 assert_matches!(
593 typecheck_restricted_expr_against_schematype(
594 BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
595 &SchemaType::Record { attrs: BTreeMap::from([]), open_attrs: false },
596 Extensions::all_available(),
597 ),
598 Err(e@TypecheckError::TypeMismatch(_)) => {
599 expect_err(
600 "",
601 &Report::new(e),
602 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type { }, but it actually has type set: `[]`").build()
603 );
604 }
605 );
606 assert_matches!(
607 typecheck_restricted_expr_against_schematype(
608 BorrowedRestrictedExpr::new(&"{a: false}".parse().unwrap()).unwrap(),
609 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
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 assert_matches!(
621 typecheck_restricted_expr_against_schematype(
622 BorrowedRestrictedExpr::new(&"{a: {}}".parse().unwrap()).unwrap(),
623 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
624 Extensions::all_available(),
625 ),
626 Err(e@TypecheckError::TypeMismatch(_)) => {
627 expect_err(
628 "",
629 &Report::new(e),
630 &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type record: `{}`").build()
631 );
632 }
633 );
634 assert_matches!(
635 typecheck_restricted_expr_against_schematype(
636 BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
637 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
638 Extensions::all_available(),
639 ),
640 Err(e@TypecheckError::TypeMismatch(_)) => {
641 expect_err(
642 "",
643 &Report::new(e),
644 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it is missing the required attribute `a`: `{}`"#).build()
645 );
646 }
647 );
648 assert_matches!(
649 typecheck_restricted_expr_against_schematype(
650 BorrowedRestrictedExpr::new(&"{a: 1, b: 1}".parse().unwrap()).unwrap(),
651 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
652 Extensions::all_available(),
653 ),
654 Err(e@TypecheckError::TypeMismatch(_)) => {
655 expect_err(
656 "",
657 &Report::new(e),
658 &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()
659 );
660 }
661 );
662 assert_matches!(
663 typecheck_restricted_expr_against_schematype(
664 BorrowedRestrictedExpr::new(&"{b: 1}".parse().unwrap()).unwrap(),
665 &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
666 Extensions::all_available(),
667 ),
668 Err(e@TypecheckError::TypeMismatch(_)) => {
669 expect_err(
670 "",
671 &Report::new(e),
672 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (optional) long }, but it contains an unexpected attribute `b`: `{"b": 1}`"#).build()
673 );
674 }
675 );
676 }
677
678 #[test]
679 fn extension() {
680 typecheck_restricted_expr_against_schematype(
681 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
682 &SchemaType::Extension {
683 name: "decimal".parse().unwrap(),
684 },
685 Extensions::all_available(),
686 )
687 .unwrap();
688 }
689
690 #[test]
691 fn non_constructor_extension_function() {
692 typecheck_restricted_expr_against_schematype(
693 BorrowedRestrictedExpr::new(&r#"ip("127.0.0.1").isLoopback()"#.parse().unwrap())
694 .unwrap(),
695 &SchemaType::Bool,
696 Extensions::all_available(),
697 )
698 .unwrap();
699 }
700
701 #[test]
702 fn extension_fails() {
703 assert_matches!(
704 typecheck_restricted_expr_against_schematype(
705 BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
706 &SchemaType::Extension { name: "ipaddr".parse().unwrap() },
707 Extensions::all_available(),
708 ),
709 Err(e@TypecheckError::TypeMismatch(_)) => {
710 expect_err(
711 "",
712 &Report::new(e),
713 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("1.1")`"#).build()
714 );
715 }
716 )
717 }
718
719 #[test]
720 fn entity() {
721 typecheck_restricted_expr_against_schematype(
722 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
723 &SchemaType::Entity {
724 ty: "User".parse().unwrap(),
725 },
726 Extensions::all_available(),
727 )
728 .unwrap();
729 }
730
731 #[test]
732 fn entity_fails() {
733 assert_matches!(
734 typecheck_restricted_expr_against_schematype(
735 BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
736 &SchemaType::Entity { ty: "Photo".parse().unwrap() },
737 Extensions::all_available(),
738 ),
739 Err(e@TypecheckError::TypeMismatch(_)) => {
740 expect_err(
741 "",
742 &Report::new(e),
743 &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type `Photo`, but it actually has type (entity of type `User`): `User::"alice"`"#).build()
744 );
745 }
746 )
747 }
748}