1use crate::model::{
7 entity::EntityModel,
8 field::{FieldKind, RelationStrength},
9};
10use candid::CandidType;
11use serde::{Deserialize, Serialize};
12
13#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
21pub struct EntitySchemaDescription {
22 pub(crate) entity_path: String,
23 pub(crate) entity_name: String,
24 pub(crate) primary_key: String,
25 pub(crate) fields: Vec<EntityFieldDescription>,
26 pub(crate) indexes: Vec<EntityIndexDescription>,
27 pub(crate) relations: Vec<EntityRelationDescription>,
28}
29
30impl EntitySchemaDescription {
31 #[must_use]
33 pub const fn new(
34 entity_path: String,
35 entity_name: String,
36 primary_key: String,
37 fields: Vec<EntityFieldDescription>,
38 indexes: Vec<EntityIndexDescription>,
39 relations: Vec<EntityRelationDescription>,
40 ) -> Self {
41 Self {
42 entity_path,
43 entity_name,
44 primary_key,
45 fields,
46 indexes,
47 relations,
48 }
49 }
50
51 #[must_use]
53 pub const fn entity_path(&self) -> &str {
54 self.entity_path.as_str()
55 }
56
57 #[must_use]
59 pub const fn entity_name(&self) -> &str {
60 self.entity_name.as_str()
61 }
62
63 #[must_use]
65 pub const fn primary_key(&self) -> &str {
66 self.primary_key.as_str()
67 }
68
69 #[must_use]
71 pub const fn fields(&self) -> &[EntityFieldDescription] {
72 self.fields.as_slice()
73 }
74
75 #[must_use]
77 pub const fn indexes(&self) -> &[EntityIndexDescription] {
78 self.indexes.as_slice()
79 }
80
81 #[must_use]
83 pub const fn relations(&self) -> &[EntityRelationDescription] {
84 self.relations.as_slice()
85 }
86}
87
88#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
96pub struct EntityFieldDescription {
97 pub(crate) name: String,
98 pub(crate) kind: String,
99 pub(crate) primary_key: bool,
100 pub(crate) queryable: bool,
101}
102
103impl EntityFieldDescription {
104 #[must_use]
106 pub const fn new(name: String, kind: String, primary_key: bool, queryable: bool) -> Self {
107 Self {
108 name,
109 kind,
110 primary_key,
111 queryable,
112 }
113 }
114
115 #[must_use]
117 pub const fn name(&self) -> &str {
118 self.name.as_str()
119 }
120
121 #[must_use]
123 pub const fn kind(&self) -> &str {
124 self.kind.as_str()
125 }
126
127 #[must_use]
129 pub const fn primary_key(&self) -> bool {
130 self.primary_key
131 }
132
133 #[must_use]
135 pub const fn queryable(&self) -> bool {
136 self.queryable
137 }
138}
139
140#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
148pub struct EntityIndexDescription {
149 pub(crate) name: String,
150 pub(crate) unique: bool,
151 pub(crate) fields: Vec<String>,
152}
153
154impl EntityIndexDescription {
155 #[must_use]
157 pub const fn new(name: String, unique: bool, fields: Vec<String>) -> Self {
158 Self {
159 name,
160 unique,
161 fields,
162 }
163 }
164
165 #[must_use]
167 pub const fn name(&self) -> &str {
168 self.name.as_str()
169 }
170
171 #[must_use]
173 pub const fn unique(&self) -> bool {
174 self.unique
175 }
176
177 #[must_use]
179 pub const fn fields(&self) -> &[String] {
180 self.fields.as_slice()
181 }
182}
183
184#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct EntityRelationDescription {
193 pub(crate) field: String,
194 pub(crate) target_path: String,
195 pub(crate) target_entity_name: String,
196 pub(crate) target_store_path: String,
197 pub(crate) strength: EntityRelationStrength,
198 pub(crate) cardinality: EntityRelationCardinality,
199}
200
201impl EntityRelationDescription {
202 #[must_use]
204 pub const fn new(
205 field: String,
206 target_path: String,
207 target_entity_name: String,
208 target_store_path: String,
209 strength: EntityRelationStrength,
210 cardinality: EntityRelationCardinality,
211 ) -> Self {
212 Self {
213 field,
214 target_path,
215 target_entity_name,
216 target_store_path,
217 strength,
218 cardinality,
219 }
220 }
221
222 #[must_use]
224 pub const fn field(&self) -> &str {
225 self.field.as_str()
226 }
227
228 #[must_use]
230 pub const fn target_path(&self) -> &str {
231 self.target_path.as_str()
232 }
233
234 #[must_use]
236 pub const fn target_entity_name(&self) -> &str {
237 self.target_entity_name.as_str()
238 }
239
240 #[must_use]
242 pub const fn target_store_path(&self) -> &str {
243 self.target_store_path.as_str()
244 }
245
246 #[must_use]
248 pub const fn strength(&self) -> EntityRelationStrength {
249 self.strength
250 }
251
252 #[must_use]
254 pub const fn cardinality(&self) -> EntityRelationCardinality {
255 self.cardinality
256 }
257}
258
259#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
265pub enum EntityRelationStrength {
266 Strong,
267 Weak,
268}
269
270#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
276pub enum EntityRelationCardinality {
277 Single,
278 List,
279 Set,
280}
281
282#[must_use]
284pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
285 let mut fields = Vec::with_capacity(model.fields.len());
286 let mut relations = Vec::new();
287 for field in model.fields {
288 let field_kind = summarize_field_kind(&field.kind);
289 let queryable = field.kind.value_kind().is_queryable();
290 let primary_key = field.name == model.primary_key.name;
291
292 fields.push(EntityFieldDescription::new(
293 field.name.to_string(),
294 field_kind,
295 primary_key,
296 queryable,
297 ));
298
299 if let Some(relation) = relation_from_field_kind(field.name, &field.kind) {
300 relations.push(relation);
301 }
302 }
303
304 let mut indexes = Vec::with_capacity(model.indexes.len());
305 for index in model.indexes {
306 indexes.push(EntityIndexDescription::new(
307 index.name.to_string(),
308 index.unique,
309 index
310 .fields
311 .iter()
312 .map(|field| (*field).to_string())
313 .collect(),
314 ));
315 }
316
317 EntitySchemaDescription::new(
318 model.path.to_string(),
319 model.entity_name.to_string(),
320 model.primary_key.name.to_string(),
321 fields,
322 indexes,
323 relations,
324 )
325}
326
327fn relation_from_field_kind(
329 field_name: &str,
330 kind: &FieldKind,
331) -> Option<EntityRelationDescription> {
332 match kind {
333 FieldKind::Relation {
334 target_path,
335 target_entity_name,
336 target_store_path,
337 strength,
338 ..
339 } => Some(EntityRelationDescription::new(
340 field_name.to_string(),
341 (*target_path).to_string(),
342 (*target_entity_name).to_string(),
343 (*target_store_path).to_string(),
344 relation_strength(*strength),
345 EntityRelationCardinality::Single,
346 )),
347 FieldKind::List(inner) => {
348 relation_from_collection_relation(field_name, inner, EntityRelationCardinality::List)
349 }
350 FieldKind::Set(inner) => {
351 relation_from_collection_relation(field_name, inner, EntityRelationCardinality::Set)
352 }
353 FieldKind::Account
354 | FieldKind::Blob
355 | FieldKind::Bool
356 | FieldKind::Date
357 | FieldKind::Decimal { .. }
358 | FieldKind::Duration
359 | FieldKind::Enum { .. }
360 | FieldKind::Float32
361 | FieldKind::Float64
362 | FieldKind::Int
363 | FieldKind::Int128
364 | FieldKind::IntBig
365 | FieldKind::Principal
366 | FieldKind::Subaccount
367 | FieldKind::Text
368 | FieldKind::Timestamp
369 | FieldKind::Uint
370 | FieldKind::Uint128
371 | FieldKind::UintBig
372 | FieldKind::Ulid
373 | FieldKind::Unit
374 | FieldKind::Map { .. }
375 | FieldKind::Structured { .. } => None,
376 }
377}
378
379fn relation_from_collection_relation(
381 field_name: &str,
382 inner: &FieldKind,
383 cardinality: EntityRelationCardinality,
384) -> Option<EntityRelationDescription> {
385 let FieldKind::Relation {
386 target_path,
387 target_entity_name,
388 target_store_path,
389 strength,
390 ..
391 } = inner
392 else {
393 return None;
394 };
395
396 Some(EntityRelationDescription::new(
397 field_name.to_string(),
398 (*target_path).to_string(),
399 (*target_entity_name).to_string(),
400 (*target_store_path).to_string(),
401 relation_strength(*strength),
402 cardinality,
403 ))
404}
405
406const fn relation_strength(strength: RelationStrength) -> EntityRelationStrength {
408 match strength {
409 RelationStrength::Strong => EntityRelationStrength::Strong,
410 RelationStrength::Weak => EntityRelationStrength::Weak,
411 }
412}
413
414fn summarize_field_kind(kind: &FieldKind) -> String {
416 match kind {
417 FieldKind::Account => "account".to_string(),
418 FieldKind::Blob => "blob".to_string(),
419 FieldKind::Bool => "bool".to_string(),
420 FieldKind::Date => "date".to_string(),
421 FieldKind::Decimal { scale } => format!("decimal(scale={scale})"),
422 FieldKind::Duration => "duration".to_string(),
423 FieldKind::Enum { path } => format!("enum({path})"),
424 FieldKind::Float32 => "float32".to_string(),
425 FieldKind::Float64 => "float64".to_string(),
426 FieldKind::Int => "int".to_string(),
427 FieldKind::Int128 => "int128".to_string(),
428 FieldKind::IntBig => "int_big".to_string(),
429 FieldKind::Principal => "principal".to_string(),
430 FieldKind::Subaccount => "subaccount".to_string(),
431 FieldKind::Text => "text".to_string(),
432 FieldKind::Timestamp => "timestamp".to_string(),
433 FieldKind::Uint => "uint".to_string(),
434 FieldKind::Uint128 => "uint128".to_string(),
435 FieldKind::UintBig => "uint_big".to_string(),
436 FieldKind::Ulid => "ulid".to_string(),
437 FieldKind::Unit => "unit".to_string(),
438 FieldKind::Relation {
439 target_entity_name,
440 key_kind,
441 strength,
442 ..
443 } => format!(
444 "relation(target={target_entity_name}, key={}, strength={})",
445 summarize_field_kind(key_kind),
446 summarize_relation_strength(*strength),
447 ),
448 FieldKind::List(inner) => format!("list<{}>", summarize_field_kind(inner)),
449 FieldKind::Set(inner) => format!("set<{}>", summarize_field_kind(inner)),
450 FieldKind::Map { key, value } => {
451 format!(
452 "map<{}, {}>",
453 summarize_field_kind(key),
454 summarize_field_kind(value)
455 )
456 }
457 FieldKind::Structured { queryable } => format!("structured(queryable={queryable})"),
458 }
459}
460
461const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
463 match strength {
464 RelationStrength::Strong => "strong",
465 RelationStrength::Weak => "weak",
466 }
467}