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