Skip to main content

prax_schema/ast/
model.rs

1//! Model definitions for the Prax schema AST.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use smol_str::SmolStr;
6
7use super::{Attribute, Documentation, Field, Ident, Span};
8
9/// A model definition (maps to a database table).
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct Model {
12    /// Model name.
13    pub name: Ident,
14    /// Model fields.
15    pub fields: IndexMap<SmolStr, Field>,
16    /// Model-level attributes (prefixed with `@@`).
17    pub attributes: Vec<Attribute>,
18    /// Documentation comment.
19    pub documentation: Option<Documentation>,
20    /// Source location.
21    pub span: Span,
22    /// Source file this model was parsed from (None for single-file path).
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub source_id: Option<crate::loader::SourceId>,
25}
26
27impl Model {
28    /// Create a new model.
29    pub fn new(name: Ident, span: Span) -> Self {
30        Self {
31            name,
32            fields: IndexMap::new(),
33            attributes: vec![],
34            documentation: None,
35            span,
36            source_id: None,
37        }
38    }
39
40    /// Get the model name as a string.
41    pub fn name(&self) -> &str {
42        self.name.as_str()
43    }
44
45    /// Add a field to the model.
46    pub fn add_field(&mut self, field: Field) {
47        self.fields.insert(field.name.name.clone(), field);
48    }
49
50    /// Get a field by name.
51    pub fn get_field(&self, name: &str) -> Option<&Field> {
52        self.fields.get(name)
53    }
54
55    /// Get the primary key field(s).
56    pub fn id_fields(&self) -> Vec<&Field> {
57        self.fields.values().filter(|f| f.is_id()).collect()
58    }
59
60    /// Get all relation fields.
61    pub fn relation_fields(&self) -> Vec<&Field> {
62        self.fields.values().filter(|f| f.is_relation()).collect()
63    }
64
65    /// Get all scalar (non-relation) fields.
66    pub fn scalar_fields(&self) -> Vec<&Field> {
67        self.fields.values().filter(|f| !f.is_relation()).collect()
68    }
69
70    /// Check if this model has a specific model-level attribute.
71    pub fn has_attribute(&self, name: &str) -> bool {
72        self.attributes.iter().any(|a| a.is(name))
73    }
74
75    /// Get a model-level attribute by name.
76    pub fn get_attribute(&self, name: &str) -> Option<&Attribute> {
77        self.attributes.iter().find(|a| a.is(name))
78    }
79
80    /// Get the database table name (from `@@map` or model name).
81    pub fn table_name(&self) -> &str {
82        self.get_attribute("map")
83            .and_then(|a| a.first_arg())
84            .and_then(|v| v.as_string())
85            .unwrap_or_else(|| self.name())
86    }
87
88    /// Set documentation.
89    pub fn with_documentation(mut self, doc: Documentation) -> Self {
90        self.documentation = Some(doc);
91        self
92    }
93}
94
95/// An enum definition.
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct Enum {
98    /// Enum name.
99    pub name: Ident,
100    /// Enum variants.
101    pub variants: Vec<EnumVariant>,
102    /// Enum-level attributes.
103    pub attributes: Vec<Attribute>,
104    /// Documentation comment.
105    pub documentation: Option<Documentation>,
106    /// Source location.
107    pub span: Span,
108    /// Source file this enum was parsed from (None for single-file path).
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub source_id: Option<crate::loader::SourceId>,
111}
112
113impl Enum {
114    /// Create a new enum.
115    pub fn new(name: Ident, span: Span) -> Self {
116        Self {
117            name,
118            variants: vec![],
119            attributes: vec![],
120            documentation: None,
121            span,
122            source_id: None,
123        }
124    }
125
126    /// Get the enum name as a string.
127    pub fn name(&self) -> &str {
128        self.name.as_str()
129    }
130
131    /// The database type name for this enum: its `@@map("...")` argument when
132    /// present, otherwise the enum name. Mirrors [`Model::table_name`] so a
133    /// schema whose enum is `RepoRole @@map("repo_role")` emits and references
134    /// the real `repo_role` Postgres type instead of `RepoRole`.
135    pub fn database_name(&self) -> &str {
136        self.attributes
137            .iter()
138            .find(|a| a.name() == "map")
139            .and_then(|a| a.first_arg())
140            .and_then(|v| v.as_string())
141            .unwrap_or_else(|| self.name.as_str())
142    }
143
144    /// Add a variant to the enum.
145    pub fn add_variant(&mut self, variant: EnumVariant) {
146        self.variants.push(variant);
147    }
148
149    /// Get a variant by name.
150    pub fn get_variant(&self, name: &str) -> Option<&EnumVariant> {
151        self.variants.iter().find(|v| v.name.as_str() == name)
152    }
153
154    /// Get the database type name (from `@@map` or enum name).
155    pub fn db_name(&self) -> &str {
156        self.attributes
157            .iter()
158            .find(|a| a.is("map"))
159            .and_then(|a| a.first_arg())
160            .and_then(|v| v.as_string())
161            .unwrap_or_else(|| self.name())
162    }
163
164    /// Set documentation.
165    pub fn with_documentation(mut self, doc: Documentation) -> Self {
166        self.documentation = Some(doc);
167        self
168    }
169}
170
171/// An enum variant.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct EnumVariant {
174    /// Variant name.
175    pub name: Ident,
176    /// Variant-level attributes.
177    pub attributes: Vec<Attribute>,
178    /// Documentation comment.
179    pub documentation: Option<Documentation>,
180    /// Source location.
181    pub span: Span,
182}
183
184impl EnumVariant {
185    /// Create a new enum variant.
186    pub fn new(name: Ident, span: Span) -> Self {
187        Self {
188            name,
189            attributes: vec![],
190            documentation: None,
191            span,
192        }
193    }
194
195    /// Get the variant name as a string.
196    pub fn name(&self) -> &str {
197        self.name.as_str()
198    }
199
200    /// Get the database value (from `@map` or variant name).
201    pub fn db_value(&self) -> &str {
202        self.attributes
203            .iter()
204            .find(|a| a.is("map"))
205            .and_then(|a| a.first_arg())
206            .and_then(|v| v.as_string())
207            .unwrap_or_else(|| self.name())
208    }
209}
210
211/// A composite type definition (for embedded documents / JSON).
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct CompositeType {
214    /// Type name.
215    pub name: Ident,
216    /// Type fields.
217    pub fields: IndexMap<SmolStr, Field>,
218    /// Documentation comment.
219    pub documentation: Option<Documentation>,
220    /// Source location.
221    pub span: Span,
222    /// Source file this type was parsed from (None for single-file path).
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub source_id: Option<crate::loader::SourceId>,
225}
226
227impl CompositeType {
228    /// Create a new composite type.
229    pub fn new(name: Ident, span: Span) -> Self {
230        Self {
231            name,
232            fields: IndexMap::new(),
233            documentation: None,
234            span,
235            source_id: None,
236        }
237    }
238
239    /// Get the type name as a string.
240    pub fn name(&self) -> &str {
241        self.name.as_str()
242    }
243
244    /// Add a field to the type.
245    pub fn add_field(&mut self, field: Field) {
246        self.fields.insert(field.name.name.clone(), field);
247    }
248
249    /// Get a field by name.
250    pub fn get_field(&self, name: &str) -> Option<&Field> {
251        self.fields.get(name)
252    }
253
254    /// Set documentation.
255    pub fn with_documentation(mut self, doc: Documentation) -> Self {
256        self.documentation = Some(doc);
257        self
258    }
259}
260
261/// A view definition (read-only model mapping to a database view).
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263pub struct View {
264    /// View name.
265    pub name: Ident,
266    /// View fields.
267    pub fields: IndexMap<SmolStr, Field>,
268    /// View-level attributes.
269    pub attributes: Vec<Attribute>,
270    /// Documentation comment.
271    pub documentation: Option<Documentation>,
272    /// Source location.
273    pub span: Span,
274    /// Source file this view was parsed from (None for single-file path).
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub source_id: Option<crate::loader::SourceId>,
277}
278
279impl View {
280    /// Create a new view.
281    pub fn new(name: Ident, span: Span) -> Self {
282        Self {
283            name,
284            fields: IndexMap::new(),
285            attributes: vec![],
286            documentation: None,
287            span,
288            source_id: None,
289        }
290    }
291
292    /// Get the view name as a string.
293    pub fn name(&self) -> &str {
294        self.name.as_str()
295    }
296
297    /// Add a field to the view.
298    pub fn add_field(&mut self, field: Field) {
299        self.fields.insert(field.name.name.clone(), field);
300    }
301
302    /// Get the database view name (from `@@map` or view name).
303    pub fn view_name(&self) -> &str {
304        self.attributes
305            .iter()
306            .find(|a| a.is("map"))
307            .and_then(|a| a.first_arg())
308            .and_then(|v| v.as_string())
309            .unwrap_or_else(|| self.name())
310    }
311
312    /// Get the SQL query that defines the view (from `@@sql` attribute).
313    pub fn sql_query(&self) -> Option<&str> {
314        self.attributes
315            .iter()
316            .find(|a| a.is("sql"))
317            .and_then(|a| a.first_arg())
318            .and_then(|v| v.as_string())
319    }
320
321    /// Check if the view is materialized (has `@@materialized` attribute).
322    pub fn is_materialized(&self) -> bool {
323        self.attributes.iter().any(|a| a.is("materialized"))
324    }
325
326    /// Get the refresh interval for materialized views (from `@@refreshInterval`).
327    pub fn refresh_interval(&self) -> Option<&str> {
328        self.attributes
329            .iter()
330            .find(|a| a.is("refreshInterval"))
331            .and_then(|a| a.first_arg())
332            .and_then(|v| v.as_string())
333    }
334
335    /// Set documentation.
336    pub fn with_documentation(mut self, doc: Documentation) -> Self {
337        self.documentation = Some(doc);
338        self
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::ast::{
346        Attribute, AttributeArg, AttributeValue, FieldType, ScalarType, TypeModifier,
347    };
348
349    fn make_span() -> Span {
350        Span::new(0, 10)
351    }
352
353    fn make_ident(name: &str) -> Ident {
354        Ident::new(name, make_span())
355    }
356
357    fn make_field(name: &str, field_type: FieldType, modifier: TypeModifier) -> Field {
358        Field::new(make_ident(name), field_type, modifier, vec![], make_span())
359    }
360
361    fn make_id_field() -> Field {
362        let mut field = make_field(
363            "id",
364            FieldType::Scalar(ScalarType::Int),
365            TypeModifier::Required,
366        );
367        field
368            .attributes
369            .push(Attribute::simple(make_ident("id"), make_span()));
370        field
371            .attributes
372            .push(Attribute::simple(make_ident("auto"), make_span()));
373        field
374    }
375
376    fn make_attribute(name: &str) -> Attribute {
377        Attribute::simple(make_ident(name), make_span())
378    }
379
380    fn make_attribute_with_string(name: &str, value: &str) -> Attribute {
381        Attribute::new(
382            make_ident(name),
383            vec![AttributeArg::positional(
384                AttributeValue::String(value.into()),
385                make_span(),
386            )],
387            make_span(),
388        )
389    }
390
391    // ==================== Model Tests ====================
392
393    #[test]
394    fn test_model_new() {
395        let model = Model::new(make_ident("User"), make_span());
396
397        assert_eq!(model.name(), "User");
398        assert!(model.fields.is_empty());
399        assert!(model.attributes.is_empty());
400        assert!(model.documentation.is_none());
401    }
402
403    #[test]
404    fn test_model_name() {
405        let model = Model::new(make_ident("BlogPost"), make_span());
406        assert_eq!(model.name(), "BlogPost");
407    }
408
409    #[test]
410    fn test_model_add_field() {
411        let mut model = Model::new(make_ident("User"), make_span());
412        let field = make_field(
413            "email",
414            FieldType::Scalar(ScalarType::String),
415            TypeModifier::Required,
416        );
417
418        model.add_field(field);
419
420        assert_eq!(model.fields.len(), 1);
421        assert!(model.fields.contains_key("email"));
422    }
423
424    #[test]
425    fn test_model_add_multiple_fields() {
426        let mut model = Model::new(make_ident("User"), make_span());
427        model.add_field(make_id_field());
428        model.add_field(make_field(
429            "email",
430            FieldType::Scalar(ScalarType::String),
431            TypeModifier::Required,
432        ));
433        model.add_field(make_field(
434            "name",
435            FieldType::Scalar(ScalarType::String),
436            TypeModifier::Optional,
437        ));
438
439        assert_eq!(model.fields.len(), 3);
440    }
441
442    #[test]
443    fn test_model_get_field() {
444        let mut model = Model::new(make_ident("User"), make_span());
445        model.add_field(make_field(
446            "email",
447            FieldType::Scalar(ScalarType::String),
448            TypeModifier::Required,
449        ));
450
451        let field = model.get_field("email");
452        assert!(field.is_some());
453        assert_eq!(field.unwrap().name(), "email");
454
455        assert!(model.get_field("nonexistent").is_none());
456    }
457
458    #[test]
459    fn test_model_id_fields() {
460        let mut model = Model::new(make_ident("User"), make_span());
461        model.add_field(make_id_field());
462        model.add_field(make_field(
463            "email",
464            FieldType::Scalar(ScalarType::String),
465            TypeModifier::Required,
466        ));
467
468        let id_fields = model.id_fields();
469        assert_eq!(id_fields.len(), 1);
470        assert_eq!(id_fields[0].name(), "id");
471    }
472
473    #[test]
474    fn test_model_id_fields_none() {
475        let mut model = Model::new(make_ident("User"), make_span());
476        model.add_field(make_field(
477            "email",
478            FieldType::Scalar(ScalarType::String),
479            TypeModifier::Required,
480        ));
481
482        let id_fields = model.id_fields();
483        assert!(id_fields.is_empty());
484    }
485
486    #[test]
487    fn test_model_relation_fields() {
488        let mut model = Model::new(make_ident("Post"), make_span());
489        model.add_field(make_id_field());
490        model.add_field(make_field(
491            "title",
492            FieldType::Scalar(ScalarType::String),
493            TypeModifier::Required,
494        ));
495        model.add_field(make_field(
496            "author",
497            FieldType::Model("User".into()),
498            TypeModifier::Required,
499        ));
500
501        let rel_fields = model.relation_fields();
502        assert_eq!(rel_fields.len(), 1);
503        assert_eq!(rel_fields[0].name(), "author");
504    }
505
506    #[test]
507    fn test_model_scalar_fields() {
508        let mut model = Model::new(make_ident("Post"), make_span());
509        model.add_field(make_id_field());
510        model.add_field(make_field(
511            "title",
512            FieldType::Scalar(ScalarType::String),
513            TypeModifier::Required,
514        ));
515        model.add_field(make_field(
516            "author",
517            FieldType::Model("User".into()),
518            TypeModifier::Required,
519        ));
520
521        let scalar_fields = model.scalar_fields();
522        assert_eq!(scalar_fields.len(), 2);
523    }
524
525    #[test]
526    fn test_model_has_attribute() {
527        let mut model = Model::new(make_ident("User"), make_span());
528        model.attributes.push(make_attribute("map"));
529
530        assert!(model.has_attribute("map"));
531        assert!(!model.has_attribute("index"));
532    }
533
534    #[test]
535    fn test_model_get_attribute() {
536        let mut model = Model::new(make_ident("User"), make_span());
537        model
538            .attributes
539            .push(make_attribute_with_string("map", "users"));
540
541        let attr = model.get_attribute("map");
542        assert!(attr.is_some());
543        assert!(attr.unwrap().is("map"));
544
545        assert!(model.get_attribute("index").is_none());
546    }
547
548    #[test]
549    fn test_model_table_name_default() {
550        let model = Model::new(make_ident("User"), make_span());
551        assert_eq!(model.table_name(), "User");
552    }
553
554    #[test]
555    fn test_model_table_name_mapped() {
556        let mut model = Model::new(make_ident("User"), make_span());
557        model
558            .attributes
559            .push(make_attribute_with_string("map", "app_users"));
560
561        assert_eq!(model.table_name(), "app_users");
562    }
563
564    #[test]
565    fn test_model_with_documentation() {
566        let model = Model::new(make_ident("User"), make_span())
567            .with_documentation(Documentation::new("Represents a user", make_span()));
568
569        assert!(model.documentation.is_some());
570        assert_eq!(model.documentation.unwrap().text, "Represents a user");
571    }
572
573    // ==================== Enum Tests ====================
574
575    #[test]
576    fn test_enum_new() {
577        let e = Enum::new(make_ident("Role"), make_span());
578
579        assert_eq!(e.name(), "Role");
580        assert!(e.variants.is_empty());
581        assert!(e.attributes.is_empty());
582        assert!(e.documentation.is_none());
583    }
584
585    #[test]
586    fn test_enum_add_variant() {
587        let mut e = Enum::new(make_ident("Role"), make_span());
588        e.add_variant(EnumVariant::new(make_ident("Admin"), make_span()));
589        e.add_variant(EnumVariant::new(make_ident("User"), make_span()));
590
591        assert_eq!(e.variants.len(), 2);
592    }
593
594    #[test]
595    fn test_enum_get_variant() {
596        let mut e = Enum::new(make_ident("Role"), make_span());
597        e.add_variant(EnumVariant::new(make_ident("Admin"), make_span()));
598        e.add_variant(EnumVariant::new(make_ident("User"), make_span()));
599
600        let variant = e.get_variant("Admin");
601        assert!(variant.is_some());
602        assert_eq!(variant.unwrap().name(), "Admin");
603
604        assert!(e.get_variant("Moderator").is_none());
605    }
606
607    #[test]
608    fn test_enum_db_name_default() {
609        let e = Enum::new(make_ident("Role"), make_span());
610        assert_eq!(e.db_name(), "Role");
611    }
612
613    #[test]
614    fn test_enum_db_name_mapped() {
615        let mut e = Enum::new(make_ident("Role"), make_span());
616        e.attributes
617            .push(make_attribute_with_string("map", "user_role"));
618
619        assert_eq!(e.db_name(), "user_role");
620    }
621
622    #[test]
623    fn test_enum_with_documentation() {
624        let e = Enum::new(make_ident("Role"), make_span())
625            .with_documentation(Documentation::new("User roles", make_span()));
626
627        assert!(e.documentation.is_some());
628    }
629
630    // ==================== EnumVariant Tests ====================
631
632    #[test]
633    fn test_enum_variant_new() {
634        let variant = EnumVariant::new(make_ident("Admin"), make_span());
635
636        assert_eq!(variant.name(), "Admin");
637        assert!(variant.attributes.is_empty());
638        assert!(variant.documentation.is_none());
639    }
640
641    #[test]
642    fn test_enum_variant_db_value_default() {
643        let variant = EnumVariant::new(make_ident("Admin"), make_span());
644        assert_eq!(variant.db_value(), "Admin");
645    }
646
647    #[test]
648    fn test_enum_variant_db_value_mapped() {
649        let mut variant = EnumVariant::new(make_ident("Admin"), make_span());
650        variant
651            .attributes
652            .push(make_attribute_with_string("map", "ADMIN_USER"));
653
654        assert_eq!(variant.db_value(), "ADMIN_USER");
655    }
656
657    // ==================== CompositeType Tests ====================
658
659    #[test]
660    fn test_composite_type_new() {
661        let ct = CompositeType::new(make_ident("Address"), make_span());
662
663        assert_eq!(ct.name(), "Address");
664        assert!(ct.fields.is_empty());
665        assert!(ct.documentation.is_none());
666    }
667
668    #[test]
669    fn test_composite_type_add_field() {
670        let mut ct = CompositeType::new(make_ident("Address"), make_span());
671        ct.add_field(make_field(
672            "street",
673            FieldType::Scalar(ScalarType::String),
674            TypeModifier::Required,
675        ));
676        ct.add_field(make_field(
677            "city",
678            FieldType::Scalar(ScalarType::String),
679            TypeModifier::Required,
680        ));
681
682        assert_eq!(ct.fields.len(), 2);
683    }
684
685    #[test]
686    fn test_composite_type_get_field() {
687        let mut ct = CompositeType::new(make_ident("Address"), make_span());
688        ct.add_field(make_field(
689            "city",
690            FieldType::Scalar(ScalarType::String),
691            TypeModifier::Required,
692        ));
693
694        let field = ct.get_field("city");
695        assert!(field.is_some());
696        assert_eq!(field.unwrap().name(), "city");
697
698        assert!(ct.get_field("country").is_none());
699    }
700
701    #[test]
702    fn test_composite_type_with_documentation() {
703        let ct = CompositeType::new(make_ident("Address"), make_span())
704            .with_documentation(Documentation::new("Mailing address", make_span()));
705
706        assert!(ct.documentation.is_some());
707    }
708
709    // ==================== View Tests ====================
710
711    #[test]
712    fn test_view_new() {
713        let view = View::new(make_ident("UserStats"), make_span());
714
715        assert_eq!(view.name(), "UserStats");
716        assert!(view.fields.is_empty());
717        assert!(view.attributes.is_empty());
718        assert!(view.documentation.is_none());
719    }
720
721    #[test]
722    fn test_view_add_field() {
723        let mut view = View::new(make_ident("UserStats"), make_span());
724        view.add_field(make_field(
725            "user_id",
726            FieldType::Scalar(ScalarType::Int),
727            TypeModifier::Required,
728        ));
729        view.add_field(make_field(
730            "post_count",
731            FieldType::Scalar(ScalarType::Int),
732            TypeModifier::Required,
733        ));
734
735        assert_eq!(view.fields.len(), 2);
736    }
737
738    #[test]
739    fn test_view_view_name_default() {
740        let view = View::new(make_ident("UserStats"), make_span());
741        assert_eq!(view.view_name(), "UserStats");
742    }
743
744    #[test]
745    fn test_view_view_name_mapped() {
746        let mut view = View::new(make_ident("UserStats"), make_span());
747        view.attributes
748            .push(make_attribute_with_string("map", "v_user_statistics"));
749
750        assert_eq!(view.view_name(), "v_user_statistics");
751    }
752
753    #[test]
754    fn test_view_with_documentation() {
755        let view = View::new(make_ident("UserStats"), make_span()).with_documentation(
756            Documentation::new("Aggregated user statistics", make_span()),
757        );
758
759        assert!(view.documentation.is_some());
760    }
761
762    // ==================== Equality Tests ====================
763
764    #[test]
765    fn test_model_equality() {
766        let model1 = Model::new(make_ident("User"), make_span());
767        let model2 = Model::new(make_ident("User"), make_span());
768
769        assert_eq!(model1, model2);
770    }
771
772    #[test]
773    fn test_model_inequality() {
774        let model1 = Model::new(make_ident("User"), make_span());
775        let model2 = Model::new(make_ident("Post"), make_span());
776
777        assert_ne!(model1, model2);
778    }
779
780    #[test]
781    fn test_enum_equality() {
782        let enum1 = Enum::new(make_ident("Role"), make_span());
783        let enum2 = Enum::new(make_ident("Role"), make_span());
784
785        assert_eq!(enum1, enum2);
786    }
787
788    #[test]
789    fn test_enum_variant_equality() {
790        let v1 = EnumVariant::new(make_ident("Admin"), make_span());
791        let v2 = EnumVariant::new(make_ident("Admin"), make_span());
792        let v3 = EnumVariant::new(make_ident("User"), make_span());
793
794        assert_eq!(v1, v2);
795        assert_ne!(v1, v3);
796    }
797
798    #[test]
799    fn test_composite_type_equality() {
800        let ct1 = CompositeType::new(make_ident("Address"), make_span());
801        let ct2 = CompositeType::new(make_ident("Address"), make_span());
802
803        assert_eq!(ct1, ct2);
804    }
805
806    #[test]
807    fn test_view_equality() {
808        let v1 = View::new(make_ident("Stats"), make_span());
809        let v2 = View::new(make_ident("Stats"), make_span());
810
811        assert_eq!(v1, v2);
812    }
813}