1use cedar_policy_core::entities::conformance::err::InvalidEnumEntityError;
20use miette::Diagnostic;
21use thiserror::Error;
22
23use std::fmt::Display;
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::level_validate::EntityDerefLevel;
35use crate::types::{EntityLUB, EntityRecordKind, RequestEnv, Type};
36use crate::ValidatorSchema;
37use itertools::Itertools;
38use smol_str::SmolStr;
39
40#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
42#[error("for policy `{policy_id}`, unrecognized entity type `{actual_entity_type}`")]
44pub struct UnrecognizedEntityType {
45 pub source_loc: Option<Loc>,
47 pub policy_id: PolicyID,
49 pub actual_entity_type: String,
51 pub suggested_entity_type: Option<String>,
54}
55
56impl Diagnostic for UnrecognizedEntityType {
57 impl_diagnostic_from_source_loc_opt_field!(source_loc);
58
59 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
60 match &self.suggested_entity_type {
61 Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
62 None => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
69#[error("for policy `{policy_id}`, unrecognized action `{actual_action_id}`")]
70pub struct UnrecognizedActionId {
71 pub source_loc: Option<Loc>,
73 pub policy_id: PolicyID,
75 pub actual_action_id: String,
77 pub hint: Option<UnrecognizedActionIdHelp>,
79}
80
81impl Diagnostic for UnrecognizedActionId {
82 impl_diagnostic_from_source_loc_opt_field!(source_loc);
83
84 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
85 self.hint
86 .as_ref()
87 .map(|help| Box::new(help) as Box<dyn std::fmt::Display>)
88 }
89}
90
91#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
93pub enum UnrecognizedActionIdHelp {
94 #[error("did you intend to include the type in action `{0}`?")]
96 AvoidActionTypeInActionId(String),
97 #[error("did you mean `{0}`?")]
99 SuggestAlternative(String),
100}
101
102pub fn unrecognized_action_id_help(
104 euid: &EntityUID,
105 schema: &ValidatorSchema,
106) -> Option<UnrecognizedActionIdHelp> {
107 let eid_str: &str = euid.eid().as_ref();
109 let eid_with_type = format!("Action::{}", eid_str);
110 let eid_with_type_and_quotes = format!("Action::\"{}\"", eid_str);
111 let maybe_id_with_type = schema.action_ids().find(|action_id| {
112 let eid = <Eid as AsRef<str>>::as_ref(action_id.name().eid());
113 eid.contains(&eid_with_type) || eid.contains(&eid_with_type_and_quotes)
114 });
115 if let Some(id) = maybe_id_with_type {
116 Some(UnrecognizedActionIdHelp::AvoidActionTypeInActionId(
118 id.name().to_string(),
119 ))
120 } else {
121 let euids_strs = schema
123 .action_ids()
124 .map(|id| id.name().to_string())
125 .collect::<Vec<_>>();
126 fuzzy_search(euid.eid().as_ref(), &euids_strs)
127 .map(UnrecognizedActionIdHelp::SuggestAlternative)
128 }
129}
130
131#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
133#[error("for policy `{policy_id}`, unable to find an applicable action given the policy scope constraints")]
134pub struct InvalidActionApplication {
135 pub source_loc: Option<Loc>,
137 pub policy_id: PolicyID,
139 pub would_in_fix_principal: bool,
141 pub would_in_fix_resource: bool,
143}
144
145impl Diagnostic for InvalidActionApplication {
146 impl_diagnostic_from_source_loc_opt_field!(source_loc);
147
148 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
149 match (self.would_in_fix_principal, self.would_in_fix_resource) {
150 (true, false) => Some(Box::new(
151 "try replacing `==` with `in` in the principal clause",
152 )),
153 (false, true) => Some(Box::new(
154 "try replacing `==` with `in` in the resource clause",
155 )),
156 (true, true) => Some(Box::new(
157 "try replacing `==` with `in` in the principal clause and the resource clause",
158 )),
159 (false, false) => None,
160 }
161 }
162}
163
164#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
166#[error("for policy `{policy_id}`, unexpected type: expected {} but saw {}",
167 match .expected.iter().next() {
168 Some(single) if .expected.len() == 1 => format!("{}", single),
169 _ => .expected.iter().join(", or ")
170 },
171 .actual)]
172pub struct UnexpectedType {
173 pub source_loc: Option<Loc>,
175 pub policy_id: PolicyID,
177 pub expected: Vec<Type>,
179 pub actual: Type,
181 pub help: Option<UnexpectedTypeHelp>,
183}
184
185impl Diagnostic for UnexpectedType {
186 impl_diagnostic_from_source_loc_opt_field!(source_loc);
187
188 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
189 self.help.as_ref().map(|h| Box::new(h) as Box<dyn Display>)
190 }
191}
192
193#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
195pub enum UnexpectedTypeHelp {
196 #[error("try using `like` to examine the contents of a string")]
198 TryUsingLike,
199 #[error(
201 "try using `contains`, `containsAny`, or `containsAll` to examine the contents of a set"
202 )]
203 TryUsingContains,
204 #[error("try using `contains` to test if a single element is in a set")]
206 TryUsingSingleContains,
207 #[error("try using `has` to test for an attribute")]
209 TryUsingHas,
210 #[error("try using `is` to test for an entity type")]
212 TryUsingIs,
213 #[error("try using `in` for entity hierarchy membership")]
215 TryUsingIn,
216 #[error(r#"try using `== ""` to test if a string is empty"#)]
218 TryUsingEqEmptyString,
219 #[error("Cedar only supports run time type tests for entities")]
221 TypeTestNotSupported,
222 #[error("Cedar does not support string concatenation")]
224 ConcatenationNotSupported,
225 #[error("Cedar does not support computing the union, intersection, or difference of sets")]
227 SetOperationsNotSupported,
228}
229
230#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
232pub struct IncompatibleTypes {
233 pub source_loc: Option<Loc>,
235 pub policy_id: PolicyID,
237 pub types: BTreeSet<Type>,
239 pub hint: LubHelp,
241 pub context: LubContext,
243}
244
245impl Diagnostic for IncompatibleTypes {
246 impl_diagnostic_from_source_loc_opt_field!(source_loc);
247
248 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
249 Some(Box::new(format!(
250 "for policy `{}`, {} must have compatible types. {}",
251 self.policy_id, self.context, self.hint
252 )))
253 }
254}
255
256impl Display for IncompatibleTypes {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 write!(f, "the types ")?;
259 join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
260 write!(f, " are not compatible")
261 }
262}
263
264#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
266pub enum LubHelp {
267 #[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
269 AttributeQualifier,
270 #[error("Compatible record types must have exactly the same attributes")]
272 RecordWidth,
273 #[error("Different entity types are never compatible even when their attributes would be compatible")]
275 EntityType,
276 #[error("Entity and record types are never compatible even when their attributes would be compatible")]
278 EntityRecord,
279 #[error("Types must be exactly equal to be compatible")]
281 None,
282}
283
284#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
286pub enum LubContext {
287 #[error("elements of a set")]
289 Set,
290 #[error("both branches of a conditional")]
292 Conditional,
293 #[error("both operands to a `==` expression")]
295 Equality,
296 #[error("elements of the first operand and the second operand to a `contains` expression")]
298 Contains,
299 #[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
301 ContainsAnyAll,
302 #[error("tag types for a `.getTag()` operation")]
304 GetTag,
305}
306
307#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
309#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
310pub struct UnsafeAttributeAccess {
311 pub source_loc: Option<Loc>,
313 pub policy_id: PolicyID,
315 pub attribute_access: AttributeAccess,
317 pub suggestion: Option<String>,
319 pub may_exist: bool,
322}
323
324impl Diagnostic for UnsafeAttributeAccess {
325 impl_diagnostic_from_source_loc_opt_field!(source_loc);
326
327 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
328 match (&self.suggestion, self.may_exist) {
329 (Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
330 (None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
331 (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)"))),
332 (None, false) => None,
333 }
334 }
335}
336
337#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
339#[error("for policy `{policy_id}`, unable to guarantee safety of access to optional attribute {attribute_access}")]
340pub struct UnsafeOptionalAttributeAccess {
341 pub source_loc: Option<Loc>,
343 pub policy_id: PolicyID,
345 pub attribute_access: AttributeAccess,
347}
348
349impl Diagnostic for UnsafeOptionalAttributeAccess {
350 impl_diagnostic_from_source_loc_opt_field!(source_loc);
351
352 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
353 Some(Box::new(format!(
354 "try testing for the attribute's presence with `{} && ..`",
355 self.attribute_access.suggested_has_guard()
356 )))
357 }
358}
359
360#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
362#[error(
363 "for policy `{policy_id}`, unable to guarantee safety of access to tag `{tag}`{}",
364 match .entity_ty.as_ref().and_then(|lub| lub.get_single_entity()) {
365 Some(ety) => format!(" on entity type `{ety}`"),
366 None => "".to_string()
367 }
368)]
369pub struct UnsafeTagAccess {
370 pub source_loc: Option<Loc>,
372 pub policy_id: PolicyID,
374 pub entity_ty: Option<EntityLUB>,
376 pub tag: Expr<Option<Type>>,
378}
379
380impl Diagnostic for UnsafeTagAccess {
381 impl_diagnostic_from_source_loc_opt_field!(source_loc);
382
383 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
384 Some(Box::new(format!(
385 "try testing for the tag's presence with `.hasTag({}) && ..`",
386 &self.tag
387 )))
388 }
389}
390
391#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
393#[error(
394 "for policy `{policy_id}`, `.getTag()` is not allowed on entities of {} because no `tags` were declared on the entity type in the schema",
395 match .entity_ty.as_ref() {
396 Some(ty) => format!("type `{ty}`"),
397 None => "this type".to_string(),
398 }
399)]
400pub struct NoTagsAllowed {
401 pub source_loc: Option<Loc>,
403 pub policy_id: PolicyID,
405 pub entity_ty: Option<EntityType>,
409}
410
411impl Diagnostic for NoTagsAllowed {
412 impl_diagnostic_from_source_loc_opt_field!(source_loc);
413}
414
415#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
417#[error("for policy `{policy_id}`, undefined extension function: {name}")]
418pub struct UndefinedFunction {
419 pub source_loc: Option<Loc>,
421 pub policy_id: PolicyID,
423 pub name: String,
425}
426
427impl Diagnostic for UndefinedFunction {
428 impl_diagnostic_from_source_loc_opt_field!(source_loc);
429}
430
431#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
433#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
434pub struct WrongNumberArguments {
435 pub source_loc: Option<Loc>,
437 pub policy_id: PolicyID,
439 pub expected: usize,
441 pub actual: usize,
443}
444
445impl Diagnostic for WrongNumberArguments {
446 impl_diagnostic_from_source_loc_opt_field!(source_loc);
447}
448
449#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
451#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
452pub struct FunctionArgumentValidation {
453 pub source_loc: Option<Loc>,
455 pub policy_id: PolicyID,
457 pub msg: String,
459}
460
461impl Diagnostic for FunctionArgumentValidation {
462 impl_diagnostic_from_source_loc_opt_field!(source_loc);
463}
464
465#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
467#[error("Internal invariant violated: `HierarchyNotRespected` error should never occur. Please file an issue")]
468pub struct HierarchyNotRespected {
469 pub source_loc: Option<Loc>,
471 pub policy_id: PolicyID,
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 Some(Box::new("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy that caused this error"))
480 }
481}
482
483#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
485#[error("for policy `{policy_id}`, {violation_kind}")]
486pub struct EntityDerefLevelViolation {
487 pub source_loc: Option<Loc>,
489 pub policy_id: PolicyID,
491 pub violation_kind: EntityDerefViolationKind,
493}
494
495#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
497pub enum EntityDerefViolationKind {
498 #[error(
500 "this policy requires level {actual_level}, which exceeds the maximum allowed level ({allowed_level})"
501 )]
502 MaximumLevelExceeded {
503 allowed_level: EntityDerefLevel,
505 actual_level: EntityDerefLevel,
507 },
508 #[error("entity literals cannot be dereferenced at any level")]
510 LiteralDerefTarget,
511}
512
513impl Diagnostic for EntityDerefLevelViolation {
514 impl_diagnostic_from_source_loc_opt_field!(source_loc);
515}
516
517#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
519#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
520pub struct EmptySetForbidden {
521 pub source_loc: Option<Loc>,
523 pub policy_id: PolicyID,
525}
526
527impl Diagnostic for EmptySetForbidden {
528 impl_diagnostic_from_source_loc_opt_field!(source_loc);
529}
530
531#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
534#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
535pub struct NonLitExtConstructor {
536 pub source_loc: Option<Loc>,
538 pub policy_id: PolicyID,
540}
541
542impl Diagnostic for NonLitExtConstructor {
543 impl_diagnostic_from_source_loc_opt_field!(source_loc);
544
545 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
546 Some(Box::new(
547 "consider applying extension constructors inside attribute values when constructing entity or context data"
548 ))
549 }
550}
551
552#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
555#[error("internal invariant violated")]
556pub struct InternalInvariantViolation {
557 pub source_loc: Option<Loc>,
559 pub policy_id: PolicyID,
561}
562
563impl Diagnostic for InternalInvariantViolation {
564 impl_diagnostic_from_source_loc_opt_field!(source_loc);
565
566 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
567 Some(Box::new(
568 "please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy for which you observed the issue"
569 ))
570 }
571}
572
573#[derive(Debug, Clone, Hash, Eq, PartialEq)]
580pub enum AttributeAccess {
581 EntityLUB(EntityLUB, Vec<SmolStr>),
584 Context(EntityUID, Vec<SmolStr>),
588 Other(Vec<SmolStr>),
592}
593
594impl AttributeAccess {
595 pub(crate) fn from_expr(
597 req_env: &RequestEnv<'_>,
598 mut expr: &Expr<Option<Type>>,
599 attr: SmolStr,
600 ) -> AttributeAccess {
601 let mut attrs: Vec<SmolStr> = vec![attr];
602 loop {
603 if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
604 return AttributeAccess::EntityLUB(lub.clone(), attrs);
605 } else if matches!(expr.expr_kind(), ExprKind::Var(Var::Context)) {
606 return match req_env.action_entity_uid() {
607 Some(action) => AttributeAccess::Context(action.clone(), attrs),
608 None => AttributeAccess::Other(attrs),
609 };
610 } else if let ExprKind::GetAttr {
611 expr: sub_expr,
612 attr,
613 } = expr.expr_kind()
614 {
615 expr = sub_expr;
616 attrs.push(attr.clone());
617 } else {
618 return AttributeAccess::Other(attrs);
619 }
620 }
621 }
622
623 pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
624 match self {
625 AttributeAccess::EntityLUB(_, attrs) => attrs,
626 AttributeAccess::Context(_, attrs) => attrs,
627 AttributeAccess::Other(attrs) => attrs,
628 }
629 }
630
631 pub(crate) fn suggested_has_guard(&self) -> String {
634 let base_expr = match self {
637 AttributeAccess::Context(_, _) => "context".into(),
638 _ => "e".into(),
639 };
640
641 let (safe_attrs, err_attr) = match self.attrs().split_first() {
642 Some((first, rest)) => (rest, first.clone()),
643 None => (&[] as &[SmolStr], "f".into()),
647 };
648
649 let full_expr = std::iter::once(&base_expr)
650 .chain(safe_attrs.iter().rev())
651 .join(".");
652 format!("{full_expr} has {err_attr}")
653 }
654}
655
656impl Display for AttributeAccess {
657 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658 let attrs_str = self.attrs().iter().rev().join(".");
659 match self {
660 AttributeAccess::EntityLUB(lub, _) => write!(
661 f,
662 "`{attrs_str}` on entity type{}",
663 match lub.get_single_entity() {
664 Some(single) => format!(" `{}`", single),
665 _ => format!("s {}", lub.iter().map(|ety| format!("`{ety}`")).join(", ")),
666 },
667 ),
668 AttributeAccess::Context(action, _) => {
669 write!(f, "`{attrs_str}` in context for {action}",)
670 }
671 AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
672 }
673 }
674}
675
676#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
679#[error("for policy `{policy_id}`: {err}")]
680pub struct InvalidEnumEntity {
681 pub source_loc: Option<Loc>,
683 pub policy_id: PolicyID,
685 pub err: InvalidEnumEntityError,
687}
688
689impl Diagnostic for InvalidEnumEntity {
690 impl_diagnostic_from_source_loc_opt_field!(source_loc);
691
692 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
693 self.err.help()
694 }
695}
696
697#[cfg(test)]
702mod test_attr_access {
703 use cedar_policy_core::{
704 ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var},
705 expr_builder::ExprBuilder as _,
706 };
707
708 use super::AttributeAccess;
709 use crate::types::{OpenTag, RequestEnv, Type};
710
711 #[allow(clippy::panic)]
713 #[track_caller]
714 fn assert_message_and_help(
715 attr_access: &Expr<Option<Type>>,
716 msg: impl AsRef<str>,
717 help: impl AsRef<str>,
718 ) {
719 let env = RequestEnv::DeclaredAction {
720 principal: &"Principal".parse().unwrap(),
721 action: &EntityUID::with_eid_and_type(
722 cedar_policy_core::ast::ACTION_ENTITY_TYPE,
723 "action",
724 )
725 .unwrap(),
726 resource: &"Resource".parse().unwrap(),
727 context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
728 principal_slot: None,
729 resource_slot: None,
730 };
731
732 let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
733 panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
734 };
735
736 let access = AttributeAccess::from_expr(&env, expr, attr.clone());
737 assert_eq!(
738 access.to_string().as_str(),
739 msg.as_ref(),
740 "Error message did not match expected"
741 );
742 assert_eq!(
743 access.suggested_has_guard().as_str(),
744 help.as_ref(),
745 "Suggested has guard did not match expected"
746 );
747 }
748
749 #[test]
750 fn context_access() {
751 let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
754 assert_message_and_help(
755 &e,
756 "`foo` in context for Action::\"action\"",
757 "context has foo",
758 );
759 let e = ExprBuilder::new().get_attr(e, "bar".into());
760 assert_message_and_help(
761 &e,
762 "`foo.bar` in context for Action::\"action\"",
763 "context.foo has bar",
764 );
765 let e = ExprBuilder::new().get_attr(e, "baz".into());
766 assert_message_and_help(
767 &e,
768 "`foo.bar.baz` in context for Action::\"action\"",
769 "context.foo.bar has baz",
770 );
771 }
772
773 #[test]
774 fn entity_access() {
775 let e = ExprBuilder::new().get_attr(
776 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
777 .val("User::\"alice\"".parse::<EntityUID>().unwrap()),
778 "foo".into(),
779 );
780 assert_message_and_help(&e, "`foo` on entity type `User`", "e has foo");
781 let e = ExprBuilder::new().get_attr(e, "bar".into());
782 assert_message_and_help(&e, "`foo.bar` on entity type `User`", "e.foo has bar");
783 let e = ExprBuilder::new().get_attr(e, "baz".into());
784 assert_message_and_help(
785 &e,
786 "`foo.bar.baz` on entity type `User`",
787 "e.foo.bar has baz",
788 );
789 }
790
791 #[test]
792 fn entity_type_attr_access() {
793 let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
794 .get_attr(
795 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
796 .var(Var::Principal),
797 "thing".into(),
798 );
799 assert_message_and_help(&e, "`thing` on entity type `User`", "e has thing");
800 let e = ExprBuilder::new().get_attr(e, "bar".into());
801 assert_message_and_help(&e, "`bar` on entity type `Thing`", "e has bar");
802 let e = ExprBuilder::new().get_attr(e, "baz".into());
803 assert_message_and_help(&e, "`bar.baz` on entity type `Thing`", "e.bar has baz");
804 }
805
806 #[test]
807 fn other_access() {
808 let e = ExprBuilder::new().get_attr(
809 ExprBuilder::new().ite(
810 ExprBuilder::new().val(true),
811 ExprBuilder::new().record([]).unwrap(),
812 ExprBuilder::new().record([]).unwrap(),
813 ),
814 "foo".into(),
815 );
816 assert_message_and_help(&e, "`foo`", "e has foo");
817 let e = ExprBuilder::new().get_attr(e, "bar".into());
818 assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
819 let e = ExprBuilder::new().get_attr(e, "baz".into());
820 assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
821 }
822}