Skip to main content

icydb_schema/node/
entity.rs

1use crate::prelude::*;
2use std::any::Any;
3
4///
5/// Entity
6///
7
8#[derive(Clone, Debug, Serialize)]
9pub struct Entity {
10    def: Def,
11    store: &'static str,
12    primary_key: PrimaryKey,
13
14    #[serde(skip_serializing_if = "Option::is_none")]
15    name: Option<&'static str>,
16
17    #[serde(skip_serializing_if = "<[_]>::is_empty")]
18    indexes: &'static [Index],
19
20    #[serde(skip_serializing_if = "<[_]>::is_empty")]
21    relations: &'static [RelationEdge],
22
23    fields: FieldList,
24    ty: Type,
25}
26
27impl Entity {
28    #[must_use]
29    #[expect(
30        clippy::too_many_arguments,
31        reason = "schema entity construction keeps store, key, index, relation, field, and type metadata explicit"
32    )]
33    pub const fn new(
34        def: Def,
35        store: &'static str,
36        primary_key: PrimaryKey,
37        name: Option<&'static str>,
38        indexes: &'static [Index],
39        relations: &'static [RelationEdge],
40        fields: FieldList,
41        ty: Type,
42    ) -> Self {
43        Self {
44            def,
45            store,
46            primary_key,
47            name,
48            indexes,
49            relations,
50            fields,
51            ty,
52        }
53    }
54
55    #[must_use]
56    pub const fn def(&self) -> &Def {
57        &self.def
58    }
59
60    #[must_use]
61    pub const fn store(&self) -> &'static str {
62        self.store
63    }
64
65    #[must_use]
66    pub const fn primary_key(&self) -> &PrimaryKey {
67        &self.primary_key
68    }
69
70    #[must_use]
71    pub const fn name(&self) -> Option<&'static str> {
72        self.name
73    }
74
75    #[must_use]
76    pub const fn indexes(&self) -> &'static [Index] {
77        self.indexes
78    }
79
80    #[must_use]
81    pub const fn relations(&self) -> &'static [RelationEdge] {
82        self.relations
83    }
84
85    #[must_use]
86    pub const fn fields(&self) -> &FieldList {
87        &self.fields
88    }
89
90    #[must_use]
91    pub const fn ty(&self) -> &Type {
92        &self.ty
93    }
94
95    /// Return the scalar primary key field if this entity uses a scalar
96    /// primary-key contract.
97    #[must_use]
98    pub fn scalar_primary_key_field(&self) -> Option<&Field> {
99        self.fields().get(self.primary_key().scalar_field()?)
100    }
101
102    /// Resolve the entity name used for schema identity.
103    #[must_use]
104    pub fn resolved_name(&self) -> &'static str {
105        self.name().unwrap_or_else(|| self.def().ident())
106    }
107}
108
109impl MacroNode for Entity {
110    fn as_any(&self) -> &dyn Any {
111        self
112    }
113}
114
115impl ValidateNode for Entity {
116    fn validate(&self) -> Result<(), ErrorTree> {
117        let mut errs = ErrorTree::new();
118        let schema = schema_read();
119
120        // store
121        match schema.cast_node::<Store>(self.store()) {
122            Ok(_) => {}
123            Err(e) => errs.add(e),
124        }
125
126        for relation in self.relations() {
127            if let Err(e) = relation.validate_for_source(self) {
128                errs.merge_for(relation.ident(), e);
129            }
130        }
131
132        errs.result()
133    }
134}
135
136impl VisitableNode for Entity {
137    fn route_key(&self) -> String {
138        self.def().path()
139    }
140
141    fn drive<V: Visitor>(&self, v: &mut V) {
142        self.def().accept(v);
143        self.fields().accept(v);
144        self.ty().accept(v);
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::build::schema_write;
152
153    fn primitive_item(primitive: Primitive) -> Item {
154        Item::new(
155            ItemTarget::Primitive(primitive),
156            None,
157            None,
158            None,
159            None,
160            &[],
161            &[],
162            false,
163        )
164    }
165
166    fn field(ident: &'static str, primitive: Primitive) -> Field {
167        Field::new(
168            ident,
169            Value::new(Cardinality::One, primitive_item(primitive)),
170            None,
171            None,
172            None,
173        )
174    }
175
176    fn store(path: &'static str) -> Store {
177        Store::new(
178            Def::new("schema_entity_relation_edge", "Store"),
179            "STORE",
180            "schema_entity_relation_edge_store",
181            path,
182            110,
183            111,
184            112,
185        )
186    }
187
188    fn entity(
189        ident: &'static str,
190        store_path: &'static str,
191        pk_fields: &'static [&'static str],
192        relations: &'static [RelationEdge],
193        fields: &'static [Field],
194    ) -> Entity {
195        Entity::new(
196            Def::new("schema_entity_relation_edge", ident),
197            store_path,
198            PrimaryKey::new(pk_fields, PrimaryKeySource::External),
199            None,
200            &[],
201            relations,
202            FieldList::new(fields),
203            Type::new(&[], &[]),
204        )
205    }
206
207    #[test]
208    fn entity_validation_checks_owned_relation_edges() {
209        let store_path = "schema_entity_relation_edge::Store";
210        schema_write().insert_node(SchemaNode::Store(store(store_path)));
211        let target_fields = Box::leak(
212            vec![
213                field("tenant_id", Primitive::Nat64),
214                field("id", Primitive::Ulid),
215            ]
216            .into_boxed_slice(),
217        );
218        schema_write().insert_node(SchemaNode::Entity(entity(
219            "User",
220            store_path,
221            &["tenant_id", "id"],
222            &[],
223            target_fields,
224        )));
225
226        let source_fields = Box::leak(
227            vec![
228                field("author_tenant_id", Primitive::Nat64),
229                field("author_id", Primitive::Ulid),
230            ]
231            .into_boxed_slice(),
232        );
233        let source_relations = Box::leak(
234            vec![RelationEdge::new(
235                "author",
236                "schema_entity_relation_edge::User",
237                &["author_tenant_id", "author_id"],
238            )]
239            .into_boxed_slice(),
240        );
241        let source = entity(
242            "Post",
243            store_path,
244            &["author_id"],
245            source_relations,
246            source_fields,
247        );
248
249        source
250            .validate()
251            .expect("entity-owned matching relation edge should validate");
252    }
253
254    #[test]
255    fn entity_validation_reports_relation_edge_errors_under_relation_name() {
256        let store_path = "schema_entity_relation_edge_error::Store";
257        schema_write().insert_node(SchemaNode::Store(Store::new(
258            Def::new("schema_entity_relation_edge_error", "Store"),
259            "STORE",
260            "schema_entity_relation_edge_error_store",
261            store_path,
262            113,
263            114,
264            115,
265        )));
266        let target_fields = Box::leak(
267            vec![
268                field("tenant_id", Primitive::Nat64),
269                field("id", Primitive::Ulid),
270            ]
271            .into_boxed_slice(),
272        );
273        schema_write().insert_node(SchemaNode::Entity(entity(
274            "User",
275            store_path,
276            &["tenant_id", "id"],
277            &[],
278            target_fields,
279        )));
280
281        let source_fields = Box::leak(vec![field("author_id", Primitive::Ulid)].into_boxed_slice());
282        let source_relations = Box::leak(
283            vec![RelationEdge::new(
284                "author",
285                "schema_entity_relation_edge_error::User",
286                &["author_id"],
287            )]
288            .into_boxed_slice(),
289        );
290        let source = entity(
291            "BrokenPost",
292            store_path,
293            &["author_id"],
294            source_relations,
295            source_fields,
296        );
297
298        let err = source
299            .validate()
300            .expect_err("entity validation should reject invalid relation edge");
301
302        assert!(
303            err.children().contains_key("author"),
304            "relation edge errors should be nested under relation name: {err}",
305        );
306    }
307}