1use miette::Diagnostic;
20use thiserror::Error;
21
22use std::fmt::Display;
23use std::ops::{Add, Neg};
24
25use cedar_policy_core::fuzzy_match::fuzzy_search;
26use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field;
27use cedar_policy_core::parser::Loc;
28
29use std::collections::BTreeSet;
30
31use cedar_policy_core::ast::{Eid, EntityType, EntityUID, Expr, ExprKind, PolicyID, Var};
32use cedar_policy_core::parser::join_with_conjunction;
33
34use crate::types::{EntityLUB, EntityRecordKind, RequestEnv, Type};
35use crate::ValidatorSchema;
36use itertools::Itertools;
37use smol_str::SmolStr;
38
39#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
41#[error("for policy `{policy_id}`, unrecognized entity type `{actual_entity_type}`")]
43pub struct UnrecognizedEntityType {
44 pub source_loc: Option<Loc>,
46 pub policy_id: PolicyID,
48 pub actual_entity_type: String,
50 pub suggested_entity_type: Option<String>,
53}
54
55impl Diagnostic for UnrecognizedEntityType {
56 impl_diagnostic_from_source_loc_opt_field!(source_loc);
57
58 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
59 match &self.suggested_entity_type {
60 Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
61 None => None,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
68#[error("for policy `{policy_id}`, unrecognized action `{actual_action_id}`")]
69pub struct UnrecognizedActionId {
70 pub source_loc: Option<Loc>,
72 pub policy_id: PolicyID,
74 pub actual_action_id: String,
76 pub hint: Option<UnrecognizedActionIdHelp>,
78}
79
80impl Diagnostic for UnrecognizedActionId {
81 impl_diagnostic_from_source_loc_opt_field!(source_loc);
82
83 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
84 self.hint
85 .as_ref()
86 .map(|help| Box::new(help) as Box<dyn std::fmt::Display>)
87 }
88}
89
90#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
92pub enum UnrecognizedActionIdHelp {
93 #[error("did you intend to include the type in action `{0}`?")]
95 AvoidActionTypeInActionId(String),
96 #[error("did you mean `{0}`?")]
98 SuggestAlternative(String),
99}
100
101pub fn unrecognized_action_id_help(
103 euid: &EntityUID,
104 schema: &ValidatorSchema,
105) -> Option<UnrecognizedActionIdHelp> {
106 let eid_str: &str = euid.eid().as_ref();
108 let eid_with_type = format!("Action::{}", eid_str);
109 let eid_with_type_and_quotes = format!("Action::\"{}\"", eid_str);
110 let maybe_id_with_type = schema.known_action_ids().find(|euid| {
111 let eid = <Eid as AsRef<str>>::as_ref(euid.eid());
112 eid.contains(&eid_with_type) || eid.contains(&eid_with_type_and_quotes)
113 });
114 if let Some(id) = maybe_id_with_type {
115 Some(UnrecognizedActionIdHelp::AvoidActionTypeInActionId(
117 id.to_string(),
118 ))
119 } else {
120 let euids_strs = schema
122 .known_action_ids()
123 .map(ToString::to_string)
124 .collect::<Vec<_>>();
125 fuzzy_search(euid.eid().as_ref(), &euids_strs)
126 .map(UnrecognizedActionIdHelp::SuggestAlternative)
127 }
128}
129
130#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
132#[error("for policy `{policy_id}`, unable to find an applicable action given the policy scope constraints")]
133pub struct InvalidActionApplication {
134 pub source_loc: Option<Loc>,
136 pub policy_id: PolicyID,
138 pub would_in_fix_principal: bool,
140 pub would_in_fix_resource: bool,
142}
143
144impl Diagnostic for InvalidActionApplication {
145 impl_diagnostic_from_source_loc_opt_field!(source_loc);
146
147 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
148 match (self.would_in_fix_principal, self.would_in_fix_resource) {
149 (true, false) => Some(Box::new(
150 "try replacing `==` with `in` in the principal clause",
151 )),
152 (false, true) => Some(Box::new(
153 "try replacing `==` with `in` in the resource clause",
154 )),
155 (true, true) => Some(Box::new(
156 "try replacing `==` with `in` in the principal clause and the resource clause",
157 )),
158 (false, false) => None,
159 }
160 }
161}
162
163#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
165#[error("for policy `{policy_id}`, unexpected type: expected {} but saw {}",
166 match .expected.iter().next() {
167 Some(single) if .expected.len() == 1 => format!("{}", single),
168 _ => .expected.iter().join(", or ")
169 },
170 .actual)]
171pub struct UnexpectedType {
172 pub source_loc: Option<Loc>,
174 pub policy_id: PolicyID,
176 pub expected: BTreeSet<Type>,
178 pub actual: Type,
180 pub help: Option<UnexpectedTypeHelp>,
182}
183
184impl Diagnostic for UnexpectedType {
185 impl_diagnostic_from_source_loc_opt_field!(source_loc);
186
187 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
188 self.help.as_ref().map(|h| Box::new(h) as Box<dyn Display>)
189 }
190}
191
192#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
194pub enum UnexpectedTypeHelp {
195 #[error("try using `like` to examine the contents of a string")]
197 TryUsingLike,
198 #[error(
200 "try using `contains`, `containsAny`, or `containsAll` to examine the contents of a set"
201 )]
202 TryUsingContains,
203 #[error("try using `contains` to test if a single element is in a set")]
205 TryUsingSingleContains,
206 #[error("try using `has` to test for an attribute")]
208 TryUsingHas,
209 #[error("try using `is` to test for an entity type")]
211 TryUsingIs,
212 #[error("try using `in` for entity hierarchy membership")]
214 TryUsingIn,
215 #[error("Cedar only supports run time type tests for entities")]
217 TypeTestNotSupported,
218 #[error("Cedar does not support string concatenation")]
220 ConcatenationNotSupported,
221 #[error("Cedar does not support computing the union, intersection, or difference of sets")]
223 SetOperationsNotSupported,
224}
225
226#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
228pub struct IncompatibleTypes {
229 pub source_loc: Option<Loc>,
231 pub policy_id: PolicyID,
233 pub types: BTreeSet<Type>,
235 pub hint: LubHelp,
237 pub context: LubContext,
239}
240
241impl Diagnostic for IncompatibleTypes {
242 impl_diagnostic_from_source_loc_opt_field!(source_loc);
243
244 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
245 Some(Box::new(format!(
246 "for policy `{}`, {} must have compatible types. {}",
247 self.policy_id, self.context, self.hint
248 )))
249 }
250}
251
252impl Display for IncompatibleTypes {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 write!(f, "the types ")?;
255 join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
256 write!(f, " are not compatible")
257 }
258}
259
260#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
262pub enum LubHelp {
263 #[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
265 AttributeQualifier,
266 #[error("Compatible record types must have exactly the same attributes")]
268 RecordWidth,
269 #[error("Different entity types are never compatible even when their attributes would be compatible")]
271 EntityType,
272 #[error("Entity and record types are never compatible even when their attributes would be compatible")]
274 EntityRecord,
275 #[error("Types must be exactly equal to be compatible")]
277 None,
278}
279
280#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
282pub enum LubContext {
283 #[error("elements of a set")]
285 Set,
286 #[error("both branches of a conditional")]
288 Conditional,
289 #[error("both operands to a `==` expression")]
291 Equality,
292 #[error("elements of the first operand and the second operand to a `contains` expression")]
294 Contains,
295 #[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
297 ContainsAnyAll,
298 #[error("tag types for a `.getTag()` operation")]
300 GetTag,
301}
302
303#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
305#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
306pub struct UnsafeAttributeAccess {
307 pub source_loc: Option<Loc>,
309 pub policy_id: PolicyID,
311 pub attribute_access: AttributeAccess,
313 pub suggestion: Option<String>,
315 pub may_exist: bool,
318}
319
320impl Diagnostic for UnsafeAttributeAccess {
321 impl_diagnostic_from_source_loc_opt_field!(source_loc);
322
323 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
324 match (&self.suggestion, self.may_exist) {
325 (Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
326 (None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
327 (Some(suggestion), true) => Some(Box::new(format!("did you mean `{suggestion}`? (there may also be additional attributes that the validator is not able to reason about)"))),
328 (None, false) => None,
329 }
330 }
331}
332
333#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
335#[error("for policy `{policy_id}`, unable to guarantee safety of access to optional attribute {attribute_access}")]
336pub struct UnsafeOptionalAttributeAccess {
337 pub source_loc: Option<Loc>,
339 pub policy_id: PolicyID,
341 pub attribute_access: AttributeAccess,
343}
344
345impl Diagnostic for UnsafeOptionalAttributeAccess {
346 impl_diagnostic_from_source_loc_opt_field!(source_loc);
347
348 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
349 Some(Box::new(format!(
350 "try testing for the attribute's presence with `{} && ..`",
351 self.attribute_access.suggested_has_guard()
352 )))
353 }
354}
355
356#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
358#[error(
359 "for policy `{policy_id}`, unable to guarantee safety of access to tag `{tag}`{}",
360 match .entity_ty.as_ref().and_then(|lub| lub.get_single_entity()) {
361 Some(ety) => format!(" on entity type `{ety}`"),
362 None => "".to_string()
363 }
364)]
365pub struct UnsafeTagAccess {
366 pub source_loc: Option<Loc>,
368 pub policy_id: PolicyID,
370 pub entity_ty: Option<EntityLUB>,
372 pub tag: Expr<Option<Type>>,
374}
375
376impl Diagnostic for UnsafeTagAccess {
377 impl_diagnostic_from_source_loc_opt_field!(source_loc);
378
379 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
380 Some(Box::new(format!(
381 "try testing for the tag's presence with `.hasTag({}) && ..`",
382 &self.tag
383 )))
384 }
385}
386
387#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
389#[error(
390 "for policy `{policy_id}`, `.getTag()` is not allowed on entities of {} because no `tags` were declared on the entity type in the schema",
391 match .entity_ty.as_ref() {
392 Some(ty) => format!("type `{ty}`"),
393 None => "this type".to_string(),
394 }
395)]
396pub struct NoTagsAllowed {
397 pub source_loc: Option<Loc>,
399 pub policy_id: PolicyID,
401 pub entity_ty: Option<EntityType>,
405}
406
407impl Diagnostic for NoTagsAllowed {
408 impl_diagnostic_from_source_loc_opt_field!(source_loc);
409}
410
411#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
413#[error("for policy `{policy_id}`, undefined extension function: {name}")]
414pub struct UndefinedFunction {
415 pub source_loc: Option<Loc>,
417 pub policy_id: PolicyID,
419 pub name: String,
421}
422
423impl Diagnostic for UndefinedFunction {
424 impl_diagnostic_from_source_loc_opt_field!(source_loc);
425}
426
427#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
429#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
430pub struct WrongNumberArguments {
431 pub source_loc: Option<Loc>,
433 pub policy_id: PolicyID,
435 pub expected: usize,
437 pub actual: usize,
439}
440
441impl Diagnostic for WrongNumberArguments {
442 impl_diagnostic_from_source_loc_opt_field!(source_loc);
443}
444
445#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
447#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
448pub struct FunctionArgumentValidation {
449 pub source_loc: Option<Loc>,
451 pub policy_id: PolicyID,
453 pub msg: String,
455}
456
457impl Diagnostic for FunctionArgumentValidation {
458 impl_diagnostic_from_source_loc_opt_field!(source_loc);
459}
460
461#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
463#[error("for policy `{policy_id}`, operands to `in` do not respect the entity hierarchy")]
464pub struct HierarchyNotRespected {
465 pub source_loc: Option<Loc>,
467 pub policy_id: PolicyID,
469 pub in_lhs: Option<EntityType>,
471 pub in_rhs: Option<EntityType>,
473}
474
475impl Diagnostic for HierarchyNotRespected {
476 impl_diagnostic_from_source_loc_opt_field!(source_loc);
477
478 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
479 match (&self.in_lhs, &self.in_rhs) {
480 (Some(in_lhs), Some(in_rhs)) => Some(Box::new(format!(
481 "`{in_lhs}` cannot be a descendant of `{in_rhs}`"
482 ))),
483 _ => None,
484 }
485 }
486}
487
488#[derive(Default, Debug, Clone, Hash, Eq, PartialEq, Error, Copy, Ord, PartialOrd)]
490pub struct EntityDerefLevel {
491 pub level: i64,
493}
494
495impl Display for EntityDerefLevel {
496 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
497 write!(f, "{}", self.level)
498 }
499}
500
501impl From<u32> for EntityDerefLevel {
502 fn from(value: u32) -> Self {
503 EntityDerefLevel {
504 level: value as i64,
505 }
506 }
507}
508
509impl Add for EntityDerefLevel {
510 type Output = Self;
511
512 fn add(self, rhs: Self) -> Self::Output {
513 EntityDerefLevel {
514 level: self.level + rhs.level,
515 }
516 }
517}
518
519impl Neg for EntityDerefLevel {
520 type Output = Self;
521
522 fn neg(self) -> Self::Output {
523 EntityDerefLevel { level: -self.level }
524 }
525}
526
527impl EntityDerefLevel {
528 pub fn decrement(&self) -> Self {
530 EntityDerefLevel {
531 level: self.level - 1,
532 }
533 }
534}
535
536#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
538#[error("for policy `{policy_id}`, the maximum allowed level {allowed_level} is violated. Actual level is {}", (allowed_level.add(actual_level.neg())))]
539pub struct EntityDerefLevelViolation {
540 pub source_loc: Option<Loc>,
542 pub policy_id: PolicyID,
544 pub allowed_level: EntityDerefLevel,
546 pub actual_level: EntityDerefLevel,
548}
549
550impl Diagnostic for EntityDerefLevelViolation {
551 impl_diagnostic_from_source_loc_opt_field!(source_loc);
552
553 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
554 Some(Box::new("Consider increasing the level"))
555 }
556}
557
558#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
560#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
561pub struct EmptySetForbidden {
562 pub source_loc: Option<Loc>,
564 pub policy_id: PolicyID,
566}
567
568impl Diagnostic for EmptySetForbidden {
569 impl_diagnostic_from_source_loc_opt_field!(source_loc);
570}
571
572#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
575#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
576pub struct NonLitExtConstructor {
577 pub source_loc: Option<Loc>,
579 pub policy_id: PolicyID,
581}
582
583impl Diagnostic for NonLitExtConstructor {
584 impl_diagnostic_from_source_loc_opt_field!(source_loc);
585
586 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
587 Some(Box::new(
588 "consider applying extension constructors inside attribute values when constructing entity or context data"
589 ))
590 }
591}
592
593#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
596#[error("internal invariant violated")]
597pub struct InternalInvariantViolation {
598 pub source_loc: Option<Loc>,
600 pub policy_id: PolicyID,
602}
603
604impl Diagnostic for InternalInvariantViolation {
605 impl_diagnostic_from_source_loc_opt_field!(source_loc);
606
607 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
608 Some(Box::new(
609 "please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy for which you observed the issue"
610 ))
611 }
612}
613
614#[derive(Debug, Clone, Hash, Eq, PartialEq)]
621pub enum AttributeAccess {
622 EntityLUB(EntityLUB, Vec<SmolStr>),
625 Context(EntityUID, Vec<SmolStr>),
629 Other(Vec<SmolStr>),
633}
634
635impl AttributeAccess {
636 pub(crate) fn from_expr(
638 req_env: &RequestEnv<'_>,
639 mut expr: &Expr<Option<Type>>,
640 attr: SmolStr,
641 ) -> AttributeAccess {
642 let mut attrs: Vec<SmolStr> = vec![attr];
643 loop {
644 if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
645 return AttributeAccess::EntityLUB(lub.clone(), attrs);
646 } else if let ExprKind::Var(Var::Context) = expr.expr_kind() {
647 return match req_env.action_entity_uid() {
648 Some(action) => AttributeAccess::Context(action.clone(), attrs),
649 None => AttributeAccess::Other(attrs),
650 };
651 } else if let ExprKind::GetAttr {
652 expr: sub_expr,
653 attr,
654 } = expr.expr_kind()
655 {
656 expr = sub_expr;
657 attrs.push(attr.clone());
658 } else {
659 return AttributeAccess::Other(attrs);
660 }
661 }
662 }
663
664 pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
665 match self {
666 AttributeAccess::EntityLUB(_, attrs) => attrs,
667 AttributeAccess::Context(_, attrs) => attrs,
668 AttributeAccess::Other(attrs) => attrs,
669 }
670 }
671
672 pub(crate) fn suggested_has_guard(&self) -> String {
675 let base_expr = match self {
678 AttributeAccess::Context(_, _) => "context".into(),
679 _ => "e".into(),
680 };
681
682 let (safe_attrs, err_attr) = match self.attrs().split_first() {
683 Some((first, rest)) => (rest, first.clone()),
684 None => (&[] as &[SmolStr], "f".into()),
688 };
689
690 let full_expr = std::iter::once(&base_expr)
691 .chain(safe_attrs.iter().rev())
692 .join(".");
693 format!("{full_expr} has {err_attr}")
694 }
695}
696
697impl Display for AttributeAccess {
698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699 let attrs_str = self.attrs().iter().rev().join(".");
700 match self {
701 AttributeAccess::EntityLUB(lub, _) => write!(
702 f,
703 "`{attrs_str}` on entity type{}",
704 match lub.get_single_entity() {
705 Some(single) => format!(" `{}`", single),
706 _ => format!("s {}", lub.iter().map(|ety| format!("`{ety}`")).join(", ")),
707 },
708 ),
709 AttributeAccess::Context(action, _) => {
710 write!(f, "`{attrs_str}` in context for {action}",)
711 }
712 AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
713 }
714 }
715}
716
717#[cfg(test)]
722mod test_attr_access {
723 use cedar_policy_core::ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var};
724
725 use super::AttributeAccess;
726 use crate::types::{OpenTag, RequestEnv, Type};
727
728 #[allow(clippy::panic)]
730 #[track_caller]
731 fn assert_message_and_help(
732 attr_access: &Expr<Option<Type>>,
733 msg: impl AsRef<str>,
734 help: impl AsRef<str>,
735 ) {
736 let env = RequestEnv::DeclaredAction {
737 principal: &"Principal".parse().unwrap(),
738 action: &EntityUID::with_eid_and_type(
739 cedar_policy_core::ast::ACTION_ENTITY_TYPE,
740 "action",
741 )
742 .unwrap(),
743 resource: &"Resource".parse().unwrap(),
744 context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
745 principal_slot: None,
746 resource_slot: None,
747 };
748
749 let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
750 panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
751 };
752
753 let access = AttributeAccess::from_expr(&env, expr, attr.clone());
754 assert_eq!(
755 access.to_string().as_str(),
756 msg.as_ref(),
757 "Error message did not match expected"
758 );
759 assert_eq!(
760 access.suggested_has_guard().as_str(),
761 help.as_ref(),
762 "Suggested has guard did not match expected"
763 );
764 }
765
766 #[test]
767 fn context_access() {
768 let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
771 assert_message_and_help(
772 &e,
773 "`foo` in context for Action::\"action\"",
774 "context has foo",
775 );
776 let e = ExprBuilder::new().get_attr(e, "bar".into());
777 assert_message_and_help(
778 &e,
779 "`foo.bar` in context for Action::\"action\"",
780 "context.foo has bar",
781 );
782 let e = ExprBuilder::new().get_attr(e, "baz".into());
783 assert_message_and_help(
784 &e,
785 "`foo.bar.baz` in context for Action::\"action\"",
786 "context.foo.bar has baz",
787 );
788 }
789
790 #[test]
791 fn entity_access() {
792 let e = ExprBuilder::new().get_attr(
793 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
794 .val("User::\"alice\"".parse::<EntityUID>().unwrap()),
795 "foo".into(),
796 );
797 assert_message_and_help(&e, "`foo` on entity type `User`", "e has foo");
798 let e = ExprBuilder::new().get_attr(e, "bar".into());
799 assert_message_and_help(&e, "`foo.bar` on entity type `User`", "e.foo has bar");
800 let e = ExprBuilder::new().get_attr(e, "baz".into());
801 assert_message_and_help(
802 &e,
803 "`foo.bar.baz` on entity type `User`",
804 "e.foo.bar has baz",
805 );
806 }
807
808 #[test]
809 fn entity_type_attr_access() {
810 let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
811 .get_attr(
812 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
813 .var(Var::Principal),
814 "thing".into(),
815 );
816 assert_message_and_help(&e, "`thing` on entity type `User`", "e has thing");
817 let e = ExprBuilder::new().get_attr(e, "bar".into());
818 assert_message_and_help(&e, "`bar` on entity type `Thing`", "e has bar");
819 let e = ExprBuilder::new().get_attr(e, "baz".into());
820 assert_message_and_help(&e, "`bar.baz` on entity type `Thing`", "e.bar has baz");
821 }
822
823 #[test]
824 fn other_access() {
825 let e = ExprBuilder::new().get_attr(
826 ExprBuilder::new().ite(
827 ExprBuilder::new().val(true),
828 ExprBuilder::new().record([]).unwrap(),
829 ExprBuilder::new().record([]).unwrap(),
830 ),
831 "foo".into(),
832 );
833 assert_message_and_help(&e, "`foo`", "e has foo");
834 let e = ExprBuilder::new().get_attr(e, "bar".into());
835 assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
836 let e = ExprBuilder::new().get_attr(e, "baz".into());
837 assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
838 }
839}