1#[cfg(test)]
8mod tests;
9
10use crate::{node::relation::RelationComponentContract, prelude::*};
11use std::ops::Not;
12
13#[derive(Clone, Debug, Serialize)]
21pub struct Item {
22 target: ItemTarget,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
25 relation: Option<&'static str>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
28 scale: Option<u32>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
31 max_len: Option<u32>,
32
33 #[serde(skip_serializing_if = "<[_]>::is_empty")]
34 validators: &'static [TypeValidator],
35
36 #[serde(skip_serializing_if = "<[_]>::is_empty")]
37 normalizers: &'static [TypeNormalizer],
38
39 #[serde(skip_serializing_if = "Not::not")]
40 indirect: bool,
41}
42
43impl Item {
44 pub(crate) fn validate_immutable_key_hooks(&self, errs: &mut ErrorTree) {
46 if !self.normalizers().is_empty() {
47 err!(
48 errs,
49 "set items and map keys cannot declare item normalizers; normalize keys before insertion or use a container normalizer"
50 );
51 }
52 }
53
54 #[must_use]
55 pub const fn new(
56 target: ItemTarget,
57 relation: Option<&'static str>,
58 scale: Option<u32>,
59 max_len: Option<u32>,
60 validators: &'static [TypeValidator],
61 normalizers: &'static [TypeNormalizer],
62 indirect: bool,
63 ) -> Self {
64 Self {
65 target,
66 relation,
67 scale,
68 max_len,
69 validators,
70 normalizers,
71 indirect,
72 }
73 }
74
75 #[must_use]
76 pub const fn target(&self) -> &ItemTarget {
77 &self.target
78 }
79
80 #[must_use]
81 pub const fn relation(&self) -> Option<&'static str> {
82 self.relation
83 }
84
85 #[must_use]
86 pub const fn scale(&self) -> Option<u32> {
87 self.scale
88 }
89
90 #[must_use]
91 pub const fn max_len(&self) -> Option<u32> {
92 self.max_len
93 }
94
95 #[must_use]
96 pub const fn validators(&self) -> &'static [TypeValidator] {
97 self.validators
98 }
99
100 #[must_use]
101 pub const fn normalizers(&self) -> &'static [TypeNormalizer] {
102 self.normalizers
103 }
104
105 #[must_use]
106 pub const fn indirect(&self) -> bool {
107 self.indirect
108 }
109
110 #[must_use]
111 pub const fn is_relation(&self) -> bool {
112 self.relation().is_some()
113 }
114}
115
116impl ValidateNode for Item {
117 fn validate(&self) -> Result<(), ErrorTree> {
118 let mut errs = ErrorTree::new();
119 let schema = schema_read();
120
121 match self.target() {
123 ItemTarget::Is(path) => {
124 if schema.check_node_as::<Entity>(path).is_ok() {
126 err!(errs, "a non-relation Item cannot reference an Entity");
127 }
128 }
129
130 ItemTarget::Primitive(_) => {}
131 }
132
133 if let Some(relation) = self.relation() {
135 match schema.cast_node::<Entity>(relation) {
136 Ok(entity) => {
137 if entity.primary_key().fields().len() != 1 {
138 err!(
139 errs,
140 "relation entity '{relation}' uses composite primary key fields {:?}; single-field relation targets require a scalar primary key; use ordered relation tuple metadata for composite targets",
141 entity.primary_key().fields()
142 );
143 } else if let Some(primary_field) = entity.scalar_primary_key_field() {
144 let expected = RelationComponentContract::from_field(primary_field);
145 let actual = RelationComponentContract::from_item(self);
146 if expected.mismatches(actual) {
147 err!(
148 errs,
149 "relation target type mismatch: expected ({:?}, scale={:?}, max_len={:?}), found ({:?}, scale={:?}, max_len={:?})",
150 expected.target(),
151 expected.scale(),
152 expected.max_len(),
153 actual.target(),
154 actual.scale(),
155 actual.max_len(),
156 );
157 }
158 } else {
159 let primary_key_field =
160 entity.primary_key().scalar_field().unwrap_or("<composite>");
161 err!(
162 errs,
163 "relation entity '{relation}' missing primary key field '{0}'",
164 primary_key_field
165 );
166 }
167 }
168 Err(_) => {
169 err!(errs, "relation entity '{relation}' not found");
170 }
171 }
172 }
173
174 errs.result()
175 }
176}
177
178impl VisitableNode for Item {
179 fn drive<V: Visitor>(&self, v: &mut V) {
180 for node in self.validators() {
181 node.accept(v);
182 }
183 }
184}
185
186#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
194pub enum ItemTarget {
195 Is(&'static str),
196 Primitive(Primitive),
197}