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    // Also protect manually constructed authoring nodes at model admission.
45    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        // Phase 1: validate target shape.
122        match self.target() {
123            ItemTarget::Is(path) => {
124                // cannot be an entity
125                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        // Phase 2: validate relation target compatibility.
134        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///
187/// ItemTarget
188///
189/// Local item target declaration, either by schema path or primitive runtime
190/// kind.
191///
192
193#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
194pub enum ItemTarget {
195    Is(&'static str),
196    Primitive(Primitive),
197}