Skip to main content

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