Skip to main content

cedar_policy_core/entities/
conformance.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17pub 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};
28use miette::Diagnostic;
29use nonempty::NonEmpty;
30use smol_str::SmolStr;
31use std::collections::{BTreeMap, HashMap};
32use thiserror::Error;
33
34/// Struct used to check whether entities conform to a schema
35#[derive(Debug, Clone)]
36pub struct EntitySchemaConformanceChecker<'a, S> {
37    /// Schema to check conformance with
38    schema: &'a S,
39    /// Extensions which are active for the conformance checks
40    extensions: &'a Extensions<'a>,
41}
42
43impl<'a, S> EntitySchemaConformanceChecker<'a, S> {
44    /// Create a new checker
45    pub fn new(schema: &'a S, extensions: &'a Extensions<'a>) -> Self {
46        Self { schema, extensions }
47    }
48}
49
50impl<S: Schema> EntitySchemaConformanceChecker<'_, S> {
51    /// Validate an action
52    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        // check that the action exactly matches the schema's definition
59        if !action.deep_eq(&schema_action) {
60            return Err(EntitySchemaConformanceError::action_declaration_mismatch(
61                uid.clone(),
62            ));
63        }
64        Ok(())
65    }
66
67    /// Validate ancestors of an entity
68    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 each ancestor that actually appears in `entity`, ensure the
75        // ancestor type is allowed by the schema
76        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                // note that `allowed_parent_types()` was transitively
81                // closed, so it's actually `allowed_ancestor_types()`
82                //
83                // thus, the check passes in this case
84            } else {
85                return Err(EntitySchemaConformanceError::invalid_ancestor_type(
86                    uid.clone(),
87                    ancestor_type.clone(),
88                ));
89            }
90        }
91        Ok(())
92    }
93
94    /// Validate attributes of an entity
95    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        // Ensure that all required attributes for `etype` are actually
103        // included in `entity`
104        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 each attribute that actually appears in `entity`, ensure it
113        // complies with the schema
114        for (attr, val) in attrs {
115            match schema_etype.attr_type(attr) {
116                None => {
117                    // `None` indicates the attribute shouldn't exist -- see
118                    // docs on the `attr_type()` trait method
119                    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                    // typecheck: ensure that the entity attribute value matches
128                    // the expected type
129                    match typecheck_value_against_schematype(val, &expected_ty, self.extensions) {
130                        Ok(()) => {} // typecheck passes
131                        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    /// Validate tags of an entity
156    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(()) => {} // typecheck passes
176                        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    /// Validate an entity against the schema, returning an
203    /// [`EntitySchemaConformanceError`] if it does not comply.
204    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                EntitySchemaConformanceError::unexpected_entity_type(self.schema, uid.clone())
212            })?;
213
214            validate_euid(self.schema, uid)?;
215            self.validate_entity_attributes(uid, entity.attrs(), &schema_etype)?;
216            self.validate_entity_ancestors(uid, entity.ancestors(), &schema_etype)?;
217            self.validate_tags(uid, entity.tags(), &schema_etype)?;
218        }
219        Ok(())
220    }
221}
222
223/// Return an [`InvalidEnumEntityError`] if `uid`'s eid is not among valid `choices`
224pub fn is_valid_enumerated_entity(
225    choices: &NonEmpty<Eid>,
226    uid: &EntityUID,
227) -> Result<(), InvalidEnumEntityError> {
228    choices
229        .iter()
230        .any(|id| uid.eid() == id)
231        .then_some(())
232        .ok_or_else(|| InvalidEnumEntityError {
233            uid: uid.clone(),
234            choices: choices.clone(),
235        })
236}
237
238/// Errors returned from `validate_euid()` and friends
239///
240/// This is NOT a publicly exported error type.
241#[derive(Debug, Error, Diagnostic)]
242pub enum ValidateEuidError {
243    /// EUID's type is an enum type, but its value is not one of the declared enum values
244    #[error(transparent)]
245    #[diagnostic(transparent)]
246    InvalidEnumEntity(#[from] InvalidEnumEntityError),
247    /// EUID's type is an action type, but it is not one of the declared actions
248    #[error(transparent)]
249    #[diagnostic(transparent)]
250    UndeclaredAction(#[from] UndeclaredAction),
251}
252
253impl From<ValidateEuidError> for EntitySchemaConformanceError {
254    fn from(e: ValidateEuidError) -> Self {
255        match e {
256            ValidateEuidError::InvalidEnumEntity(e) => InvalidEnumEntity::from(e).into(),
257            ValidateEuidError::UndeclaredAction(e) => e.into(),
258        }
259    }
260}
261
262/// Validate if `euid` is valid
263///
264/// As of this writing, the only ways for an `euid` to be invalid are if it is
265/// of enumerated entity type or action type, in which case it needs to have one
266/// of the specific entity IDs declared in the schema.
267pub fn validate_euid(schema: &impl Schema, euid: &EntityUID) -> Result<(), ValidateEuidError> {
268    let entity_type = euid.entity_type();
269    if let Some(desc) = schema.entity_type(entity_type) {
270        if let Some(choices) = desc.enum_entity_eids() {
271            is_valid_enumerated_entity(choices, euid)?;
272        }
273    }
274    if entity_type.is_action() && schema.action(euid).is_none() {
275        return Err(ValidateEuidError::UndeclaredAction(UndeclaredAction {
276            uid: euid.clone(),
277        }));
278    }
279    Ok(())
280}
281
282fn validate_euids_in_subexpressions<'a>(
283    exprs: impl IntoIterator<Item = &'a crate::ast::Expr>,
284    schema: &impl Schema,
285) -> std::result::Result<(), ValidateEuidError> {
286    exprs.into_iter().try_for_each(|e| match e.expr_kind() {
287        ExprKind::Lit(Literal::EntityUID(euid)) => validate_euid(schema, euid.as_ref()),
288        _ => Ok(()),
289    })
290}
291
292/// Validate if enumerated entities and action UIDs in `val` are valid
293pub fn validate_euids_in_partial_value(
294    schema: &impl Schema,
295    val: &PartialValue,
296) -> Result<(), ValidateEuidError> {
297    match val {
298        PartialValue::Value(val) => validate_euids_in_subexpressions(
299            RestrictedExpr::from(val.clone()).subexpressions(),
300            schema,
301        ),
302        PartialValue::Residual(e) => validate_euids_in_subexpressions(e.subexpressions(), schema),
303    }
304}
305
306/// Check whether the given `PartialValue` typechecks with the given `SchemaType`.
307/// If the typecheck passes, return `Ok(())`.
308/// If the typecheck fails, return an appropriate `Err`.
309pub fn typecheck_value_against_schematype(
310    value: &PartialValue,
311    expected_ty: &SchemaType,
312    extensions: &Extensions<'_>,
313) -> Result<(), TypecheckError> {
314    match RestrictedExpr::try_from(value.clone()) {
315        Ok(expr) => typecheck_restricted_expr_against_schematype(
316            expr.as_borrowed(),
317            expected_ty,
318            extensions,
319        ),
320        Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
321            // this case should be unreachable for the case of `PartialValue`s
322            // which are entity attributes, because a `PartialValue` computed
323            // from a `RestrictedExpr` should only have trivial residuals.
324            // And as of this writing, there are no callers of this function that
325            // pass anything other than entity attributes.
326            // Nonetheless, rather than relying on these delicate invariants,
327            // it's safe to consider this as passing.
328            Ok(())
329        }
330    }
331}
332
333/// Check whether the given `RestrictedExpr` is a valid instance of
334/// `SchemaType`.  We do not have type information for unknowns, so this
335/// function liberally treats unknowns as implementing any schema type.  If the
336/// typecheck passes, return `Ok(())`.  If the typecheck fails, return an
337/// appropriate `Err`.
338pub fn typecheck_restricted_expr_against_schematype(
339    expr: BorrowedRestrictedExpr<'_>,
340    expected_ty: &SchemaType,
341    extensions: &Extensions<'_>,
342) -> Result<(), TypecheckError> {
343    use SchemaType::*;
344    let type_mismatch_err = || {
345        Err(TypeMismatchError::type_mismatch(
346            expected_ty.clone(),
347            expr.try_type_of(extensions),
348            expr.to_owned(),
349        )
350        .into())
351    };
352
353    match expr.expr_kind() {
354        // Check for `unknowns`.  Unless explicitly annotated, we don't have the
355        // information to know whether the unknown value matches the expected type.
356        // For now we consider this as passing -- we can't really report a type
357        // error <https://github.com/cedar-policy/cedar/issues/418>.
358        ExprKind::Unknown(u) => match u.type_annotation.clone().and_then(SchemaType::from_ty) {
359            Some(ty) => {
360                if &ty == expected_ty {
361                    return Ok(());
362                } else {
363                    return type_mismatch_err();
364                }
365            }
366            None => return Ok(()),
367        },
368        // Check for extension function calls. Restricted expressions permit all
369        // extension function calls, including those that aren't constructors.
370        // Checking the return type here before matching on the expected type lets
371        // us handle extension functions that return, e.g., bool and not an extension type.
372        ExprKind::ExtensionFunctionApp { fn_name, .. } => {
373            return match extensions.func(fn_name)?.return_type() {
374                None => {
375                    // This is actually another `unknown` case. The return type
376                    // is `None` only when the function is an "unknown"
377                    Ok(())
378                }
379                Some(rty) => {
380                    if rty == expected_ty {
381                        Ok(())
382                    } else {
383                        type_mismatch_err()
384                    }
385                }
386            };
387        }
388        _ => (),
389    };
390
391    // We know `expr` is a restricted expression, so it must either be an
392    // extension function call or a literal bool, long string, set or record.
393    // This means we don't need to check if it's a `has` or `==` expression to
394    // decide if it typechecks against `Bool`. Anything other an than a boolean
395    // literal is an error. To handle extension function calls, which could
396    // return `Bool`, we have already checked if the expression is an extension
397    // function in the prior `match` expression.
398    match expected_ty {
399        Bool => {
400            if expr.as_bool().is_some() {
401                Ok(())
402            } else {
403                type_mismatch_err()
404            }
405        }
406        Long => {
407            if expr.as_long().is_some() {
408                Ok(())
409            } else {
410                type_mismatch_err()
411            }
412        }
413        String => {
414            if expr.as_string().is_some() {
415                Ok(())
416            } else {
417                type_mismatch_err()
418            }
419        }
420        EmptySet => {
421            if expr.as_set_elements().is_some_and(|e| e.count() == 0) {
422                Ok(())
423            } else {
424                type_mismatch_err()
425            }
426        }
427        Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => Ok(()),
428        Set { element_ty: elty } => match expr.as_set_elements() {
429            Some(mut els) => els.try_for_each(|e| {
430                typecheck_restricted_expr_against_schematype(e, elty, extensions)
431            }),
432            None => type_mismatch_err(),
433        },
434        Record { attrs, open_attrs } => match expr.as_record_pairs() {
435            Some(pairs) => {
436                let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
437                // Check that all attributes required by the schema are present
438                // in the record.
439                attrs.iter().try_for_each(|(k, v)| {
440                    if !v.required {
441                        Ok(())
442                    } else {
443                        match pairs_map.get(k) {
444                            Some(inner_e) => typecheck_restricted_expr_against_schematype(
445                                *inner_e,
446                                &v.attr_type,
447                                extensions,
448                            ),
449                            None => Err(TypeMismatchError::missing_required_attr(
450                                expected_ty.clone(),
451                                k.clone(),
452                                expr.to_owned(),
453                            )
454                            .into()),
455                        }
456                    }
457                })?;
458                // Check that all attributes in the record are present (as
459                // required or optional) in the schema.
460                pairs_map
461                    .iter()
462                    .try_for_each(|(k, inner_e)| match attrs.get(*k) {
463                        Some(sch_ty) => typecheck_restricted_expr_against_schematype(
464                            *inner_e,
465                            &sch_ty.attr_type,
466                            extensions,
467                        ),
468                        None => {
469                            if *open_attrs {
470                                Ok(())
471                            } else {
472                                Err(TypeMismatchError::unexpected_attr(
473                                    expected_ty.clone(),
474                                    (*k).clone(),
475                                    expr.to_owned(),
476                                )
477                                .into())
478                            }
479                        }
480                    })?;
481                Ok(())
482            }
483            None => type_mismatch_err(),
484        },
485        // Extension functions are handled by the first `match` in this function.
486        Extension { .. } => type_mismatch_err(),
487        Entity { ty } => match expr.as_euid() {
488            Some(actual_euid) if actual_euid.entity_type() == ty => Ok(()),
489            _ => type_mismatch_err(),
490        },
491    }
492}
493
494/// Errors returned by [`typecheck_value_against_schematype()`] and
495/// [`typecheck_restricted_expr_against_schematype()`]
496#[derive(Debug, Diagnostic, Error)]
497pub enum TypecheckError {
498    /// The given value had a type different than what was expected
499    #[error(transparent)]
500    #[diagnostic(transparent)]
501    TypeMismatch(#[from] TypeMismatchError),
502    /// Error looking up an extension function. This error can occur when
503    /// typechecking a `RestrictedExpr` because that may require getting
504    /// information about any extension functions referenced in the
505    /// `RestrictedExpr`; and it can occur when typechecking a `PartialValue`
506    /// because that may require getting information about any extension
507    /// functions referenced in residuals.
508    #[error(transparent)]
509    #[diagnostic(transparent)]
510    ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
511}
512
513#[cfg(test)]
514mod test_typecheck {
515    use std::collections::BTreeMap;
516
517    use cool_asserts::assert_matches;
518    use miette::Report;
519    use smol_str::ToSmolStr;
520
521    use crate::{
522        entities::{
523            conformance::TypecheckError, AttributeType, BorrowedRestrictedExpr, Expr, SchemaType,
524            Unknown,
525        },
526        extensions::Extensions,
527        test_utils::{expect_err, ExpectedErrorMessageBuilder},
528    };
529
530    use super::typecheck_restricted_expr_against_schematype;
531
532    #[test]
533    fn unknown() {
534        typecheck_restricted_expr_against_schematype(
535            BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
536            &SchemaType::Bool,
537            Extensions::all_available(),
538        )
539        .unwrap();
540        typecheck_restricted_expr_against_schematype(
541            BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
542            &SchemaType::String,
543            Extensions::all_available(),
544        )
545        .unwrap();
546        typecheck_restricted_expr_against_schematype(
547            BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
548            &SchemaType::Set {
549                element_ty: Box::new(SchemaType::Extension {
550                    name: "decimal".parse().unwrap(),
551                }),
552            },
553            Extensions::all_available(),
554        )
555        .unwrap();
556    }
557
558    #[test]
559    fn bool() {
560        typecheck_restricted_expr_against_schematype(
561            BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
562            &SchemaType::Bool,
563            Extensions::all_available(),
564        )
565        .unwrap();
566    }
567
568    #[test]
569    fn bool_fails() {
570        assert_matches!(
571            typecheck_restricted_expr_against_schematype(
572                BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
573                &SchemaType::Bool,
574                Extensions::all_available(),
575            ),
576            Err(e@TypecheckError::TypeMismatch(_)) => {
577                expect_err(
578                    "",
579                    &Report::new(e),
580                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type bool, but it actually has type long: `1`").build()
581                );
582            }
583        )
584    }
585
586    #[test]
587    fn long() {
588        typecheck_restricted_expr_against_schematype(
589            BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
590            &SchemaType::Long,
591            Extensions::all_available(),
592        )
593        .unwrap();
594    }
595
596    #[test]
597    fn long_fails() {
598        assert_matches!(
599            typecheck_restricted_expr_against_schematype(
600                BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
601                &SchemaType::Long,
602                Extensions::all_available(),
603            ),
604            Err(e@TypecheckError::TypeMismatch(_)) => {
605                expect_err(
606                    "",
607                    &Report::new(e),
608                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
609                );
610            }
611        )
612    }
613
614    #[test]
615    fn string() {
616        typecheck_restricted_expr_against_schematype(
617            BorrowedRestrictedExpr::new(&r#""foo""#.parse().unwrap()).unwrap(),
618            &SchemaType::String,
619            Extensions::all_available(),
620        )
621        .unwrap();
622    }
623
624    #[test]
625    fn string_fails() {
626        assert_matches!(
627            typecheck_restricted_expr_against_schematype(
628                BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
629                &SchemaType::String,
630                Extensions::all_available(),
631            ),
632            Err(e@TypecheckError::TypeMismatch(_)) => {
633                expect_err(
634                    "",
635                    &Report::new(e),
636                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type bool: `false`").build()
637                );
638            }
639        )
640    }
641
642    #[test]
643    fn test_typecheck_set() {
644        typecheck_restricted_expr_against_schematype(
645            BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
646            &SchemaType::Set {
647                element_ty: Box::new(SchemaType::Long),
648            },
649            Extensions::all_available(),
650        )
651        .unwrap();
652        typecheck_restricted_expr_against_schematype(
653            BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
654            &SchemaType::Set {
655                element_ty: Box::new(SchemaType::Bool),
656            },
657            Extensions::all_available(),
658        )
659        .unwrap();
660    }
661
662    #[test]
663    fn test_typecheck_set_fails() {
664        assert_matches!(
665            typecheck_restricted_expr_against_schematype(
666                BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
667                &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
668                Extensions::all_available(),
669            ),
670            Err(e@TypecheckError::TypeMismatch(_)) => {
671                expect_err(
672                    "",
673                    &Report::new(e),
674                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type [string], but it actually has type record: `{}`").build()
675                );
676            }
677        );
678        assert_matches!(
679            typecheck_restricted_expr_against_schematype(
680                BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
681                &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
682                Extensions::all_available(),
683            ),
684            Err(e@TypecheckError::TypeMismatch(_)) => {
685                expect_err(
686                    "",
687                    &Report::new(e),
688                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type long: `1`").build()
689                );
690            }
691        );
692        assert_matches!(
693            typecheck_restricted_expr_against_schematype(
694                BorrowedRestrictedExpr::new(&"[1, true]".parse().unwrap()).unwrap(),
695                &SchemaType::Set { element_ty: Box::new(SchemaType::Long) },
696                Extensions::all_available(),
697            ),
698            Err(e@TypecheckError::TypeMismatch(_)) => {
699                expect_err(
700                    "",
701                    &Report::new(e),
702                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `true`").build()
703                );
704            }
705        )
706    }
707
708    #[test]
709    fn test_typecheck_record() {
710        typecheck_restricted_expr_against_schematype(
711            BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
712            &SchemaType::Record {
713                attrs: BTreeMap::new(),
714                open_attrs: false,
715            },
716            Extensions::all_available(),
717        )
718        .unwrap();
719        typecheck_restricted_expr_against_schematype(
720            BorrowedRestrictedExpr::new(&"{a: 1}".parse().unwrap()).unwrap(),
721            &SchemaType::Record {
722                attrs: BTreeMap::from([(
723                    "a".to_smolstr(),
724                    AttributeType {
725                        attr_type: SchemaType::Long,
726                        required: true,
727                    },
728                )]),
729                open_attrs: false,
730            },
731            Extensions::all_available(),
732        )
733        .unwrap();
734        typecheck_restricted_expr_against_schematype(
735            BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
736            &SchemaType::Record {
737                attrs: BTreeMap::from([(
738                    "a".to_smolstr(),
739                    AttributeType {
740                        attr_type: SchemaType::Long,
741                        required: false,
742                    },
743                )]),
744                open_attrs: false,
745            },
746            Extensions::all_available(),
747        )
748        .unwrap();
749    }
750
751    #[test]
752    fn test_typecheck_record_fails() {
753        assert_matches!(
754            typecheck_restricted_expr_against_schematype(
755                BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
756                &SchemaType::Record { attrs: BTreeMap::from([]), open_attrs: false },
757                Extensions::all_available(),
758            ),
759            Err(e@TypecheckError::TypeMismatch(_)) => {
760                expect_err(
761                    "",
762                    &Report::new(e),
763                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type {  }, but it actually has type set: `[]`").build()
764                );
765            }
766        );
767        assert_matches!(
768            typecheck_restricted_expr_against_schematype(
769                BorrowedRestrictedExpr::new(&"{a: false}".parse().unwrap()).unwrap(),
770                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
771                Extensions::all_available(),
772            ),
773            Err(e@TypecheckError::TypeMismatch(_)) => {
774                expect_err(
775                    "",
776                    &Report::new(e),
777                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
778                );
779            }
780        );
781        assert_matches!(
782            typecheck_restricted_expr_against_schematype(
783                BorrowedRestrictedExpr::new(&"{a: {}}".parse().unwrap()).unwrap(),
784                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
785                Extensions::all_available(),
786            ),
787            Err(e@TypecheckError::TypeMismatch(_)) => {
788                expect_err(
789                    "",
790                    &Report::new(e),
791                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type record: `{}`").build()
792                );
793            }
794        );
795        assert_matches!(
796            typecheck_restricted_expr_against_schematype(
797                BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
798                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
799                Extensions::all_available(),
800            ),
801            Err(e@TypecheckError::TypeMismatch(_)) => {
802                expect_err(
803                    "",
804                    &Report::new(e),
805                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it is missing the required attribute `a`: `{}`"#).build()
806                );
807            }
808        );
809        assert_matches!(
810            typecheck_restricted_expr_against_schematype(
811                BorrowedRestrictedExpr::new(&"{a: 1, b: 1}".parse().unwrap()).unwrap(),
812                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
813                Extensions::all_available(),
814            ),
815            Err(e@TypecheckError::TypeMismatch(_)) => {
816                expect_err(
817                    "",
818                    &Report::new(e),
819                    &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()
820                );
821            }
822        );
823        assert_matches!(
824            typecheck_restricted_expr_against_schematype(
825                BorrowedRestrictedExpr::new(&"{b: 1}".parse().unwrap()).unwrap(),
826                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
827                Extensions::all_available(),
828            ),
829            Err(e@TypecheckError::TypeMismatch(_)) => {
830                expect_err(
831                    "",
832                    &Report::new(e),
833                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (optional) long }, but it contains an unexpected attribute `b`: `{"b": 1}`"#).build()
834                );
835            }
836        );
837    }
838
839    #[test]
840    fn extension() {
841        typecheck_restricted_expr_against_schematype(
842            BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
843            &SchemaType::Extension {
844                name: "decimal".parse().unwrap(),
845            },
846            Extensions::all_available(),
847        )
848        .unwrap();
849    }
850
851    #[test]
852    fn non_constructor_extension_function() {
853        typecheck_restricted_expr_against_schematype(
854            BorrowedRestrictedExpr::new(&r#"ip("127.0.0.1").isLoopback()"#.parse().unwrap())
855                .unwrap(),
856            &SchemaType::Bool,
857            Extensions::all_available(),
858        )
859        .unwrap();
860    }
861
862    #[test]
863    fn extension_fails() {
864        assert_matches!(
865            typecheck_restricted_expr_against_schematype(
866                BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
867                &SchemaType::Extension { name: "ipaddr".parse().unwrap() },
868                Extensions::all_available(),
869            ),
870            Err(e@TypecheckError::TypeMismatch(_)) => {
871                expect_err(
872                    "",
873                    &Report::new(e),
874                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("1.1")`"#).build()
875                );
876            }
877        )
878    }
879
880    #[test]
881    fn entity() {
882        typecheck_restricted_expr_against_schematype(
883            BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
884            &SchemaType::Entity {
885                ty: "User".parse().unwrap(),
886            },
887            Extensions::all_available(),
888        )
889        .unwrap();
890    }
891
892    #[test]
893    fn entity_fails() {
894        assert_matches!(
895            typecheck_restricted_expr_against_schematype(
896                BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
897                &SchemaType::Entity { ty: "Photo".parse().unwrap() },
898                Extensions::all_available(),
899            ),
900            Err(e@TypecheckError::TypeMismatch(_)) => {
901                expect_err(
902                    "",
903                    &Report::new(e),
904                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type `Photo`, but it actually has type (entity of type `User`): `User::"alice"`"#).build()
905                );
906            }
907        )
908    }
909}