prax_schema/ast/
types.rs

1//! Type definitions for the Prax schema AST.
2
3use serde::{Deserialize, Serialize};
4use smol_str::SmolStr;
5
6/// A span in the source code for error reporting.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Span {
9    /// Start offset in bytes.
10    pub start: usize,
11    /// End offset in bytes.
12    pub end: usize,
13}
14
15impl Span {
16    /// Create a new span.
17    pub fn new(start: usize, end: usize) -> Self {
18        Self { start, end }
19    }
20
21    /// Get the length of the span.
22    pub fn len(&self) -> usize {
23        self.end - self.start
24    }
25
26    /// Check if the span is empty.
27    pub fn is_empty(&self) -> bool {
28        self.start == self.end
29    }
30
31    /// Merge two spans into one that covers both.
32    pub fn merge(self, other: Span) -> Span {
33        Span {
34            start: self.start.min(other.start),
35            end: self.end.max(other.end),
36        }
37    }
38}
39
40impl From<(usize, usize)> for Span {
41    fn from((start, end): (usize, usize)) -> Self {
42        Self { start, end }
43    }
44}
45
46/// An identifier with source location.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Ident {
49    /// The identifier name.
50    pub name: SmolStr,
51    /// Source location.
52    pub span: Span,
53}
54
55impl Ident {
56    /// Create a new identifier.
57    pub fn new(name: impl Into<SmolStr>, span: Span) -> Self {
58        Self {
59            name: name.into(),
60            span,
61        }
62    }
63
64    /// Get the name as a string slice.
65    pub fn as_str(&self) -> &str {
66        &self.name
67    }
68}
69
70impl std::fmt::Display for Ident {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}", self.name)
73    }
74}
75
76/// Scalar types supported by Prax.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub enum ScalarType {
79    /// Integer type (maps to INT/INTEGER).
80    Int,
81    /// Big integer type (maps to BIGINT).
82    BigInt,
83    /// Floating point type (maps to FLOAT/REAL).
84    Float,
85    /// Decimal type for precise calculations (maps to DECIMAL/NUMERIC).
86    Decimal,
87    /// String type (maps to VARCHAR/TEXT).
88    String,
89    /// Boolean type.
90    Boolean,
91    /// Date and time type.
92    DateTime,
93    /// Date only type.
94    Date,
95    /// Time only type.
96    Time,
97    /// JSON type.
98    Json,
99    /// Binary/Bytes type.
100    Bytes,
101    /// UUID type (128-bit universally unique identifier).
102    Uuid,
103    /// CUID type (collision-resistant unique identifier).
104    Cuid,
105    /// CUID2 type (next generation collision-resistant unique identifier).
106    Cuid2,
107    /// NanoID type (URL-friendly unique identifier).
108    NanoId,
109    /// ULID type (Universally Unique Lexicographically Sortable Identifier).
110    Ulid,
111}
112
113impl ScalarType {
114    /// Parse a scalar type from a string.
115    #[allow(clippy::should_implement_trait)]
116    pub fn from_str(s: &str) -> Option<Self> {
117        match s {
118            "Int" => Some(Self::Int),
119            "BigInt" => Some(Self::BigInt),
120            "Float" => Some(Self::Float),
121            "Decimal" => Some(Self::Decimal),
122            "String" => Some(Self::String),
123            "Boolean" | "Bool" => Some(Self::Boolean),
124            "DateTime" => Some(Self::DateTime),
125            "Date" => Some(Self::Date),
126            "Time" => Some(Self::Time),
127            "Json" => Some(Self::Json),
128            "Bytes" => Some(Self::Bytes),
129            "Uuid" | "UUID" => Some(Self::Uuid),
130            "Cuid" | "CUID" => Some(Self::Cuid),
131            "Cuid2" | "CUID2" => Some(Self::Cuid2),
132            "NanoId" | "NanoID" | "Nanoid" => Some(Self::NanoId),
133            "Ulid" | "ULID" => Some(Self::Ulid),
134            _ => None,
135        }
136    }
137
138    /// Get the type name as a string.
139    pub fn as_str(&self) -> &'static str {
140        match self {
141            Self::Int => "Int",
142            Self::BigInt => "BigInt",
143            Self::Float => "Float",
144            Self::Decimal => "Decimal",
145            Self::String => "String",
146            Self::Boolean => "Boolean",
147            Self::DateTime => "DateTime",
148            Self::Date => "Date",
149            Self::Time => "Time",
150            Self::Json => "Json",
151            Self::Bytes => "Bytes",
152            Self::Uuid => "Uuid",
153            Self::Cuid => "Cuid",
154            Self::Cuid2 => "Cuid2",
155            Self::NanoId => "NanoId",
156            Self::Ulid => "Ulid",
157        }
158    }
159
160    /// Check if this type is an identifier type (UUID, CUID, etc.).
161    pub fn is_id_type(&self) -> bool {
162        matches!(
163            self,
164            Self::Uuid | Self::Cuid | Self::Cuid2 | Self::NanoId | Self::Ulid
165        )
166    }
167}
168
169impl std::fmt::Display for ScalarType {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        write!(f, "{}", self.as_str())
172    }
173}
174
175/// A field type in the schema.
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub enum FieldType {
178    /// A scalar type (Int, String, etc.).
179    Scalar(ScalarType),
180    /// A reference to an enum defined in the schema.
181    Enum(SmolStr),
182    /// A reference to a model (relation).
183    Model(SmolStr),
184    /// A reference to a composite type.
185    Composite(SmolStr),
186    /// An unsupported/unknown type (for forward compatibility).
187    Unsupported(SmolStr),
188}
189
190impl FieldType {
191    /// Check if this is a scalar type.
192    pub fn is_scalar(&self) -> bool {
193        matches!(self, Self::Scalar(_))
194    }
195
196    /// Check if this is a relation to another model.
197    pub fn is_relation(&self) -> bool {
198        matches!(self, Self::Model(_))
199    }
200
201    /// Check if this is an enum type.
202    pub fn is_enum(&self) -> bool {
203        matches!(self, Self::Enum(_))
204    }
205
206    /// Get the type name as a string.
207    pub fn type_name(&self) -> &str {
208        match self {
209            Self::Scalar(s) => s.as_str(),
210            Self::Enum(name)
211            | Self::Model(name)
212            | Self::Composite(name)
213            | Self::Unsupported(name) => name.as_str(),
214        }
215    }
216}
217
218impl std::fmt::Display for FieldType {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        write!(f, "{}", self.type_name())
221    }
222}
223
224/// Modifier for field types.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226pub enum TypeModifier {
227    /// Required field (no modifier).
228    Required,
229    /// Optional field (`?` suffix).
230    Optional,
231    /// List/Array field (`[]` suffix).
232    List,
233    /// Optional list field (`[]?` suffix - rare but supported).
234    OptionalList,
235}
236
237impl TypeModifier {
238    /// Check if the field is optional.
239    pub fn is_optional(&self) -> bool {
240        matches!(self, Self::Optional | Self::OptionalList)
241    }
242
243    /// Check if the field is a list.
244    pub fn is_list(&self) -> bool {
245        matches!(self, Self::List | Self::OptionalList)
246    }
247}
248
249/// A documentation comment.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub struct Documentation {
252    /// The documentation text (without `///` prefix).
253    pub text: String,
254    /// Source location.
255    pub span: Span,
256}
257
258impl Documentation {
259    /// Create new documentation.
260    pub fn new(text: impl Into<String>, span: Span) -> Self {
261        Self {
262            text: text.into(),
263            span,
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    // ==================== Span Tests ====================
273
274    #[test]
275    fn test_span_new() {
276        let span = Span::new(10, 20);
277        assert_eq!(span.start, 10);
278        assert_eq!(span.end, 20);
279    }
280
281    #[test]
282    fn test_span_len() {
283        let span = Span::new(5, 15);
284        assert_eq!(span.len(), 10);
285    }
286
287    #[test]
288    fn test_span_len_zero() {
289        let span = Span::new(10, 10);
290        assert_eq!(span.len(), 0);
291    }
292
293    #[test]
294    fn test_span_is_empty_true() {
295        let span = Span::new(5, 5);
296        assert!(span.is_empty());
297    }
298
299    #[test]
300    fn test_span_is_empty_false() {
301        let span = Span::new(5, 10);
302        assert!(!span.is_empty());
303    }
304
305    #[test]
306    fn test_span_merge_adjacent() {
307        let span1 = Span::new(0, 10);
308        let span2 = Span::new(10, 20);
309        let merged = span1.merge(span2);
310        assert_eq!(merged.start, 0);
311        assert_eq!(merged.end, 20);
312    }
313
314    #[test]
315    fn test_span_merge_overlapping() {
316        let span1 = Span::new(5, 15);
317        let span2 = Span::new(10, 25);
318        let merged = span1.merge(span2);
319        assert_eq!(merged.start, 5);
320        assert_eq!(merged.end, 25);
321    }
322
323    #[test]
324    fn test_span_merge_disjoint() {
325        let span1 = Span::new(0, 5);
326        let span2 = Span::new(20, 30);
327        let merged = span1.merge(span2);
328        assert_eq!(merged.start, 0);
329        assert_eq!(merged.end, 30);
330    }
331
332    #[test]
333    fn test_span_from_tuple() {
334        let span: Span = (10, 20).into();
335        assert_eq!(span.start, 10);
336        assert_eq!(span.end, 20);
337    }
338
339    #[test]
340    fn test_span_equality() {
341        let span1 = Span::new(10, 20);
342        let span2 = Span::new(10, 20);
343        let span3 = Span::new(10, 25);
344        assert_eq!(span1, span2);
345        assert_ne!(span1, span3);
346    }
347
348    #[test]
349    fn test_span_clone() {
350        let span1 = Span::new(10, 20);
351        let span2 = span1;
352        assert_eq!(span1, span2);
353    }
354
355    // ==================== Ident Tests ====================
356
357    #[test]
358    fn test_ident_new() {
359        let ident = Ident::new("user_id", Span::new(0, 7));
360        assert_eq!(ident.name.as_str(), "user_id");
361        assert_eq!(ident.span.start, 0);
362        assert_eq!(ident.span.end, 7);
363    }
364
365    #[test]
366    fn test_ident_as_str() {
367        let ident = Ident::new("field_name", Span::new(0, 10));
368        assert_eq!(ident.as_str(), "field_name");
369    }
370
371    #[test]
372    fn test_ident_display() {
373        let ident = Ident::new("MyModel", Span::new(0, 7));
374        assert_eq!(format!("{}", ident), "MyModel");
375    }
376
377    #[test]
378    fn test_ident_equality() {
379        let ident1 = Ident::new("name", Span::new(0, 4));
380        let ident2 = Ident::new("name", Span::new(0, 4));
381        let ident3 = Ident::new("name", Span::new(5, 9));
382        let ident4 = Ident::new("other", Span::new(0, 5));
383
384        assert_eq!(ident1, ident2);
385        assert_ne!(ident1, ident3); // different span
386        assert_ne!(ident1, ident4); // different name
387    }
388
389    #[test]
390    fn test_ident_from_string() {
391        let name = String::from("dynamic_name");
392        let ident = Ident::new(name, Span::new(0, 12));
393        assert_eq!(ident.as_str(), "dynamic_name");
394    }
395
396    // ==================== ScalarType Tests ====================
397
398    #[test]
399    fn test_scalar_type_from_str_int() {
400        assert_eq!(ScalarType::from_str("Int"), Some(ScalarType::Int));
401    }
402
403    #[test]
404    fn test_scalar_type_from_str_bigint() {
405        assert_eq!(ScalarType::from_str("BigInt"), Some(ScalarType::BigInt));
406    }
407
408    #[test]
409    fn test_scalar_type_from_str_float() {
410        assert_eq!(ScalarType::from_str("Float"), Some(ScalarType::Float));
411    }
412
413    #[test]
414    fn test_scalar_type_from_str_decimal() {
415        assert_eq!(ScalarType::from_str("Decimal"), Some(ScalarType::Decimal));
416    }
417
418    #[test]
419    fn test_scalar_type_from_str_string() {
420        assert_eq!(ScalarType::from_str("String"), Some(ScalarType::String));
421    }
422
423    #[test]
424    fn test_scalar_type_from_str_boolean() {
425        assert_eq!(ScalarType::from_str("Boolean"), Some(ScalarType::Boolean));
426    }
427
428    #[test]
429    fn test_scalar_type_from_str_bool_alias() {
430        assert_eq!(ScalarType::from_str("Bool"), Some(ScalarType::Boolean));
431    }
432
433    #[test]
434    fn test_scalar_type_from_str_datetime() {
435        assert_eq!(ScalarType::from_str("DateTime"), Some(ScalarType::DateTime));
436    }
437
438    #[test]
439    fn test_scalar_type_from_str_date() {
440        assert_eq!(ScalarType::from_str("Date"), Some(ScalarType::Date));
441    }
442
443    #[test]
444    fn test_scalar_type_from_str_time() {
445        assert_eq!(ScalarType::from_str("Time"), Some(ScalarType::Time));
446    }
447
448    #[test]
449    fn test_scalar_type_from_str_json() {
450        assert_eq!(ScalarType::from_str("Json"), Some(ScalarType::Json));
451    }
452
453    #[test]
454    fn test_scalar_type_from_str_bytes() {
455        assert_eq!(ScalarType::from_str("Bytes"), Some(ScalarType::Bytes));
456    }
457
458    #[test]
459    fn test_scalar_type_from_str_uuid() {
460        assert_eq!(ScalarType::from_str("Uuid"), Some(ScalarType::Uuid));
461    }
462
463    #[test]
464    fn test_scalar_type_from_str_uuid_uppercase() {
465        assert_eq!(ScalarType::from_str("UUID"), Some(ScalarType::Uuid));
466    }
467
468    #[test]
469    fn test_scalar_type_from_str_cuid() {
470        assert_eq!(ScalarType::from_str("Cuid"), Some(ScalarType::Cuid));
471        assert_eq!(ScalarType::from_str("CUID"), Some(ScalarType::Cuid));
472    }
473
474    #[test]
475    fn test_scalar_type_from_str_cuid2() {
476        assert_eq!(ScalarType::from_str("Cuid2"), Some(ScalarType::Cuid2));
477        assert_eq!(ScalarType::from_str("CUID2"), Some(ScalarType::Cuid2));
478    }
479
480    #[test]
481    fn test_scalar_type_from_str_nanoid() {
482        assert_eq!(ScalarType::from_str("NanoId"), Some(ScalarType::NanoId));
483        assert_eq!(ScalarType::from_str("NanoID"), Some(ScalarType::NanoId));
484        assert_eq!(ScalarType::from_str("Nanoid"), Some(ScalarType::NanoId));
485    }
486
487    #[test]
488    fn test_scalar_type_from_str_ulid() {
489        assert_eq!(ScalarType::from_str("Ulid"), Some(ScalarType::Ulid));
490        assert_eq!(ScalarType::from_str("ULID"), Some(ScalarType::Ulid));
491    }
492
493    #[test]
494    fn test_scalar_type_from_str_unknown() {
495        assert_eq!(ScalarType::from_str("Unknown"), None);
496        assert_eq!(ScalarType::from_str("int"), None); // case sensitive
497        assert_eq!(ScalarType::from_str(""), None);
498    }
499
500    #[test]
501    fn test_scalar_type_as_str() {
502        assert_eq!(ScalarType::Int.as_str(), "Int");
503        assert_eq!(ScalarType::BigInt.as_str(), "BigInt");
504        assert_eq!(ScalarType::Float.as_str(), "Float");
505        assert_eq!(ScalarType::Decimal.as_str(), "Decimal");
506        assert_eq!(ScalarType::String.as_str(), "String");
507        assert_eq!(ScalarType::Boolean.as_str(), "Boolean");
508        assert_eq!(ScalarType::DateTime.as_str(), "DateTime");
509        assert_eq!(ScalarType::Date.as_str(), "Date");
510        assert_eq!(ScalarType::Time.as_str(), "Time");
511        assert_eq!(ScalarType::Json.as_str(), "Json");
512        assert_eq!(ScalarType::Bytes.as_str(), "Bytes");
513        assert_eq!(ScalarType::Uuid.as_str(), "Uuid");
514        assert_eq!(ScalarType::Cuid.as_str(), "Cuid");
515        assert_eq!(ScalarType::Cuid2.as_str(), "Cuid2");
516        assert_eq!(ScalarType::NanoId.as_str(), "NanoId");
517        assert_eq!(ScalarType::Ulid.as_str(), "Ulid");
518    }
519
520    #[test]
521    fn test_scalar_type_is_id_type() {
522        assert!(ScalarType::Uuid.is_id_type());
523        assert!(ScalarType::Cuid.is_id_type());
524        assert!(ScalarType::Cuid2.is_id_type());
525        assert!(ScalarType::NanoId.is_id_type());
526        assert!(ScalarType::Ulid.is_id_type());
527        assert!(!ScalarType::Int.is_id_type());
528        assert!(!ScalarType::String.is_id_type());
529    }
530
531    #[test]
532    fn test_scalar_type_display() {
533        assert_eq!(format!("{}", ScalarType::Int), "Int");
534        assert_eq!(format!("{}", ScalarType::String), "String");
535        assert_eq!(format!("{}", ScalarType::DateTime), "DateTime");
536    }
537
538    #[test]
539    fn test_scalar_type_equality() {
540        assert_eq!(ScalarType::Int, ScalarType::Int);
541        assert_ne!(ScalarType::Int, ScalarType::String);
542    }
543
544    // ==================== FieldType Tests ====================
545
546    #[test]
547    fn test_field_type_scalar() {
548        let ft = FieldType::Scalar(ScalarType::Int);
549        assert!(ft.is_scalar());
550        assert!(!ft.is_relation());
551        assert!(!ft.is_enum());
552        assert_eq!(ft.type_name(), "Int");
553    }
554
555    #[test]
556    fn test_field_type_enum() {
557        let ft = FieldType::Enum("Role".into());
558        assert!(!ft.is_scalar());
559        assert!(!ft.is_relation());
560        assert!(ft.is_enum());
561        assert_eq!(ft.type_name(), "Role");
562    }
563
564    #[test]
565    fn test_field_type_model() {
566        let ft = FieldType::Model("User".into());
567        assert!(!ft.is_scalar());
568        assert!(ft.is_relation());
569        assert!(!ft.is_enum());
570        assert_eq!(ft.type_name(), "User");
571    }
572
573    #[test]
574    fn test_field_type_composite() {
575        let ft = FieldType::Composite("Address".into());
576        assert!(!ft.is_scalar());
577        assert!(!ft.is_relation());
578        assert!(!ft.is_enum());
579        assert_eq!(ft.type_name(), "Address");
580    }
581
582    #[test]
583    fn test_field_type_unsupported() {
584        let ft = FieldType::Unsupported("CustomType".into());
585        assert!(!ft.is_scalar());
586        assert!(!ft.is_relation());
587        assert!(!ft.is_enum());
588        assert_eq!(ft.type_name(), "CustomType");
589    }
590
591    #[test]
592    fn test_field_type_display() {
593        assert_eq!(
594            format!("{}", FieldType::Scalar(ScalarType::String)),
595            "String"
596        );
597        assert_eq!(format!("{}", FieldType::Enum("Status".into())), "Status");
598        assert_eq!(format!("{}", FieldType::Model("Post".into())), "Post");
599    }
600
601    #[test]
602    fn test_field_type_equality() {
603        let ft1 = FieldType::Scalar(ScalarType::Int);
604        let ft2 = FieldType::Scalar(ScalarType::Int);
605        let ft3 = FieldType::Scalar(ScalarType::String);
606
607        assert_eq!(ft1, ft2);
608        assert_ne!(ft1, ft3);
609    }
610
611    // ==================== TypeModifier Tests ====================
612
613    #[test]
614    fn test_type_modifier_required() {
615        let tm = TypeModifier::Required;
616        assert!(!tm.is_optional());
617        assert!(!tm.is_list());
618    }
619
620    #[test]
621    fn test_type_modifier_optional() {
622        let tm = TypeModifier::Optional;
623        assert!(tm.is_optional());
624        assert!(!tm.is_list());
625    }
626
627    #[test]
628    fn test_type_modifier_list() {
629        let tm = TypeModifier::List;
630        assert!(!tm.is_optional());
631        assert!(tm.is_list());
632    }
633
634    #[test]
635    fn test_type_modifier_optional_list() {
636        let tm = TypeModifier::OptionalList;
637        assert!(tm.is_optional());
638        assert!(tm.is_list());
639    }
640
641    #[test]
642    fn test_type_modifier_equality() {
643        assert_eq!(TypeModifier::Required, TypeModifier::Required);
644        assert_eq!(TypeModifier::Optional, TypeModifier::Optional);
645        assert_ne!(TypeModifier::Required, TypeModifier::Optional);
646    }
647
648    // ==================== Documentation Tests ====================
649
650    #[test]
651    fn test_documentation_new() {
652        let doc = Documentation::new("This is a doc comment", Span::new(0, 21));
653        assert_eq!(doc.text, "This is a doc comment");
654        assert_eq!(doc.span.start, 0);
655        assert_eq!(doc.span.end, 21);
656    }
657
658    #[test]
659    fn test_documentation_from_string() {
660        let text = String::from("Dynamic documentation");
661        let doc = Documentation::new(text, Span::new(0, 21));
662        assert_eq!(doc.text, "Dynamic documentation");
663    }
664
665    #[test]
666    fn test_documentation_equality() {
667        let doc1 = Documentation::new("Same text", Span::new(0, 9));
668        let doc2 = Documentation::new("Same text", Span::new(0, 9));
669        let doc3 = Documentation::new("Different", Span::new(0, 9));
670
671        assert_eq!(doc1, doc2);
672        assert_ne!(doc1, doc3);
673    }
674
675    #[test]
676    fn test_documentation_multiline() {
677        let doc = Documentation::new("Line 1\nLine 2\nLine 3", Span::new(0, 20));
678        assert!(doc.text.contains('\n'));
679        assert!(doc.text.starts_with("Line 1"));
680        assert!(doc.text.ends_with("Line 3"));
681    }
682}