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
17use std::collections::BTreeMap;
18
19use super::{json::err::TypeMismatchError, EntityTypeDescription, Schema, SchemaType};
20use super::{Eid, EntityUID, Literal};
21use crate::ast::{
22    BorrowedRestrictedExpr, Entity, PartialValue, PartialValueToRestrictedExprError, RestrictedExpr,
23};
24use crate::entities::ExprKind;
25use crate::extensions::{ExtensionFunctionLookupError, Extensions};
26use miette::Diagnostic;
27use smol_str::SmolStr;
28use thiserror::Error;
29pub mod err;
30
31use err::{EntitySchemaConformanceError, InvalidEnumEntityError, UnexpectedEntityTypeError};
32
33/// Struct used to check whether entities conform to a schema
34#[derive(Debug, Clone)]
35pub struct EntitySchemaConformanceChecker<'a, S> {
36    /// Schema to check conformance with
37    schema: &'a S,
38    /// Extensions which are active for the conformance checks
39    extensions: &'a Extensions<'a>,
40}
41
42impl<'a, S> EntitySchemaConformanceChecker<'a, S> {
43    /// Create a new checker
44    pub fn new(schema: &'a S, extensions: &'a Extensions<'a>) -> Self {
45        Self { schema, extensions }
46    }
47}
48
49impl<S: Schema> EntitySchemaConformanceChecker<'_, S> {
50    /// Validate an entity against the schema, returning an
51    /// [`EntitySchemaConformanceError`] if it does not comply.
52    pub fn validate_entity(&self, entity: &Entity) -> Result<(), EntitySchemaConformanceError> {
53        let uid = entity.uid();
54        let etype = uid.entity_type();
55        if etype.is_action() {
56            let schema_action = self
57                .schema
58                .action(uid)
59                .ok_or_else(|| EntitySchemaConformanceError::undeclared_action(uid.clone()))?;
60            // check that the action exactly matches the schema's definition
61            if !entity.deep_eq(&schema_action) {
62                return Err(EntitySchemaConformanceError::action_declaration_mismatch(
63                    uid.clone(),
64                ));
65            }
66        } else {
67            validate_euid(self.schema, uid)
68                .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
69            let schema_etype = self.schema.entity_type(etype).ok_or_else(|| {
70                let suggested_types = self
71                    .schema
72                    .entity_types_with_basename(&etype.name().basename())
73                    .collect();
74                UnexpectedEntityTypeError {
75                    uid: uid.clone(),
76                    suggested_types,
77                }
78            })?;
79            // Ensure that all required attributes for `etype` are actually
80            // included in `entity`
81            for required_attr in schema_etype.required_attrs() {
82                if entity.get(&required_attr).is_none() {
83                    return Err(EntitySchemaConformanceError::missing_entity_attr(
84                        uid.clone(),
85                        required_attr,
86                    ));
87                }
88            }
89            // For each attribute that actually appears in `entity`, ensure it
90            // complies with the schema
91            for (attr, val) in entity.attrs() {
92                match schema_etype.attr_type(attr) {
93                    None => {
94                        // `None` indicates the attribute shouldn't exist -- see
95                        // docs on the `attr_type()` trait method
96                        if !schema_etype.open_attributes() {
97                            return Err(EntitySchemaConformanceError::unexpected_entity_attr(
98                                uid.clone(),
99                                attr.clone(),
100                            ));
101                        }
102                    }
103                    Some(expected_ty) => {
104                        // typecheck: ensure that the entity attribute value matches
105                        // the expected type
106                        match typecheck_value_against_schematype(val, &expected_ty, self.extensions)
107                        {
108                            Ok(()) => {} // typecheck passes
109                            Err(TypecheckError::TypeMismatch(err)) => {
110                                return Err(EntitySchemaConformanceError::type_mismatch(
111                                    uid.clone(),
112                                    attr.clone(),
113                                    err,
114                                ));
115                            }
116                            Err(TypecheckError::ExtensionFunctionLookup(err)) => {
117                                return Err(
118                                    EntitySchemaConformanceError::extension_function_lookup(
119                                        uid.clone(),
120                                        attr.clone(),
121                                        err,
122                                    ),
123                                );
124                            }
125                        }
126                    }
127                }
128                validate_euids_in_partial_value(self.schema, val)
129                    .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
130            }
131            // For each ancestor that actually appears in `entity`, ensure the
132            // ancestor type is allowed by the schema
133            for ancestor_euid in entity.ancestors() {
134                validate_euid(self.schema, ancestor_euid)
135                    .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
136                let ancestor_type = ancestor_euid.entity_type();
137                if schema_etype.allowed_parent_types().contains(ancestor_type) {
138                    // note that `allowed_parent_types()` was transitively
139                    // closed, so it's actually `allowed_ancestor_types()`
140                    //
141                    // thus, the check passes in this case
142                } else {
143                    return Err(EntitySchemaConformanceError::invalid_ancestor_type(
144                        uid.clone(),
145                        ancestor_type.clone(),
146                    ));
147                }
148            }
149
150            for (_, val) in entity.tags() {
151                validate_euids_in_partial_value(self.schema, val)
152                    .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
153            }
154        }
155        Ok(())
156    }
157}
158
159/// Return an [`InvalidEnumEntityError`] if `uid`'s eid is not among valid `choices`
160pub fn is_valid_enumerated_entity(
161    choices: &[Eid],
162    uid: &EntityUID,
163) -> Result<(), InvalidEnumEntityError> {
164    choices
165        .iter()
166        .find(|id| uid.eid() == *id)
167        .ok_or_else(|| InvalidEnumEntityError {
168            uid: uid.clone(),
169            choices: choices.to_vec(),
170        })
171        .map(|_| ())
172}
173
174/// Validate if `euid` is valid, provided that it's of an enumerated type
175pub(crate) fn validate_euid(
176    schema: &impl Schema,
177    euid: &EntityUID,
178) -> Result<(), InvalidEnumEntityError> {
179    if let Some(desc) = schema.entity_type(euid.entity_type()) {
180        if let Some(choices) = desc.enum_entity_eids() {
181            is_valid_enumerated_entity(&Vec::from(choices), euid)?;
182        }
183    }
184    Ok(())
185}
186
187fn validate_euids_in_subexpressions<'a>(
188    exprs: impl IntoIterator<Item = &'a crate::ast::Expr>,
189    schema: &impl Schema,
190) -> std::result::Result<(), InvalidEnumEntityError> {
191    exprs.into_iter().try_for_each(|e| match e.expr_kind() {
192        ExprKind::Lit(Literal::EntityUID(euid)) => validate_euid(schema, euid.as_ref()),
193        _ => Ok(()),
194    })
195}
196
197/// Validate if enumerated entities in `val` are valid
198pub fn validate_euids_in_partial_value(
199    schema: &impl Schema,
200    val: &PartialValue,
201) -> Result<(), InvalidEnumEntityError> {
202    match val {
203        PartialValue::Value(val) => validate_euids_in_subexpressions(
204            RestrictedExpr::from(val.clone()).subexpressions(),
205            schema,
206        ),
207        PartialValue::Residual(e) => validate_euids_in_subexpressions(e.subexpressions(), schema),
208    }
209}
210
211/// Check whether the given `PartialValue` typechecks with the given `SchemaType`.
212/// If the typecheck passes, return `Ok(())`.
213/// If the typecheck fails, return an appropriate `Err`.
214pub fn typecheck_value_against_schematype(
215    value: &PartialValue,
216    expected_ty: &SchemaType,
217    extensions: &Extensions<'_>,
218) -> Result<(), TypecheckError> {
219    match RestrictedExpr::try_from(value.clone()) {
220        Ok(expr) => typecheck_restricted_expr_against_schematype(
221            expr.as_borrowed(),
222            expected_ty,
223            extensions,
224        ),
225        Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
226            // this case should be unreachable for the case of `PartialValue`s
227            // which are entity attributes, because a `PartialValue` computed
228            // from a `RestrictedExpr` should only have trivial residuals.
229            // And as of this writing, there are no callers of this function that
230            // pass anything other than entity attributes.
231            // Nonetheless, rather than relying on these delicate invariants,
232            // it's safe to consider this as passing.
233            Ok(())
234        }
235    }
236}
237
238/// Check whether the given `RestrictedExpr` is a valid instance of
239/// `SchemaType`.  We do not have type information for unknowns, so this
240/// function liberally treats unknowns as implementing any schema type.  If the
241/// typecheck passes, return `Ok(())`.  If the typecheck fails, return an
242/// appropriate `Err`.
243pub fn typecheck_restricted_expr_against_schematype(
244    expr: BorrowedRestrictedExpr<'_>,
245    expected_ty: &SchemaType,
246    extensions: &Extensions<'_>,
247) -> Result<(), TypecheckError> {
248    use SchemaType::*;
249    let type_mismatch_err = || {
250        Err(TypeMismatchError::type_mismatch(
251            expected_ty.clone(),
252            expr.try_type_of(extensions),
253            expr.to_owned(),
254        )
255        .into())
256    };
257
258    match expr.expr_kind() {
259        // Check for `unknowns`.  Unless explicitly annotated, we don't have the
260        // information to know whether the unknown value matches the expected type.
261        // For now we consider this as passing -- we can't really report a type
262        // error <https://github.com/cedar-policy/cedar/issues/418>.
263        ExprKind::Unknown(u) => match u.type_annotation.clone().and_then(SchemaType::from_ty) {
264            Some(ty) => {
265                if &ty == expected_ty {
266                    return Ok(());
267                } else {
268                    return type_mismatch_err();
269                }
270            }
271            None => return Ok(()),
272        },
273        // Check for extension function calls. Restricted expressions permit all
274        // extension function calls, including those that aren't constructors.
275        // Checking the return type here before matching on the expected type lets
276        // us handle extension functions that return, e.g., bool and not an extension type.
277        ExprKind::ExtensionFunctionApp { fn_name, .. } => {
278            return match extensions.func(fn_name)?.return_type() {
279                None => {
280                    // This is actually another `unknown` case. The return type
281                    // is `None` only when the function is an "unknown"
282                    Ok(())
283                }
284                Some(rty) => {
285                    if rty == expected_ty {
286                        Ok(())
287                    } else {
288                        type_mismatch_err()
289                    }
290                }
291            };
292        }
293        _ => (),
294    };
295
296    // We know `expr` is a restricted expression, so it must either be an
297    // extension function call or a literal bool, long string, set or record.
298    // This means we don't need to check if it's a `has` or `==` expression to
299    // decide if it typechecks against `Bool`. Anything other an than a boolean
300    // literal is an error. To handle extension function calls, which could
301    // return `Bool`, we have already checked if the expression is an extension
302    // function in the prior `match` expression.
303    match expected_ty {
304        Bool => {
305            if expr.as_bool().is_some() {
306                Ok(())
307            } else {
308                type_mismatch_err()
309            }
310        }
311        Long => {
312            if expr.as_long().is_some() {
313                Ok(())
314            } else {
315                type_mismatch_err()
316            }
317        }
318        String => {
319            if expr.as_string().is_some() {
320                Ok(())
321            } else {
322                type_mismatch_err()
323            }
324        }
325        EmptySet => {
326            if expr.as_set_elements().is_some_and(|e| e.count() == 0) {
327                Ok(())
328            } else {
329                type_mismatch_err()
330            }
331        }
332        Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => Ok(()),
333        Set { element_ty: elty } => match expr.as_set_elements() {
334            Some(mut els) => els.try_for_each(|e| {
335                typecheck_restricted_expr_against_schematype(e, elty, extensions)
336            }),
337            None => type_mismatch_err(),
338        },
339        Record { attrs, open_attrs } => match expr.as_record_pairs() {
340            Some(pairs) => {
341                let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
342                // Check that all attributes required by the schema are present
343                // in the record.
344                attrs.iter().try_for_each(|(k, v)| {
345                    if !v.required {
346                        Ok(())
347                    } else {
348                        match pairs_map.get(k) {
349                            Some(inner_e) => typecheck_restricted_expr_against_schematype(
350                                *inner_e,
351                                &v.attr_type,
352                                extensions,
353                            ),
354                            None => Err(TypeMismatchError::missing_required_attr(
355                                expected_ty.clone(),
356                                k.clone(),
357                                expr.to_owned(),
358                            )
359                            .into()),
360                        }
361                    }
362                })?;
363                // Check that all attributes in the record are present (as
364                // required or optional) in the schema.
365                pairs_map
366                    .iter()
367                    .try_for_each(|(k, inner_e)| match attrs.get(*k) {
368                        Some(sch_ty) => typecheck_restricted_expr_against_schematype(
369                            *inner_e,
370                            &sch_ty.attr_type,
371                            extensions,
372                        ),
373                        None => {
374                            if *open_attrs {
375                                Ok(())
376                            } else {
377                                Err(TypeMismatchError::unexpected_attr(
378                                    expected_ty.clone(),
379                                    (*k).clone(),
380                                    expr.to_owned(),
381                                )
382                                .into())
383                            }
384                        }
385                    })?;
386                Ok(())
387            }
388            None => type_mismatch_err(),
389        },
390        // Extension functions are handled by the first `match` in this function.
391        Extension { .. } => type_mismatch_err(),
392        Entity { ty } => match expr.as_euid() {
393            Some(actual_euid) if actual_euid.entity_type() == ty => Ok(()),
394            _ => type_mismatch_err(),
395        },
396    }
397}
398
399/// Errors returned by [`typecheck_value_against_schematype()`] and
400/// [`typecheck_restricted_expr_against_schematype()`]
401#[derive(Debug, Diagnostic, Error)]
402pub enum TypecheckError {
403    /// The given value had a type different than what was expected
404    #[error(transparent)]
405    #[diagnostic(transparent)]
406    TypeMismatch(#[from] TypeMismatchError),
407    /// Error looking up an extension function. This error can occur when
408    /// typechecking a `RestrictedExpr` because that may require getting
409    /// information about any extension functions referenced in the
410    /// `RestrictedExpr`; and it can occur when typechecking a `PartialValue`
411    /// because that may require getting information about any extension
412    /// functions referenced in residuals.
413    #[error(transparent)]
414    #[diagnostic(transparent)]
415    ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
416}
417
418#[cfg(test)]
419mod test_typecheck {
420    use std::collections::BTreeMap;
421
422    use cool_asserts::assert_matches;
423    use miette::Report;
424    use smol_str::ToSmolStr;
425
426    use crate::{
427        entities::{
428            conformance::TypecheckError, AttributeType, BorrowedRestrictedExpr, Expr, SchemaType,
429            Unknown,
430        },
431        extensions::Extensions,
432        test_utils::{expect_err, ExpectedErrorMessageBuilder},
433    };
434
435    use super::typecheck_restricted_expr_against_schematype;
436
437    #[test]
438    fn unknown() {
439        typecheck_restricted_expr_against_schematype(
440            BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
441            &SchemaType::Bool,
442            Extensions::all_available(),
443        )
444        .unwrap();
445        typecheck_restricted_expr_against_schematype(
446            BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
447            &SchemaType::String,
448            Extensions::all_available(),
449        )
450        .unwrap();
451        typecheck_restricted_expr_against_schematype(
452            BorrowedRestrictedExpr::new(&Expr::unknown(Unknown::new_untyped("foo"))).unwrap(),
453            &SchemaType::Set {
454                element_ty: Box::new(SchemaType::Extension {
455                    name: "decimal".parse().unwrap(),
456                }),
457            },
458            Extensions::all_available(),
459        )
460        .unwrap();
461    }
462
463    #[test]
464    fn bool() {
465        typecheck_restricted_expr_against_schematype(
466            BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
467            &SchemaType::Bool,
468            Extensions::all_available(),
469        )
470        .unwrap();
471    }
472
473    #[test]
474    fn bool_fails() {
475        assert_matches!(
476            typecheck_restricted_expr_against_schematype(
477                BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
478                &SchemaType::Bool,
479                Extensions::all_available(),
480            ),
481            Err(e@TypecheckError::TypeMismatch(_)) => {
482                expect_err(
483                    "",
484                    &Report::new(e),
485                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type bool, but it actually has type long: `1`").build()
486                );
487            }
488        )
489    }
490
491    #[test]
492    fn long() {
493        typecheck_restricted_expr_against_schematype(
494            BorrowedRestrictedExpr::new(&"1".parse().unwrap()).unwrap(),
495            &SchemaType::Long,
496            Extensions::all_available(),
497        )
498        .unwrap();
499    }
500
501    #[test]
502    fn long_fails() {
503        assert_matches!(
504            typecheck_restricted_expr_against_schematype(
505                BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
506                &SchemaType::Long,
507                Extensions::all_available(),
508            ),
509            Err(e@TypecheckError::TypeMismatch(_)) => {
510                expect_err(
511                    "",
512                    &Report::new(e),
513                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
514                );
515            }
516        )
517    }
518
519    #[test]
520    fn string() {
521        typecheck_restricted_expr_against_schematype(
522            BorrowedRestrictedExpr::new(&r#""foo""#.parse().unwrap()).unwrap(),
523            &SchemaType::String,
524            Extensions::all_available(),
525        )
526        .unwrap();
527    }
528
529    #[test]
530    fn string_fails() {
531        assert_matches!(
532            typecheck_restricted_expr_against_schematype(
533                BorrowedRestrictedExpr::new(&"false".parse().unwrap()).unwrap(),
534                &SchemaType::String,
535                Extensions::all_available(),
536            ),
537            Err(e@TypecheckError::TypeMismatch(_)) => {
538                expect_err(
539                    "",
540                    &Report::new(e),
541                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type bool: `false`").build()
542                );
543            }
544        )
545    }
546
547    #[test]
548    fn test_typecheck_set() {
549        typecheck_restricted_expr_against_schematype(
550            BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
551            &SchemaType::Set {
552                element_ty: Box::new(SchemaType::Long),
553            },
554            Extensions::all_available(),
555        )
556        .unwrap();
557        typecheck_restricted_expr_against_schematype(
558            BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
559            &SchemaType::Set {
560                element_ty: Box::new(SchemaType::Bool),
561            },
562            Extensions::all_available(),
563        )
564        .unwrap();
565    }
566
567    #[test]
568    fn test_typecheck_set_fails() {
569        assert_matches!(
570            typecheck_restricted_expr_against_schematype(
571                BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
572                &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
573                Extensions::all_available(),
574            ),
575            Err(e@TypecheckError::TypeMismatch(_)) => {
576                expect_err(
577                    "",
578                    &Report::new(e),
579                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type [string], but it actually has type record: `{}`").build()
580                );
581            }
582        );
583        assert_matches!(
584            typecheck_restricted_expr_against_schematype(
585                BorrowedRestrictedExpr::new(&"[1, 2, 3]".parse().unwrap()).unwrap(),
586                &SchemaType::Set { element_ty: Box::new(SchemaType::String) },
587                Extensions::all_available(),
588            ),
589            Err(e@TypecheckError::TypeMismatch(_)) => {
590                expect_err(
591                    "",
592                    &Report::new(e),
593                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type string, but it actually has type long: `1`").build()
594                );
595            }
596        );
597        assert_matches!(
598            typecheck_restricted_expr_against_schematype(
599                BorrowedRestrictedExpr::new(&"[1, true]".parse().unwrap()).unwrap(),
600                &SchemaType::Set { element_ty: Box::new(SchemaType::Long) },
601                Extensions::all_available(),
602            ),
603            Err(e@TypecheckError::TypeMismatch(_)) => {
604                expect_err(
605                    "",
606                    &Report::new(e),
607                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `true`").build()
608                );
609            }
610        )
611    }
612
613    #[test]
614    fn test_typecheck_record() {
615        typecheck_restricted_expr_against_schematype(
616            BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
617            &SchemaType::Record {
618                attrs: BTreeMap::new(),
619                open_attrs: false,
620            },
621            Extensions::all_available(),
622        )
623        .unwrap();
624        typecheck_restricted_expr_against_schematype(
625            BorrowedRestrictedExpr::new(&"{a: 1}".parse().unwrap()).unwrap(),
626            &SchemaType::Record {
627                attrs: BTreeMap::from([(
628                    "a".to_smolstr(),
629                    AttributeType {
630                        attr_type: SchemaType::Long,
631                        required: true,
632                    },
633                )]),
634                open_attrs: false,
635            },
636            Extensions::all_available(),
637        )
638        .unwrap();
639        typecheck_restricted_expr_against_schematype(
640            BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
641            &SchemaType::Record {
642                attrs: BTreeMap::from([(
643                    "a".to_smolstr(),
644                    AttributeType {
645                        attr_type: SchemaType::Long,
646                        required: false,
647                    },
648                )]),
649                open_attrs: false,
650            },
651            Extensions::all_available(),
652        )
653        .unwrap();
654    }
655
656    #[test]
657    fn test_typecheck_record_fails() {
658        assert_matches!(
659            typecheck_restricted_expr_against_schematype(
660                BorrowedRestrictedExpr::new(&"[]".parse().unwrap()).unwrap(),
661                &SchemaType::Record { attrs: BTreeMap::from([]), open_attrs: false },
662                Extensions::all_available(),
663            ),
664            Err(e@TypecheckError::TypeMismatch(_)) => {
665                expect_err(
666                    "",
667                    &Report::new(e),
668                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type {  }, but it actually has type set: `[]`").build()
669                );
670            }
671        );
672        assert_matches!(
673            typecheck_restricted_expr_against_schematype(
674                BorrowedRestrictedExpr::new(&"{a: false}".parse().unwrap()).unwrap(),
675                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
676                Extensions::all_available(),
677            ),
678            Err(e@TypecheckError::TypeMismatch(_)) => {
679                expect_err(
680                    "",
681                    &Report::new(e),
682                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type bool: `false`").build()
683                );
684            }
685        );
686        assert_matches!(
687            typecheck_restricted_expr_against_schematype(
688                BorrowedRestrictedExpr::new(&"{a: {}}".parse().unwrap()).unwrap(),
689                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
690                Extensions::all_available(),
691            ),
692            Err(e@TypecheckError::TypeMismatch(_)) => {
693                expect_err(
694                    "",
695                    &Report::new(e),
696                    &ExpectedErrorMessageBuilder::error("type mismatch: value was expected to have type long, but it actually has type record: `{}`").build()
697                );
698            }
699        );
700        assert_matches!(
701            typecheck_restricted_expr_against_schematype(
702                BorrowedRestrictedExpr::new(&"{}".parse().unwrap()).unwrap(),
703                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
704                Extensions::all_available(),
705            ),
706            Err(e@TypecheckError::TypeMismatch(_)) => {
707                expect_err(
708                    "",
709                    &Report::new(e),
710                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it is missing the required attribute `a`: `{}`"#).build()
711                );
712            }
713        );
714        assert_matches!(
715            typecheck_restricted_expr_against_schematype(
716                BorrowedRestrictedExpr::new(&"{a: 1, b: 1}".parse().unwrap()).unwrap(),
717                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: true })]), open_attrs: false },
718                Extensions::all_available(),
719            ),
720            Err(e@TypecheckError::TypeMismatch(_)) => {
721                expect_err(
722                    "",
723                    &Report::new(e),
724                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (required) long }, but it contains an unexpected attribute `b`: `{"a": 1, "b": 1}`"#).build()
725                );
726            }
727        );
728        assert_matches!(
729            typecheck_restricted_expr_against_schematype(
730                BorrowedRestrictedExpr::new(&"{b: 1}".parse().unwrap()).unwrap(),
731                &SchemaType::Record { attrs: BTreeMap::from([("a".to_smolstr(), AttributeType { attr_type: SchemaType::Long, required: false })]), open_attrs: false },
732                Extensions::all_available(),
733            ),
734            Err(e@TypecheckError::TypeMismatch(_)) => {
735                expect_err(
736                    "",
737                    &Report::new(e),
738                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type { "a" => (optional) long }, but it contains an unexpected attribute `b`: `{"b": 1}`"#).build()
739                );
740            }
741        );
742    }
743
744    #[test]
745    fn extension() {
746        typecheck_restricted_expr_against_schematype(
747            BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
748            &SchemaType::Extension {
749                name: "decimal".parse().unwrap(),
750            },
751            Extensions::all_available(),
752        )
753        .unwrap();
754    }
755
756    #[test]
757    fn non_constructor_extension_function() {
758        typecheck_restricted_expr_against_schematype(
759            BorrowedRestrictedExpr::new(&r#"ip("127.0.0.1").isLoopback()"#.parse().unwrap())
760                .unwrap(),
761            &SchemaType::Bool,
762            Extensions::all_available(),
763        )
764        .unwrap();
765    }
766
767    #[test]
768    fn extension_fails() {
769        assert_matches!(
770            typecheck_restricted_expr_against_schematype(
771                BorrowedRestrictedExpr::new(&r#"decimal("1.1")"#.parse().unwrap()).unwrap(),
772                &SchemaType::Extension { name: "ipaddr".parse().unwrap() },
773                Extensions::all_available(),
774            ),
775            Err(e@TypecheckError::TypeMismatch(_)) => {
776                expect_err(
777                    "",
778                    &Report::new(e),
779                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("1.1")`"#).build()
780                );
781            }
782        )
783    }
784
785    #[test]
786    fn entity() {
787        typecheck_restricted_expr_against_schematype(
788            BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
789            &SchemaType::Entity {
790                ty: "User".parse().unwrap(),
791            },
792            Extensions::all_available(),
793        )
794        .unwrap();
795    }
796
797    #[test]
798    fn entity_fails() {
799        assert_matches!(
800            typecheck_restricted_expr_against_schematype(
801                BorrowedRestrictedExpr::new(&r#"User::"alice""#.parse().unwrap()).unwrap(),
802                &SchemaType::Entity { ty: "Photo".parse().unwrap() },
803                Extensions::all_available(),
804            ),
805            Err(e@TypecheckError::TypeMismatch(_)) => {
806                expect_err(
807                    "",
808                    &Report::new(e),
809                    &ExpectedErrorMessageBuilder::error(r#"type mismatch: value was expected to have type `Photo`, but it actually has type (entity of type `User`): `User::"alice"`"#).build()
810                );
811            }
812        )
813    }
814}