Skip to main content

icydb_schema/node/
item.rs

1use super::relation::RelationComponentContract;
2use crate::prelude::*;
3use std::ops::Not;
4
5///
6/// Item
7///
8/// Canonical schema item descriptor for one scalar, relation, or primitive
9/// field target plus its attached sanitizers and validators.
10///
11
12#[derive(Clone, Debug, Serialize)]
13pub struct Item {
14    target: ItemTarget,
15
16    #[serde(skip_serializing_if = "Option::is_none")]
17    relation: Option<&'static str>,
18
19    #[serde(skip_serializing_if = "Option::is_none")]
20    scale: Option<u32>,
21
22    #[serde(skip_serializing_if = "Option::is_none")]
23    max_len: Option<u32>,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    max_bytes: Option<u32>,
27
28    #[serde(skip_serializing_if = "<[_]>::is_empty")]
29    validators: &'static [TypeValidator],
30
31    #[serde(skip_serializing_if = "<[_]>::is_empty")]
32    sanitizers: &'static [TypeSanitizer],
33
34    #[serde(skip_serializing_if = "Not::not")]
35    indirect: bool,
36}
37
38impl Item {
39    #[must_use]
40    #[expect(
41        clippy::too_many_arguments,
42        reason = "schema item construction keeps generated scalar, relation, and validation metadata explicit"
43    )]
44    pub const fn new(
45        target: ItemTarget,
46        relation: Option<&'static str>,
47        scale: Option<u32>,
48        max_len: Option<u32>,
49        max_bytes: Option<u32>,
50        validators: &'static [TypeValidator],
51        sanitizers: &'static [TypeSanitizer],
52        indirect: bool,
53    ) -> Self {
54        Self {
55            target,
56            relation,
57            scale,
58            max_len,
59            max_bytes,
60            validators,
61            sanitizers,
62            indirect,
63        }
64    }
65
66    #[must_use]
67    pub const fn target(&self) -> &ItemTarget {
68        &self.target
69    }
70
71    #[must_use]
72    pub const fn relation(&self) -> Option<&'static str> {
73        self.relation
74    }
75
76    #[must_use]
77    pub const fn scale(&self) -> Option<u32> {
78        self.scale
79    }
80
81    #[must_use]
82    pub const fn max_len(&self) -> Option<u32> {
83        self.max_len
84    }
85
86    #[must_use]
87    pub const fn max_bytes(&self) -> Option<u32> {
88        self.max_bytes
89    }
90
91    #[must_use]
92    pub const fn validators(&self) -> &'static [TypeValidator] {
93        self.validators
94    }
95
96    #[must_use]
97    pub const fn sanitizers(&self) -> &'static [TypeSanitizer] {
98        self.sanitizers
99    }
100
101    #[must_use]
102    pub const fn indirect(&self) -> bool {
103        self.indirect
104    }
105
106    #[must_use]
107    pub const fn is_relation(&self) -> bool {
108        self.relation().is_some()
109    }
110}
111
112impl ValidateNode for Item {
113    fn validate(&self) -> Result<(), ErrorTree> {
114        let mut errs = ErrorTree::new();
115        let schema = schema_read();
116
117        // Phase 1: validate target shape.
118        match self.target() {
119            ItemTarget::Is(path) => {
120                // cannot be an entity
121                if schema.check_node_as::<Entity>(path).is_ok() {
122                    err!(errs, "a non-relation Item cannot reference an Entity");
123                }
124            }
125
126            ItemTarget::Primitive(_) => {}
127        }
128
129        // Phase 2: validate relation target compatibility.
130        if let Some(relation) = self.relation() {
131            match schema.cast_node::<Entity>(relation) {
132                Ok(entity) => {
133                    if entity.primary_key().fields().len() != 1 {
134                        err!(
135                            errs,
136                            "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",
137                            entity.primary_key().fields()
138                        );
139                    } else if let Some(primary_field) = entity.scalar_primary_key_field() {
140                        let expected = RelationComponentContract::from_field(primary_field);
141                        let actual = RelationComponentContract::from_item(self);
142                        if expected.mismatches(actual) {
143                            err!(
144                                errs,
145                                "relation target type mismatch: expected ({:?}, scale={:?}, max_len={:?}, max_bytes={:?}), found ({:?}, scale={:?}, max_len={:?}, max_bytes={:?})",
146                                expected.target(),
147                                expected.scale(),
148                                expected.max_len(),
149                                expected.max_bytes(),
150                                actual.target(),
151                                actual.scale(),
152                                actual.max_len(),
153                                actual.max_bytes(),
154                            );
155                        }
156                    } else {
157                        let primary_key_field =
158                            entity.primary_key().scalar_field().unwrap_or("<composite>");
159                        err!(
160                            errs,
161                            "relation entity '{relation}' missing primary key field '{0}'",
162                            primary_key_field
163                        );
164                    }
165                }
166                Err(_) => {
167                    err!(errs, "relation entity '{relation}' not found");
168                }
169            }
170        }
171
172        errs.result()
173    }
174}
175
176impl VisitableNode for Item {
177    fn drive<V: Visitor>(&self, v: &mut V) {
178        for node in self.validators() {
179            node.accept(v);
180        }
181    }
182}
183
184///
185/// ItemTarget
186///
187/// Local item target declaration, either by schema path or primitive runtime
188/// kind.
189///
190
191#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
192pub enum ItemTarget {
193    Is(&'static str),
194    Primitive(Primitive),
195}
196
197#[cfg(test)]
198mod tests;