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.action_ids().find(|action_id| {
111 let eid = <Eid as AsRef<str>>::as_ref(action_id.name().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.name().to_string(),
118 ))
119 } else {
120 let euids_strs = schema
122 .action_ids()
123 .map(|id| id.name().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: Vec<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(r#"try using `== ""` to test if a string is empty"#)]
217 TryUsingEqEmptyString,
218 #[error("Cedar only supports run time type tests for entities")]
220 TypeTestNotSupported,
221 #[error("Cedar does not support string concatenation")]
223 ConcatenationNotSupported,
224 #[error("Cedar does not support computing the union, intersection, or difference of sets")]
226 SetOperationsNotSupported,
227}
228
229#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
231pub struct IncompatibleTypes {
232 pub source_loc: Option<Loc>,
234 pub policy_id: PolicyID,
236 pub types: BTreeSet<Type>,
238 pub hint: LubHelp,
240 pub context: LubContext,
242}
243
244impl Diagnostic for IncompatibleTypes {
245 impl_diagnostic_from_source_loc_opt_field!(source_loc);
246
247 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
248 Some(Box::new(format!(
249 "for policy `{}`, {} must have compatible types. {}",
250 self.policy_id, self.context, self.hint
251 )))
252 }
253}
254
255impl Display for IncompatibleTypes {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 write!(f, "the types ")?;
258 join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
259 write!(f, " are not compatible")
260 }
261}
262
263#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
265pub enum LubHelp {
266 #[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
268 AttributeQualifier,
269 #[error("Compatible record types must have exactly the same attributes")]
271 RecordWidth,
272 #[error("Different entity types are never compatible even when their attributes would be compatible")]
274 EntityType,
275 #[error("Entity and record types are never compatible even when their attributes would be compatible")]
277 EntityRecord,
278 #[error("Types must be exactly equal to be compatible")]
280 None,
281}
282
283#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
285pub enum LubContext {
286 #[error("elements of a set")]
288 Set,
289 #[error("both branches of a conditional")]
291 Conditional,
292 #[error("both operands to a `==` expression")]
294 Equality,
295 #[error("elements of the first operand and the second operand to a `contains` expression")]
297 Contains,
298 #[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
300 ContainsAnyAll,
301 #[error("tag types for a `.getTag()` operation")]
303 GetTag,
304}
305
306#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
308#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
309pub struct UnsafeAttributeAccess {
310 pub source_loc: Option<Loc>,
312 pub policy_id: PolicyID,
314 pub attribute_access: AttributeAccess,
316 pub suggestion: Option<String>,
318 pub may_exist: bool,
321}
322
323impl Diagnostic for UnsafeAttributeAccess {
324 impl_diagnostic_from_source_loc_opt_field!(source_loc);
325
326 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
327 match (&self.suggestion, self.may_exist) {
328 (Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
329 (None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
330 (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)"))),
331 (None, false) => None,
332 }
333 }
334}
335
336#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
338#[error("for policy `{policy_id}`, unable to guarantee safety of access to optional attribute {attribute_access}")]
339pub struct UnsafeOptionalAttributeAccess {
340 pub source_loc: Option<Loc>,
342 pub policy_id: PolicyID,
344 pub attribute_access: AttributeAccess,
346}
347
348impl Diagnostic for UnsafeOptionalAttributeAccess {
349 impl_diagnostic_from_source_loc_opt_field!(source_loc);
350
351 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
352 Some(Box::new(format!(
353 "try testing for the attribute's presence with `{} && ..`",
354 self.attribute_access.suggested_has_guard()
355 )))
356 }
357}
358
359#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
361#[error(
362 "for policy `{policy_id}`, unable to guarantee safety of access to tag `{tag}`{}",
363 match .entity_ty.as_ref().and_then(|lub| lub.get_single_entity()) {
364 Some(ety) => format!(" on entity type `{ety}`"),
365 None => "".to_string()
366 }
367)]
368pub struct UnsafeTagAccess {
369 pub source_loc: Option<Loc>,
371 pub policy_id: PolicyID,
373 pub entity_ty: Option<EntityLUB>,
375 pub tag: Expr<Option<Type>>,
377}
378
379impl Diagnostic for UnsafeTagAccess {
380 impl_diagnostic_from_source_loc_opt_field!(source_loc);
381
382 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
383 Some(Box::new(format!(
384 "try testing for the tag's presence with `.hasTag({}) && ..`",
385 &self.tag
386 )))
387 }
388}
389
390#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
392#[error(
393 "for policy `{policy_id}`, `.getTag()` is not allowed on entities of {} because no `tags` were declared on the entity type in the schema",
394 match .entity_ty.as_ref() {
395 Some(ty) => format!("type `{ty}`"),
396 None => "this type".to_string(),
397 }
398)]
399pub struct NoTagsAllowed {
400 pub source_loc: Option<Loc>,
402 pub policy_id: PolicyID,
404 pub entity_ty: Option<EntityType>,
408}
409
410impl Diagnostic for NoTagsAllowed {
411 impl_diagnostic_from_source_loc_opt_field!(source_loc);
412}
413
414#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
416#[error("for policy `{policy_id}`, undefined extension function: {name}")]
417pub struct UndefinedFunction {
418 pub source_loc: Option<Loc>,
420 pub policy_id: PolicyID,
422 pub name: String,
424}
425
426impl Diagnostic for UndefinedFunction {
427 impl_diagnostic_from_source_loc_opt_field!(source_loc);
428}
429
430#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
432#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
433pub struct WrongNumberArguments {
434 pub source_loc: Option<Loc>,
436 pub policy_id: PolicyID,
438 pub expected: usize,
440 pub actual: usize,
442}
443
444impl Diagnostic for WrongNumberArguments {
445 impl_diagnostic_from_source_loc_opt_field!(source_loc);
446}
447
448#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
450#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
451pub struct FunctionArgumentValidation {
452 pub source_loc: Option<Loc>,
454 pub policy_id: PolicyID,
456 pub msg: String,
458}
459
460impl Diagnostic for FunctionArgumentValidation {
461 impl_diagnostic_from_source_loc_opt_field!(source_loc);
462}
463
464#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
466#[error("for policy `{policy_id}`, operands to `in` do not respect the entity hierarchy")]
467pub struct HierarchyNotRespected {
468 pub source_loc: Option<Loc>,
470 pub policy_id: PolicyID,
472 pub in_lhs: Option<EntityType>,
474 pub in_rhs: Option<EntityType>,
476}
477
478impl Diagnostic for HierarchyNotRespected {
479 impl_diagnostic_from_source_loc_opt_field!(source_loc);
480
481 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
482 match (&self.in_lhs, &self.in_rhs) {
483 (Some(in_lhs), Some(in_rhs)) => Some(Box::new(format!(
484 "`{in_lhs}` cannot be a descendant of `{in_rhs}`"
485 ))),
486 _ => None,
487 }
488 }
489}
490
491#[derive(Default, Debug, Clone, Hash, Eq, PartialEq, Error, Copy, Ord, PartialOrd)]
493pub struct EntityDerefLevel {
494 pub level: i64,
496}
497
498impl Display for EntityDerefLevel {
499 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
500 write!(f, "{}", self.level)
501 }
502}
503
504impl From<u32> for EntityDerefLevel {
505 fn from(value: u32) -> Self {
506 EntityDerefLevel {
507 level: value as i64,
508 }
509 }
510}
511
512impl Add for EntityDerefLevel {
513 type Output = Self;
514
515 fn add(self, rhs: Self) -> Self::Output {
516 EntityDerefLevel {
517 level: self.level + rhs.level,
518 }
519 }
520}
521
522impl Neg for EntityDerefLevel {
523 type Output = Self;
524
525 fn neg(self) -> Self::Output {
526 EntityDerefLevel { level: -self.level }
527 }
528}
529
530impl EntityDerefLevel {
531 pub fn decrement(&self) -> Self {
533 EntityDerefLevel {
534 level: self.level - 1,
535 }
536 }
537}
538
539#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
541#[error("for policy `{policy_id}`, the maximum allowed level {allowed_level} is violated. Actual level is {}", (allowed_level.add(actual_level.neg())))]
542pub struct EntityDerefLevelViolation {
543 pub source_loc: Option<Loc>,
545 pub policy_id: PolicyID,
547 pub allowed_level: EntityDerefLevel,
549 pub actual_level: EntityDerefLevel,
551}
552
553impl Diagnostic for EntityDerefLevelViolation {
554 impl_diagnostic_from_source_loc_opt_field!(source_loc);
555
556 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
557 Some(Box::new("Consider increasing the level"))
558 }
559}
560
561#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
563#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
564pub struct EmptySetForbidden {
565 pub source_loc: Option<Loc>,
567 pub policy_id: PolicyID,
569}
570
571impl Diagnostic for EmptySetForbidden {
572 impl_diagnostic_from_source_loc_opt_field!(source_loc);
573}
574
575#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
578#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
579pub struct NonLitExtConstructor {
580 pub source_loc: Option<Loc>,
582 pub policy_id: PolicyID,
584}
585
586impl Diagnostic for NonLitExtConstructor {
587 impl_diagnostic_from_source_loc_opt_field!(source_loc);
588
589 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
590 Some(Box::new(
591 "consider applying extension constructors inside attribute values when constructing entity or context data"
592 ))
593 }
594}
595
596#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
599#[error("internal invariant violated")]
600pub struct InternalInvariantViolation {
601 pub source_loc: Option<Loc>,
603 pub policy_id: PolicyID,
605}
606
607impl Diagnostic for InternalInvariantViolation {
608 impl_diagnostic_from_source_loc_opt_field!(source_loc);
609
610 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
611 Some(Box::new(
612 "please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy for which you observed the issue"
613 ))
614 }
615}
616
617#[derive(Debug, Clone, Hash, Eq, PartialEq)]
624pub enum AttributeAccess {
625 EntityLUB(EntityLUB, Vec<SmolStr>),
628 Context(EntityUID, Vec<SmolStr>),
632 Other(Vec<SmolStr>),
636}
637
638impl AttributeAccess {
639 pub(crate) fn from_expr(
641 req_env: &RequestEnv<'_>,
642 mut expr: &Expr<Option<Type>>,
643 attr: SmolStr,
644 ) -> AttributeAccess {
645 let mut attrs: Vec<SmolStr> = vec![attr];
646 loop {
647 if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
648 return AttributeAccess::EntityLUB(lub.clone(), attrs);
649 } else if matches!(expr.expr_kind(), ExprKind::Var(Var::Context)) {
650 return match req_env.action_entity_uid() {
651 Some(action) => AttributeAccess::Context(action.clone(), attrs),
652 None => AttributeAccess::Other(attrs),
653 };
654 } else if let ExprKind::GetAttr {
655 expr: sub_expr,
656 attr,
657 } = expr.expr_kind()
658 {
659 expr = sub_expr;
660 attrs.push(attr.clone());
661 } else {
662 return AttributeAccess::Other(attrs);
663 }
664 }
665 }
666
667 pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
668 match self {
669 AttributeAccess::EntityLUB(_, attrs) => attrs,
670 AttributeAccess::Context(_, attrs) => attrs,
671 AttributeAccess::Other(attrs) => attrs,
672 }
673 }
674
675 pub(crate) fn suggested_has_guard(&self) -> String {
678 let base_expr = match self {
681 AttributeAccess::Context(_, _) => "context".into(),
682 _ => "e".into(),
683 };
684
685 let (safe_attrs, err_attr) = match self.attrs().split_first() {
686 Some((first, rest)) => (rest, first.clone()),
687 None => (&[] as &[SmolStr], "f".into()),
691 };
692
693 let full_expr = std::iter::once(&base_expr)
694 .chain(safe_attrs.iter().rev())
695 .join(".");
696 format!("{full_expr} has {err_attr}")
697 }
698}
699
700impl Display for AttributeAccess {
701 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
702 let attrs_str = self.attrs().iter().rev().join(".");
703 match self {
704 AttributeAccess::EntityLUB(lub, _) => write!(
705 f,
706 "`{attrs_str}` on entity type{}",
707 match lub.get_single_entity() {
708 Some(single) => format!(" `{}`", single),
709 _ => format!("s {}", lub.iter().map(|ety| format!("`{ety}`")).join(", ")),
710 },
711 ),
712 AttributeAccess::Context(action, _) => {
713 write!(f, "`{attrs_str}` in context for {action}",)
714 }
715 AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
716 }
717 }
718}
719
720#[cfg(test)]
725mod test_attr_access {
726 use cedar_policy_core::{
727 ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var},
728 expr_builder::ExprBuilder as _,
729 };
730
731 use super::AttributeAccess;
732 use crate::types::{OpenTag, RequestEnv, Type};
733
734 #[allow(clippy::panic)]
736 #[track_caller]
737 fn assert_message_and_help(
738 attr_access: &Expr<Option<Type>>,
739 msg: impl AsRef<str>,
740 help: impl AsRef<str>,
741 ) {
742 let env = RequestEnv::DeclaredAction {
743 principal: &"Principal".parse().unwrap(),
744 action: &EntityUID::with_eid_and_type(
745 cedar_policy_core::ast::ACTION_ENTITY_TYPE,
746 "action",
747 )
748 .unwrap(),
749 resource: &"Resource".parse().unwrap(),
750 context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
751 principal_slot: None,
752 resource_slot: None,
753 };
754
755 let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
756 panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
757 };
758
759 let access = AttributeAccess::from_expr(&env, expr, attr.clone());
760 assert_eq!(
761 access.to_string().as_str(),
762 msg.as_ref(),
763 "Error message did not match expected"
764 );
765 assert_eq!(
766 access.suggested_has_guard().as_str(),
767 help.as_ref(),
768 "Suggested has guard did not match expected"
769 );
770 }
771
772 #[test]
773 fn context_access() {
774 let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
777 assert_message_and_help(
778 &e,
779 "`foo` in context for Action::\"action\"",
780 "context has foo",
781 );
782 let e = ExprBuilder::new().get_attr(e, "bar".into());
783 assert_message_and_help(
784 &e,
785 "`foo.bar` in context for Action::\"action\"",
786 "context.foo has bar",
787 );
788 let e = ExprBuilder::new().get_attr(e, "baz".into());
789 assert_message_and_help(
790 &e,
791 "`foo.bar.baz` in context for Action::\"action\"",
792 "context.foo.bar has baz",
793 );
794 }
795
796 #[test]
797 fn entity_access() {
798 let e = ExprBuilder::new().get_attr(
799 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
800 .val("User::\"alice\"".parse::<EntityUID>().unwrap()),
801 "foo".into(),
802 );
803 assert_message_and_help(&e, "`foo` on entity type `User`", "e has foo");
804 let e = ExprBuilder::new().get_attr(e, "bar".into());
805 assert_message_and_help(&e, "`foo.bar` on entity type `User`", "e.foo has bar");
806 let e = ExprBuilder::new().get_attr(e, "baz".into());
807 assert_message_and_help(
808 &e,
809 "`foo.bar.baz` on entity type `User`",
810 "e.foo.bar has baz",
811 );
812 }
813
814 #[test]
815 fn entity_type_attr_access() {
816 let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
817 .get_attr(
818 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
819 .var(Var::Principal),
820 "thing".into(),
821 );
822 assert_message_and_help(&e, "`thing` on entity type `User`", "e has thing");
823 let e = ExprBuilder::new().get_attr(e, "bar".into());
824 assert_message_and_help(&e, "`bar` on entity type `Thing`", "e has bar");
825 let e = ExprBuilder::new().get_attr(e, "baz".into());
826 assert_message_and_help(&e, "`bar.baz` on entity type `Thing`", "e.bar has baz");
827 }
828
829 #[test]
830 fn other_access() {
831 let e = ExprBuilder::new().get_attr(
832 ExprBuilder::new().ite(
833 ExprBuilder::new().val(true),
834 ExprBuilder::new().record([]).unwrap(),
835 ExprBuilder::new().record([]).unwrap(),
836 ),
837 "foo".into(),
838 );
839 assert_message_and_help(&e, "`foo`", "e has foo");
840 let e = ExprBuilder::new().get_attr(e, "bar".into());
841 assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
842 let e = ExprBuilder::new().get_attr(e, "baz".into());
843 assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
844 }
845}