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 = "<[_]>::is_empty")]
25    validators: &'static [TypeValidator],
26
27    #[serde(skip_serializing_if = "<[_]>::is_empty")]
28    sanitizers: &'static [TypeSanitizer],
29
30    #[serde(skip_serializing_if = "Not::not")]
31    indirect: bool,
32}
33
34impl Item {
35    #[must_use]
36    pub const fn new(
37        target: ItemTarget,
38        relation: Option<&'static str>,
39        scale: Option<u32>,
40        max_len: Option<u32>,
41        validators: &'static [TypeValidator],
42        sanitizers: &'static [TypeSanitizer],
43        indirect: bool,
44    ) -> Self {
45        Self {
46            target,
47            relation,
48            scale,
49            max_len,
50            validators,
51            sanitizers,
52            indirect,
53        }
54    }
55
56    #[must_use]
57    pub const fn target(&self) -> &ItemTarget {
58        &self.target
59    }
60
61    #[must_use]
62    pub const fn relation(&self) -> Option<&'static str> {
63        self.relation
64    }
65
66    #[must_use]
67    pub const fn scale(&self) -> Option<u32> {
68        self.scale
69    }
70
71    #[must_use]
72    pub const fn max_len(&self) -> Option<u32> {
73        self.max_len
74    }
75
76    #[must_use]
77    pub const fn validators(&self) -> &'static [TypeValidator] {
78        self.validators
79    }
80
81    #[must_use]
82    pub const fn sanitizers(&self) -> &'static [TypeSanitizer] {
83        self.sanitizers
84    }
85
86    #[must_use]
87    pub const fn indirect(&self) -> bool {
88        self.indirect
89    }
90
91    #[must_use]
92    pub const fn is_relation(&self) -> bool {
93        self.relation().is_some()
94    }
95}
96
97impl ValidateNode for Item {
98    fn validate(&self) -> Result<(), ErrorTree> {
99        let mut errs = ErrorTree::new();
100        let schema = schema_read();
101
102        // Phase 1: validate target shape.
103        match self.target() {
104            ItemTarget::Is(path) => {
105                // cannot be an entity
106                if schema.check_node_as::<Entity>(path).is_ok() {
107                    err!(errs, "a non-relation Item cannot reference an Entity");
108                }
109            }
110
111            ItemTarget::Primitive(_) => {}
112        }
113
114        // Phase 2: validate relation target compatibility.
115        if let Some(relation) = self.relation() {
116            // Step 1: Ensure the relation path exists and is an Entity
117            match schema.cast_node::<Entity>(relation) {
118                Ok(entity) => {
119                    // Step 2: Get target of the relation entity (usually from its primary key field)
120                    if let Some(primary_field) = entity.get_pk_field() {
121                        let relation_target = primary_field.value().item().target();
122
123                        // Step 3: Compare declared item target and primitive metadata.
124                        let relation_scale = primary_field.value().item().scale();
125                        let relation_max_len = primary_field.value().item().max_len();
126                        if self.target() != relation_target
127                            || self.scale() != relation_scale
128                            || self.max_len() != relation_max_len
129                        {
130                            err!(
131                                errs,
132                                "relation target type mismatch: expected ({:?}, scale={:?}, max_len={:?}), found ({:?}, scale={:?}, max_len={:?})",
133                                relation_target,
134                                relation_scale,
135                                relation_max_len,
136                                self.target(),
137                                self.scale(),
138                                self.max_len()
139                            );
140                        }
141                    } else {
142                        err!(
143                            errs,
144                            "relation entity '{relation}' missing primary key field '{0}'",
145                            entity.primary_key().field()
146                        );
147                    }
148                }
149                Err(_) => {
150                    err!(errs, "relation entity '{relation}' not found");
151                }
152            }
153        }
154
155        errs.result()
156    }
157}
158
159impl VisitableNode for Item {
160    fn drive<V: Visitor>(&self, v: &mut V) {
161        for node in self.validators() {
162            node.accept(v);
163        }
164    }
165}
166
167///
168/// ItemTarget
169///
170/// Local item target declaration, either by schema path or primitive runtime
171/// kind.
172///
173
174#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
175pub enum ItemTarget {
176    Is(&'static str),
177    Primitive(Primitive),
178}