icydb_schema/node/
item.rs1#[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 = "Option::is_none")]
34 max_bytes: Option<u32>,
35
36 #[serde(skip_serializing_if = "<[_]>::is_empty")]
37 validators: &'static [TypeValidator],
38
39 #[serde(skip_serializing_if = "<[_]>::is_empty")]
40 sanitizers: &'static [TypeSanitizer],
41
42 #[serde(skip_serializing_if = "Not::not")]
43 indirect: bool,
44}
45
46impl Item {
47 #[must_use]
48 #[expect(
49 clippy::too_many_arguments,
50 reason = "schema item construction keeps generated scalar, relation, and validation metadata explicit"
51 )]
52 pub const fn new(
53 target: ItemTarget,
54 relation: Option<&'static str>,
55 scale: Option<u32>,
56 max_len: Option<u32>,
57 max_bytes: Option<u32>,
58 validators: &'static [TypeValidator],
59 sanitizers: &'static [TypeSanitizer],
60 indirect: bool,
61 ) -> Self {
62 Self {
63 target,
64 relation,
65 scale,
66 max_len,
67 max_bytes,
68 validators,
69 sanitizers,
70 indirect,
71 }
72 }
73
74 #[must_use]
75 pub const fn target(&self) -> &ItemTarget {
76 &self.target
77 }
78
79 #[must_use]
80 pub const fn relation(&self) -> Option<&'static str> {
81 self.relation
82 }
83
84 #[must_use]
85 pub const fn scale(&self) -> Option<u32> {
86 self.scale
87 }
88
89 #[must_use]
90 pub const fn max_len(&self) -> Option<u32> {
91 self.max_len
92 }
93
94 #[must_use]
95 pub const fn max_bytes(&self) -> Option<u32> {
96 self.max_bytes
97 }
98
99 #[must_use]
100 pub const fn validators(&self) -> &'static [TypeValidator] {
101 self.validators
102 }
103
104 #[must_use]
105 pub const fn sanitizers(&self) -> &'static [TypeSanitizer] {
106 self.sanitizers
107 }
108
109 #[must_use]
110 pub const fn indirect(&self) -> bool {
111 self.indirect
112 }
113
114 #[must_use]
115 pub const fn is_relation(&self) -> bool {
116 self.relation().is_some()
117 }
118}
119
120impl ValidateNode for Item {
121 fn validate(&self) -> Result<(), ErrorTree> {
122 let mut errs = ErrorTree::new();
123 let schema = schema_read();
124
125 match self.target() {
127 ItemTarget::Is(path) => {
128 if schema.check_node_as::<Entity>(path).is_ok() {
130 err!(errs, "a non-relation Item cannot reference an Entity");
131 }
132 }
133
134 ItemTarget::Primitive(_) => {}
135 }
136
137 if let Some(relation) = self.relation() {
139 match schema.cast_node::<Entity>(relation) {
140 Ok(entity) => {
141 if entity.primary_key().fields().len() != 1 {
142 err!(
143 errs,
144 "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",
145 entity.primary_key().fields()
146 );
147 } else if let Some(primary_field) = entity.scalar_primary_key_field() {
148 let expected = RelationComponentContract::from_field(primary_field);
149 let actual = RelationComponentContract::from_item(self);
150 if expected.mismatches(actual) {
151 err!(
152 errs,
153 "relation target type mismatch: expected ({:?}, scale={:?}, max_len={:?}, max_bytes={:?}), found ({:?}, scale={:?}, max_len={:?}, max_bytes={:?})",
154 expected.target(),
155 expected.scale(),
156 expected.max_len(),
157 expected.max_bytes(),
158 actual.target(),
159 actual.scale(),
160 actual.max_len(),
161 actual.max_bytes(),
162 );
163 }
164 } else {
165 let primary_key_field =
166 entity.primary_key().scalar_field().unwrap_or("<composite>");
167 err!(
168 errs,
169 "relation entity '{relation}' missing primary key field '{0}'",
170 primary_key_field
171 );
172 }
173 }
174 Err(_) => {
175 err!(errs, "relation entity '{relation}' not found");
176 }
177 }
178 }
179
180 errs.result()
181 }
182}
183
184impl VisitableNode for Item {
185 fn drive<V: Visitor>(&self, v: &mut V) {
186 for node in self.validators() {
187 node.accept(v);
188 }
189 }
190}
191
192#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
200pub enum ItemTarget {
201 Is(&'static str),
202 Primitive(Primitive),
203}