Skip to main content

icydb_schema/node/
item.rs

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