Skip to main content

icydb_schema/node/
item.rs

1//! Module: node::item
2//!
3//! Responsibility: schema node model declarations and validation hooks.
4//! Does not own: runtime catalog authority or storage execution.
5//! Boundary: models macro/schema metadata before runtime acceptance.
6
7#[cfg(test)]
8mod tests;
9
10use crate::{node::relation::RelationComponentContract, prelude::*};
11use std::ops::Not;
12
13///
14/// Item
15///
16/// Canonical schema item descriptor for one scalar, relation, or primitive
17/// field target plus its attached sanitizers and validators.
18///
19
20#[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        // Phase 1: validate target shape.
126        match self.target() {
127            ItemTarget::Is(path) => {
128                // cannot be an entity
129                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        // Phase 2: validate relation target compatibility.
138        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///
193/// ItemTarget
194///
195/// Local item target declaration, either by schema path or primitive runtime
196/// kind.
197///
198
199#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
200pub enum ItemTarget {
201    Is(&'static str),
202    Primitive(Primitive),
203}