1use crate::prelude::*;
2use std::any::Any;
3
4#[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 #[must_use]
98 pub fn scalar_primary_key_field(&self) -> Option<&Field> {
99 self.fields().get(self.primary_key().scalar_field()?)
100 }
101
102 #[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 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_stable(
178 Def::new("schema_entity_relation_edge", "Store"),
179 "STORE",
180 "schema_entity_relation_edge_store",
181 path,
182 StoreStableMemoryConfig::new(110, 111, 112),
183 )
184 }
185
186 fn entity(
187 ident: &'static str,
188 store_path: &'static str,
189 pk_fields: &'static [&'static str],
190 relations: &'static [RelationEdge],
191 fields: &'static [Field],
192 ) -> Entity {
193 Entity::new(
194 Def::new("schema_entity_relation_edge", ident),
195 store_path,
196 PrimaryKey::new(pk_fields, PrimaryKeySource::External),
197 None,
198 &[],
199 relations,
200 FieldList::new(fields),
201 Type::new(&[], &[]),
202 )
203 }
204
205 #[test]
206 fn entity_validation_checks_owned_relation_edges() {
207 let store_path = "schema_entity_relation_edge::Store";
208 schema_write().insert_node(SchemaNode::Store(store(store_path)));
209 let target_fields = Box::leak(
210 vec![
211 field("tenant_id", Primitive::Nat64),
212 field("id", Primitive::Ulid),
213 ]
214 .into_boxed_slice(),
215 );
216 schema_write().insert_node(SchemaNode::Entity(entity(
217 "User",
218 store_path,
219 &["tenant_id", "id"],
220 &[],
221 target_fields,
222 )));
223
224 let source_fields = Box::leak(
225 vec![
226 field("author_tenant_id", Primitive::Nat64),
227 field("author_id", Primitive::Ulid),
228 ]
229 .into_boxed_slice(),
230 );
231 let source_relations = Box::leak(
232 vec![RelationEdge::new(
233 "author",
234 "schema_entity_relation_edge::User",
235 &["author_tenant_id", "author_id"],
236 )]
237 .into_boxed_slice(),
238 );
239 let source = entity(
240 "Post",
241 store_path,
242 &["author_id"],
243 source_relations,
244 source_fields,
245 );
246
247 source
248 .validate()
249 .expect("entity-owned matching relation edge should validate");
250 }
251
252 #[test]
253 fn entity_validation_reports_relation_edge_errors_under_relation_name() {
254 let store_path = "schema_entity_relation_edge_error::Store";
255 schema_write().insert_node(SchemaNode::Store(Store::new_stable(
256 Def::new("schema_entity_relation_edge_error", "Store"),
257 "STORE",
258 "schema_entity_relation_edge_error_store",
259 store_path,
260 StoreStableMemoryConfig::new(113, 114, 115),
261 )));
262 let target_fields = Box::leak(
263 vec![
264 field("tenant_id", Primitive::Nat64),
265 field("id", Primitive::Ulid),
266 ]
267 .into_boxed_slice(),
268 );
269 schema_write().insert_node(SchemaNode::Entity(entity(
270 "User",
271 store_path,
272 &["tenant_id", "id"],
273 &[],
274 target_fields,
275 )));
276
277 let source_fields = Box::leak(vec![field("author_id", Primitive::Ulid)].into_boxed_slice());
278 let source_relations = Box::leak(
279 vec![RelationEdge::new(
280 "author",
281 "schema_entity_relation_edge_error::User",
282 &["author_id"],
283 )]
284 .into_boxed_slice(),
285 );
286 let source = entity(
287 "BrokenPost",
288 store_path,
289 &["author_id"],
290 source_relations,
291 source_fields,
292 );
293
294 let err = source
295 .validate()
296 .expect_err("entity validation should reject invalid relation edge");
297
298 assert!(
299 err.children().contains_key("author"),
300 "relation edge errors should be nested under relation name: {err}",
301 );
302 }
303}