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(
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}