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