1use crate::ast::*;
18use crate::entities::{
19 err::{EntitiesError, InvalidEntityStructureError},
20 json::err::JsonSerializationError,
21 EntityJson,
22};
23use crate::evaluator::{EvaluationError, RestrictedEvaluator};
24use crate::extensions::Extensions;
25use crate::parser::err::ParseErrors;
26use crate::parser::Loc;
27use crate::transitive_closure::TCNode;
28use crate::FromNormalizedStr;
29use educe::Educe;
30use itertools::Itertools;
31use miette::Diagnostic;
32use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize};
33use smol_str::SmolStr;
34use std::collections::{BTreeMap, HashMap, HashSet};
35use std::str::FromStr;
36use std::sync::Arc;
37use thiserror::Error;
38
39#[cfg(feature = "tolerant-ast")]
40static ERROR_NAME: std::sync::LazyLock<Name> = std::sync::LazyLock::new(|| {
41 Name(InternalName::from(Id::new_unchecked_const(
42 "EntityTypeError",
43 )))
44});
45
46#[cfg(feature = "tolerant-ast")]
47static EID_ERROR_STR: &str = "Eid::Error";
48
49#[cfg(feature = "tolerant-ast")]
50static ENTITY_TYPE_ERROR_STR: &str = "EntityType::Error";
51
52#[cfg(feature = "tolerant-ast")]
53static ENTITY_UID_ERROR_STR: &str = "EntityUID::Error";
54
55pub static ACTION_ENTITY_TYPE: &str = "Action";
57
58#[derive(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
59#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
60pub enum EntityType {
62 EntityType(Name),
64 #[cfg(feature = "tolerant-ast")]
65 ErrorEntityType,
67}
68
69impl<'de> Deserialize<'de> for EntityType {
70 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71 where
72 D: Deserializer<'de>,
73 {
74 let name = Name::deserialize(deserializer)?;
75 Ok(EntityType::EntityType(name))
76 }
77}
78
79impl Serialize for EntityType {
80 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81 where
82 S: Serializer,
83 {
84 match self {
85 EntityType::EntityType(name) => name.serialize(serializer),
86 #[cfg(feature = "tolerant-ast")]
87 EntityType::ErrorEntityType => serializer.serialize_str(ENTITY_TYPE_ERROR_STR),
88 }
89 }
90}
91
92impl EntityType {
93 pub fn is_action(&self) -> bool {
98 match self {
99 EntityType::EntityType(name) => {
100 name.as_ref().basename() == &Id::new_unchecked_const(ACTION_ENTITY_TYPE)
101 }
102 #[cfg(feature = "tolerant-ast")]
103 EntityType::ErrorEntityType => false,
104 }
105 }
106
107 pub fn name(&self) -> &Name {
109 match self {
110 EntityType::EntityType(name) => name,
111 #[cfg(feature = "tolerant-ast")]
112 EntityType::ErrorEntityType => &ERROR_NAME,
113 }
114 }
115
116 pub fn into_name(self) -> Name {
119 match self {
120 EntityType::EntityType(name) => name,
121 #[cfg(feature = "tolerant-ast")]
122 EntityType::ErrorEntityType => ERROR_NAME.clone(),
123 }
124 }
125
126 pub fn loc(&self) -> Option<&Loc> {
128 match self {
129 EntityType::EntityType(name) => name.as_ref().loc(),
130 #[cfg(feature = "tolerant-ast")]
131 EntityType::ErrorEntityType => None,
132 }
133 }
134
135 pub fn with_loc(&self, loc: Option<&Loc>) -> Self {
137 match self {
138 EntityType::EntityType(name) => EntityType::EntityType(Name(InternalName {
139 id: name.0.id.clone(),
140 path: name.0.path.clone(),
141 loc: loc.cloned(),
142 })),
143 #[cfg(feature = "tolerant-ast")]
144 EntityType::ErrorEntityType => self.clone(),
145 }
146 }
147
148 pub fn qualify_with(&self, namespace: Option<&Name>) -> Self {
150 match self {
151 EntityType::EntityType(name) => Self::EntityType(name.qualify_with_name(namespace)),
152 #[cfg(feature = "tolerant-ast")]
153 EntityType::ErrorEntityType => Self::ErrorEntityType,
154 }
155 }
156
157 pub fn from_normalized_str(src: &str) -> Result<Self, ParseErrors> {
159 Name::from_normalized_str(src).map(Into::into)
160 }
161}
162
163impl From<Name> for EntityType {
164 fn from(n: Name) -> Self {
165 Self::EntityType(n)
166 }
167}
168
169impl From<EntityType> for Name {
170 fn from(ty: EntityType) -> Name {
171 match ty {
172 EntityType::EntityType(name) => name,
173 #[cfg(feature = "tolerant-ast")]
174 EntityType::ErrorEntityType => ERROR_NAME.clone(),
175 }
176 }
177}
178
179impl AsRef<Name> for EntityType {
180 fn as_ref(&self) -> &Name {
181 match self {
182 EntityType::EntityType(name) => name,
183 #[cfg(feature = "tolerant-ast")]
184 EntityType::ErrorEntityType => &ERROR_NAME,
185 }
186 }
187}
188
189impl FromStr for EntityType {
190 type Err = ParseErrors;
191
192 fn from_str(s: &str) -> Result<Self, Self::Err> {
193 s.parse().map(Self::EntityType)
194 }
195}
196
197impl std::fmt::Display for EntityType {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 EntityType::EntityType(name) => write!(f, "{name}"),
201 #[cfg(feature = "tolerant-ast")]
202 EntityType::ErrorEntityType => write!(f, "{ENTITY_TYPE_ERROR_STR}"),
203 }
204 }
205}
206
207#[derive(Educe, Serialize, Deserialize, Debug, Clone)]
209#[serde(rename = "EntityUID")]
210#[educe(PartialEq, Eq, Hash, PartialOrd, Ord)]
211pub struct EntityUIDImpl {
212 ty: EntityType,
214 eid: Eid,
216 #[serde(skip)]
218 #[educe(PartialEq(ignore))]
219 #[educe(Hash(ignore))]
220 #[educe(PartialOrd(ignore))]
221 loc: Option<Loc>,
222}
223
224impl EntityUIDImpl {
225 pub fn loc(&self) -> Option<Loc> {
227 self.loc.clone()
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
233pub enum EntityUID {
234 EntityUID(EntityUIDImpl),
236 #[cfg(feature = "tolerant-ast")]
237 Error,
239}
240
241impl<'de> Deserialize<'de> for EntityUID {
242 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
243 where
244 D: Deserializer<'de>,
245 {
246 let uid_impl = EntityUIDImpl::deserialize(deserializer)?;
247 Ok(EntityUID::EntityUID(uid_impl))
248 }
249}
250
251impl Serialize for EntityUID {
252 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253 where
254 S: Serializer,
255 {
256 match self {
257 EntityUID::EntityUID(uid_impl) => uid_impl.serialize(serializer),
258 #[cfg(feature = "tolerant-ast")]
259 EntityUID::Error => serializer.serialize_str(ENTITY_UID_ERROR_STR),
260 }
261 }
262}
263
264impl StaticallyTyped for EntityUID {
265 fn type_of(&self) -> Type {
266 match self {
267 EntityUID::EntityUID(entity_uid) => Type::Entity {
268 ty: entity_uid.ty.clone(),
269 },
270 #[cfg(feature = "tolerant-ast")]
271 EntityUID::Error => Type::Entity {
272 ty: EntityType::ErrorEntityType,
273 },
274 }
275 }
276}
277
278#[cfg(test)]
279impl EntityUID {
280 pub(crate) fn with_eid(eid: &str) -> Self {
283 Self::EntityUID(EntityUIDImpl {
284 ty: Self::test_entity_type(),
285 eid: Eid::Eid(eid.into()),
286 loc: None,
287 })
288 }
289
290 pub(crate) fn test_entity_type() -> EntityType {
292 let name = Name::parse_unqualified_name("test_entity_type")
293 .expect("test_entity_type should be a valid identifier");
294 EntityType::EntityType(name)
295 }
296}
297
298impl EntityUID {
299 pub fn with_eid_and_type(typename: &str, eid: &str) -> Result<Self, ParseErrors> {
301 Ok(Self::EntityUID(EntityUIDImpl {
302 ty: EntityType::EntityType(Name::parse_unqualified_name(typename)?),
303 eid: Eid::Eid(eid.into()),
304 loc: None,
305 }))
306 }
307
308 pub fn components(self) -> (EntityType, Eid) {
311 match self {
312 EntityUID::EntityUID(entity_uid) => (entity_uid.ty, entity_uid.eid),
313 #[cfg(feature = "tolerant-ast")]
314 EntityUID::Error => (EntityType::ErrorEntityType, Eid::ErrorEid),
315 }
316 }
317
318 pub fn loc(&self) -> Option<&Loc> {
320 match self {
321 EntityUID::EntityUID(entity_uid) => entity_uid.loc.as_ref(),
322 #[cfg(feature = "tolerant-ast")]
323 EntityUID::Error => None,
324 }
325 }
326
327 pub fn from_components(ty: EntityType, eid: Eid, loc: Option<Loc>) -> Self {
329 Self::EntityUID(EntityUIDImpl { ty, eid, loc })
330 }
331
332 pub fn entity_type(&self) -> &EntityType {
334 match self {
335 EntityUID::EntityUID(entity_uid) => &entity_uid.ty,
336 #[cfg(feature = "tolerant-ast")]
337 EntityUID::Error => &EntityType::ErrorEntityType,
338 }
339 }
340
341 pub fn eid(&self) -> &Eid {
343 match self {
344 EntityUID::EntityUID(entity_uid) => &entity_uid.eid,
345 #[cfg(feature = "tolerant-ast")]
346 EntityUID::Error => &Eid::ErrorEid,
347 }
348 }
349
350 pub fn is_action(&self) -> bool {
352 self.entity_type().is_action()
353 }
354}
355
356impl std::fmt::Display for EntityUID {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(f, "{}::\"{}\"", self.entity_type(), self.eid().escaped())
359 }
360}
361
362impl std::str::FromStr for EntityUID {
364 type Err = ParseErrors;
365
366 fn from_str(s: &str) -> Result<Self, Self::Err> {
367 crate::parser::parse_euid(s)
368 }
369}
370
371impl FromNormalizedStr for EntityUID {
372 fn describe_self() -> &'static str {
373 "Entity UID"
374 }
375}
376
377#[cfg(feature = "arbitrary")]
378impl<'a> arbitrary::Arbitrary<'a> for EntityUID {
379 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
380 Ok(Self::EntityUID(EntityUIDImpl {
381 ty: u.arbitrary()?,
382 eid: u.arbitrary()?,
383 loc: None,
384 }))
385 }
386}
387
388#[derive(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
398pub enum Eid {
399 Eid(SmolStr),
401 #[cfg(feature = "tolerant-ast")]
402 ErrorEid,
404}
405
406impl<'de> Deserialize<'de> for Eid {
407 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
408 where
409 D: Deserializer<'de>,
410 {
411 let value = String::deserialize(deserializer)?;
412 Ok(Eid::Eid(SmolStr::from(value)))
413 }
414}
415
416impl Serialize for Eid {
417 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
418 where
419 S: Serializer,
420 {
421 match self {
422 Eid::Eid(s) => s.serialize(serializer),
423 #[cfg(feature = "tolerant-ast")]
424 Eid::ErrorEid => serializer.serialize_str(EID_ERROR_STR),
425 }
426 }
427}
428
429impl Eid {
430 pub fn new(eid: impl Into<SmolStr>) -> Self {
432 Eid::Eid(eid.into())
433 }
434
435 pub fn escaped(&self) -> SmolStr {
437 match self {
438 Eid::Eid(smol_str) => smol_str.escape_debug().collect(),
439 #[cfg(feature = "tolerant-ast")]
440 Eid::ErrorEid => SmolStr::new_static(EID_ERROR_STR),
441 }
442 }
443
444 pub fn into_smolstr(self) -> SmolStr {
446 match self {
447 Eid::Eid(smol_str) => smol_str,
448 #[cfg(feature = "tolerant-ast")]
449 Eid::ErrorEid => SmolStr::new_static(EID_ERROR_STR),
450 }
451 }
452}
453
454impl AsRef<str> for Eid {
455 fn as_ref(&self) -> &str {
456 match self {
457 Eid::Eid(smol_str) => smol_str,
458 #[cfg(feature = "tolerant-ast")]
459 Eid::ErrorEid => EID_ERROR_STR,
460 }
461 }
462}
463
464#[cfg(feature = "arbitrary")]
465impl<'a> arbitrary::Arbitrary<'a> for Eid {
466 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
467 let x: String = u.arbitrary()?;
468 Ok(Self::Eid(x.into()))
469 }
470}
471
472#[derive(Debug, Clone)]
474pub struct Entity {
475 uid: EntityUID,
477
478 attrs: BTreeMap<SmolStr, PartialValue>,
482
483 indirect_ancestors: HashSet<EntityUID>,
485
486 parents: HashSet<EntityUID>,
492
493 tags: BTreeMap<SmolStr, PartialValue>,
498}
499
500impl std::hash::Hash for Entity {
501 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
502 self.uid.hash(state);
503 }
504}
505
506impl Entity {
507 pub fn new(
512 uid: EntityUID,
513 attrs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
514 indirect_ancestors: HashSet<EntityUID>,
515 parents: HashSet<EntityUID>,
516 tags: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
517 extensions: &Extensions<'_>,
518 ) -> Result<Self, EntityAttrEvaluationError> {
519 let evaluator = RestrictedEvaluator::new(extensions);
520 let evaluate_kvs = |(k, v): (SmolStr, RestrictedExpr), was_attr: bool| {
521 let attr_val = evaluator
522 .partial_interpret(v.as_borrowed())
523 .map_err(|err| EntityAttrEvaluationError {
524 uid: uid.clone(),
525 attr_or_tag: k.clone(),
526 was_attr,
527 err,
528 })?;
529 Ok((k, attr_val))
530 };
531 let evaluated_attrs = attrs
532 .into_iter()
533 .map(|kv| evaluate_kvs(kv, true))
534 .collect::<Result<_, EntityAttrEvaluationError>>()?;
535 let evaluated_tags = tags
536 .into_iter()
537 .map(|kv| evaluate_kvs(kv, false))
538 .collect::<Result<_, EntityAttrEvaluationError>>()?;
539 Ok(Entity {
540 uid,
541 attrs: evaluated_attrs,
542 indirect_ancestors,
543 parents,
544 tags: evaluated_tags,
545 })
546 }
547
548 pub fn new_with_attr_partial_value(
553 uid: EntityUID,
554 attrs: impl IntoIterator<Item = (SmolStr, PartialValue)>,
555 indirect_ancestors: HashSet<EntityUID>,
556 parents: HashSet<EntityUID>,
557 tags: impl IntoIterator<Item = (SmolStr, PartialValue)>,
558 ) -> Self {
559 Self {
560 uid,
561 attrs: attrs.into_iter().collect(),
562 indirect_ancestors,
563 parents,
564 tags: tags.into_iter().collect(),
565 }
566 }
567
568 pub fn uid(&self) -> &EntityUID {
570 &self.uid
571 }
572
573 pub fn get(&self, attr: &str) -> Option<&PartialValue> {
575 self.attrs.get(attr)
576 }
577
578 pub fn get_tag(&self, tag: &str) -> Option<&PartialValue> {
580 self.tags.get(tag)
581 }
582
583 pub fn is_descendant_of(&self, e: &EntityUID) -> bool {
585 self.parents.contains(e) || self.indirect_ancestors.contains(e)
586 }
587
588 pub fn is_indirect_descendant_of(&self, e: &EntityUID) -> bool {
590 self.indirect_ancestors.contains(e)
591 }
592
593 pub fn is_child_of(&self, e: &EntityUID) -> bool {
595 self.parents.contains(e)
596 }
597
598 pub fn ancestors(&self) -> impl Iterator<Item = &EntityUID> {
600 self.parents.iter().chain(self.indirect_ancestors.iter())
601 }
602
603 pub fn indirect_ancestors(&self) -> impl Iterator<Item = &EntityUID> {
605 self.indirect_ancestors.iter()
606 }
607
608 pub fn parents(&self) -> impl Iterator<Item = &EntityUID> {
610 self.parents.iter()
611 }
612
613 pub fn attrs_len(&self) -> usize {
615 self.attrs.len()
616 }
617
618 pub fn tags_len(&self) -> usize {
620 self.tags.len()
621 }
622
623 pub fn keys(&self) -> impl Iterator<Item = &SmolStr> {
625 self.attrs.keys()
626 }
627
628 pub fn tag_keys(&self) -> impl Iterator<Item = &SmolStr> {
630 self.tags.keys()
631 }
632
633 pub fn attrs(&self) -> impl Iterator<Item = (&SmolStr, &PartialValue)> {
635 self.attrs.iter()
636 }
637
638 pub fn tags(&self) -> impl Iterator<Item = (&SmolStr, &PartialValue)> {
640 self.tags.iter()
641 }
642
643 pub fn with_uid(uid: EntityUID) -> Self {
645 Self {
646 uid,
647 attrs: BTreeMap::new(),
648 indirect_ancestors: HashSet::new(),
649 parents: HashSet::new(),
650 tags: BTreeMap::new(),
651 }
652 }
653
654 pub fn deep_eq(&self, other: &Self) -> bool {
660 self.uid == other.uid
661 && self.attrs == other.attrs
662 && self.tags == other.tags
663 && (self.ancestors().collect::<HashSet<_>>())
664 == (other.ancestors().collect::<HashSet<_>>())
665 }
666
667 pub fn add_indirect_ancestor(&mut self, uid: EntityUID) {
674 if !self.parents.contains(&uid) {
675 self.indirect_ancestors.insert(uid);
676 }
677 }
678
679 pub fn add_parent(&mut self, uid: EntityUID) {
685 self.indirect_ancestors.remove(&uid);
686 self.parents.insert(uid);
687 }
688
689 pub fn remove_indirect_ancestor(&mut self, uid: &EntityUID) {
695 self.indirect_ancestors.remove(uid);
696 }
697
698 pub fn remove_parent(&mut self, uid: &EntityUID) {
704 self.parents.remove(uid);
705 }
706
707 pub fn remove_all_indirect_ancestors(&mut self) {
712 self.indirect_ancestors.clear();
713 }
714
715 #[expect(
717 clippy::type_complexity,
718 reason = "needs to return a 5-tuple by design"
719 )]
720 pub fn into_inner(
721 self,
722 ) -> (
723 EntityUID,
724 HashMap<SmolStr, PartialValue>,
725 HashSet<EntityUID>,
726 HashSet<EntityUID>,
727 HashMap<SmolStr, PartialValue>,
728 ) {
729 (
730 self.uid,
731 self.attrs.into_iter().collect(),
732 self.indirect_ancestors,
733 self.parents,
734 self.tags.into_iter().collect(),
735 )
736 }
737
738 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
740 let ejson = EntityJson::from_entity(self)?;
741 serde_json::to_writer_pretty(f, &ejson).map_err(JsonSerializationError::from)?;
742 Ok(())
743 }
744
745 pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
747 let ejson = EntityJson::from_entity(self)?;
748 let v = serde_json::to_value(ejson).map_err(JsonSerializationError::from)?;
749 Ok(v)
750 }
751
752 pub fn to_json_string(&self) -> Result<String, EntitiesError> {
754 let ejson = EntityJson::from_entity(self)?;
755 let string = serde_json::to_string(&ejson).map_err(JsonSerializationError::from)?;
756 Ok(string)
757 }
758
759 pub fn try_validate(self) -> Result<Self, EntitiesError> {
768 self.validate()?;
769 Ok(self)
770 }
771
772 pub fn validate(&self) -> Result<(), EntitiesError> {
774 if self.parents.contains(&self.uid) || self.indirect_ancestors.contains(&self.uid) {
776 return Err(InvalidEntityStructureError::SelfAncestor {
777 uid: self.uid.clone(),
778 }
779 .into());
780 }
781 if let Some(dup) = self.parents.intersection(&self.indirect_ancestors).next() {
783 return Err(InvalidEntityStructureError::DuplicateAncestor {
784 uid: self.uid.clone(),
785 ancestor: dup.clone(),
786 }
787 .into());
788 }
789 if self.uid.is_action() {
791 if let Some(parent) = self
792 .parents
793 .iter()
794 .chain(self.indirect_ancestors.iter())
795 .find(|p| !p.is_action())
796 {
797 return Err(InvalidEntityStructureError::ActionParentIsNotAction {
798 uid: self.uid.clone(),
799 parent: parent.clone(),
800 }
801 .into());
802 }
803 }
804 Ok(())
805 }
806}
807
808impl PartialEq for Entity {
810 fn eq(&self, other: &Self) -> bool {
811 self.uid() == other.uid()
812 }
813}
814
815impl Eq for Entity {}
816
817impl StaticallyTyped for Entity {
818 fn type_of(&self) -> Type {
819 self.uid.type_of()
820 }
821}
822
823impl TCNode<EntityUID> for Entity {
824 fn get_key(&self) -> EntityUID {
825 self.uid().clone()
826 }
827
828 fn add_edge_to(&mut self, k: EntityUID) {
829 self.add_indirect_ancestor(k);
830 }
831
832 fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
833 Box::new(self.ancestors())
834 }
835
836 fn has_edge_to(&self, e: &EntityUID) -> bool {
837 self.is_descendant_of(e)
838 }
839
840 fn reset_edges(&mut self) {
841 self.remove_all_indirect_ancestors()
842 }
843
844 fn direct_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
845 Box::new(self.parents())
846 }
847}
848
849impl TCNode<EntityUID> for Arc<Entity> {
850 fn get_key(&self) -> EntityUID {
851 self.uid().clone()
852 }
853
854 fn add_edge_to(&mut self, k: EntityUID) {
855 Arc::make_mut(self).add_indirect_ancestor(k)
857 }
858
859 fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
860 Box::new(self.ancestors())
861 }
862
863 fn has_edge_to(&self, e: &EntityUID) -> bool {
864 self.is_descendant_of(e)
865 }
866
867 fn reset_edges(&mut self) {
868 Arc::make_mut(self).remove_all_indirect_ancestors()
870 }
871
872 fn direct_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
873 Box::new(self.parents())
874 }
875}
876
877impl std::fmt::Display for Entity {
878 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
879 write!(
880 f,
881 "{}:\n attrs:{}\n ancestors:{}",
882 self.uid,
883 self.attrs
884 .iter()
885 .map(|(k, v)| format!("{k}: {v}"))
886 .join("; "),
887 self.ancestors().join(", ")
888 )
889 }
890}
891
892#[derive(Debug, Diagnostic, Error)]
898#[error("failed to evaluate {} `{attr_or_tag}` of `{uid}`: {err}", if *.was_attr { "attribute" } else { "tag" })]
899pub struct EntityAttrEvaluationError {
900 pub uid: EntityUID,
902 pub attr_or_tag: SmolStr,
904 pub was_attr: bool,
906 #[diagnostic(transparent)]
908 pub err: EvaluationError,
909}
910
911#[cfg(test)]
912mod test {
913 use std::str::FromStr;
914
915 use super::*;
916
917 #[test]
918 fn display() {
919 let e = EntityUID::with_eid("eid");
920 assert_eq!(format!("{e}"), "test_entity_type::\"eid\"");
921 }
922
923 #[test]
924 fn test_euid_equality() {
925 let e1 = EntityUID::with_eid("foo");
926 let e2 = EntityUID::from_components(
927 Name::parse_unqualified_name("test_entity_type")
928 .expect("should be a valid identifier")
929 .into(),
930 Eid::Eid("foo".into()),
931 None,
932 );
933 let e3 = EntityUID::from_components(
934 Name::parse_unqualified_name("Unspecified")
935 .expect("should be a valid identifier")
936 .into(),
937 Eid::Eid("foo".into()),
938 None,
939 );
940
941 assert_eq!(e1, e1);
943 assert_eq!(e2, e2);
944
945 assert_eq!(e1, e2);
947
948 assert!(e1 != e3);
950 }
951
952 #[test]
953 fn action_checker() {
954 let euid = EntityUID::from_str("Action::\"view\"").unwrap();
955 assert!(euid.is_action());
956 let euid = EntityUID::from_str("Foo::Action::\"view\"").unwrap();
957 assert!(euid.is_action());
958 let euid = EntityUID::from_str("Foo::\"view\"").unwrap();
959 assert!(!euid.is_action());
960 let euid = EntityUID::from_str("Action::Foo::\"view\"").unwrap();
961 assert!(!euid.is_action());
962 }
963
964 #[test]
965 fn action_type_is_valid_id() {
966 Id::from_normalized_str(ACTION_ENTITY_TYPE).unwrap();
967 }
968
969 #[cfg(feature = "tolerant-ast")]
970 #[test]
971 fn error_entity() {
972 use cool_asserts::assert_matches;
973
974 let e = EntityUID::Error;
975 assert_matches!(e.eid(), Eid::ErrorEid);
976 assert_matches!(e.entity_type(), EntityType::ErrorEntityType);
977 assert!(!e.is_action());
978 assert_matches!(e.loc(), None);
979
980 let error_eid = Eid::ErrorEid;
981 assert_eq!(error_eid.escaped(), "Eid::Error");
982
983 let error_type = EntityType::ErrorEntityType;
984 assert!(!error_type.is_action());
985 assert_eq!(error_type.qualify_with(None), EntityType::ErrorEntityType);
986 assert_eq!(
987 error_type.qualify_with(Some(&Name(InternalName::from(Id::new_unchecked_const(
988 "EntityTypeError"
989 ))))),
990 EntityType::ErrorEntityType
991 );
992
993 assert_eq!(
994 error_type.name(),
995 &Name(InternalName::from(Id::new_unchecked_const(
996 "EntityTypeError"
997 )))
998 );
999 assert_eq!(error_type.loc(), None)
1000 }
1001
1002 #[test]
1003 fn entity_type_deserialization() {
1004 let json = r#""some_entity_type""#;
1005 let entity_type: EntityType = serde_json::from_str(json).unwrap();
1006 assert_eq!(
1007 entity_type.name().0.to_string(),
1008 "some_entity_type".to_string()
1009 )
1010 }
1011
1012 #[test]
1013 fn entity_type_serialization() {
1014 let entity_type = EntityType::EntityType(Name(InternalName::from(
1015 Id::new_unchecked_const("some_entity_type"),
1016 )));
1017 let serialized = serde_json::to_string(&entity_type).unwrap();
1018
1019 assert_eq!(serialized, r#""some_entity_type""#);
1020 }
1021
1022 #[test]
1023 fn euid_ordering_matches_type_then_eid() {
1024 let euid1 = EntityUID::from_str("AA::\"zzz\"").unwrap();
1025 let euid2 = EntityUID::from_str("B::\"aaa\"").unwrap();
1026 assert!(euid1 < euid2);
1028 let euid3 = EntityUID::from_str("AA::\"aaa\"").unwrap();
1029 assert!(euid3 < euid1);
1031 }
1032}
1033
1034#[cfg(test)]
1035mod validate_test {
1036 use std::str::FromStr;
1037
1038 use super::*;
1039
1040 fn entity_with(
1042 uid: EntityUID,
1043 parents: impl IntoIterator<Item = EntityUID>,
1044 indirect_ancestors: impl IntoIterator<Item = EntityUID>,
1045 ) -> Entity {
1046 Entity::new_with_attr_partial_value(
1047 uid,
1048 std::iter::empty(),
1049 indirect_ancestors.into_iter().collect(),
1050 parents.into_iter().collect(),
1051 std::iter::empty(),
1052 )
1053 }
1054
1055 #[test]
1056 fn self_in_parents_rejected() {
1057 let uid = EntityUID::with_eid("self");
1058 let e = entity_with(uid.clone(), [uid], []);
1059 assert!(e.try_validate().is_err());
1060 }
1061
1062 #[test]
1063 fn self_in_indirect_ancestors_rejected() {
1064 let uid = EntityUID::with_eid("self");
1065 let e = entity_with(uid.clone(), [], [uid]);
1066 assert!(e.try_validate().is_err());
1067 }
1068
1069 #[test]
1070 fn no_self_ancestor_accepted() {
1071 let e = entity_with(EntityUID::with_eid("ok"), [], []);
1072 assert!(e.try_validate().is_ok());
1073 }
1074
1075 #[test]
1076 fn duplicate_ancestor_rejected() {
1077 let uid = EntityUID::with_eid("child");
1078 let ancestor = EntityUID::with_eid("parent");
1079 let e = entity_with(uid, [ancestor.clone()], [ancestor]);
1080 assert!(e.try_validate().is_err());
1081 }
1082
1083 #[test]
1084 fn action_with_non_action_parent_rejected() {
1085 let uid = EntityUID::from_str("Action::\"view\"").unwrap();
1086 let parent = EntityUID::from_str("User::\"alice\"").unwrap();
1087 let e = entity_with(uid, [parent], []);
1088 assert!(e.try_validate().is_err());
1089 }
1090
1091 #[test]
1092 fn action_with_action_parent_accepted() {
1093 let uid = EntityUID::from_str("Action::\"view\"").unwrap();
1094 let parent = EntityUID::from_str("Action::\"read\"").unwrap();
1095 let e = entity_with(uid, [parent], []);
1096 assert!(e.try_validate().is_ok());
1097 }
1098
1099 #[test]
1100 fn non_action_with_action_parent_accepted() {
1101 let uid = EntityUID::from_str("User::\"alice\"").unwrap();
1102 let parent = EntityUID::from_str("Action::\"read\"").unwrap();
1103 let e = entity_with(uid, [parent], []);
1104 assert!(e.try_validate().is_ok());
1105 }
1106}