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    // ==================== PostgreSQL Extension Types ====================
113    // These types require the corresponding PostgreSQL extension to be enabled.
114    /// Vector type for AI/ML embeddings (requires `vector` extension).
115    /// Stores dense vectors of float4 values.
116    /// Usage: `embedding Vector(1536)` for OpenAI embeddings.
117    Vector(Option<u32>),
118    /// Half-precision vector type (requires `vector` extension).
119    /// Stores dense vectors of float2 values, using half the storage of Vector.
120    /// Usage: `embedding HalfVector(1536)` for memory-efficient embeddings.
121    HalfVector(Option<u32>),
122    /// Sparse vector type (requires `vector` extension).
123    /// Stores sparse vectors with explicit indices and values.
124    /// Usage: `features SparseVector(10000)` for high-dimensional sparse data.
125    SparseVector(Option<u32>),
126    /// Bit vector type (requires `vector` extension).
127    /// Stores binary vectors for binary quantization.
128    /// Usage: `hash Bit(256)` for binary hash storage.
129    Bit(Option<u32>),
130}
131
132impl ScalarType {
133    /// Parse a scalar type from a string.
134    /// For parameterized types like Vector(1536), use `parse_with_param`.
135    #[allow(clippy::should_implement_trait)]
136    pub fn from_str(s: &str) -> Option<Self> {
137        match s {
138            "Int" => Some(Self::Int),
139            "BigInt" => Some(Self::BigInt),
140            "Float" => Some(Self::Float),
141            "Decimal" => Some(Self::Decimal),
142            "String" => Some(Self::String),
143            "Boolean" | "Bool" => Some(Self::Boolean),
144            "DateTime" => Some(Self::DateTime),
145            "Date" => Some(Self::Date),
146            "Time" => Some(Self::Time),
147            "Json" => Some(Self::Json),
148            "Bytes" => Some(Self::Bytes),
149            "Uuid" | "UUID" => Some(Self::Uuid),
150            "Cuid" | "CUID" => Some(Self::Cuid),
151            "Cuid2" | "CUID2" => Some(Self::Cuid2),
152            "NanoId" | "NanoID" | "Nanoid" => Some(Self::NanoId),
153            "Ulid" | "ULID" => Some(Self::Ulid),
154            // Vector types without dimension
155            "Vector" => Some(Self::Vector(None)),
156            "HalfVector" | "Halfvec" => Some(Self::HalfVector(None)),
157            "SparseVector" | "Sparsevec" => Some(Self::SparseVector(None)),
158            "Bit" => Some(Self::Bit(None)),
159            _ => None,
160        }
161    }
162
163    /// Parse a scalar type with an optional dimension parameter.
164    /// Handles types like `Vector(1536)`, `HalfVector(768)`, etc.
165    pub fn parse_with_param(name: &str, param: Option<u32>) -> Option<Self> {
166        match name {
167            "Vector" => Some(Self::Vector(param)),
168            "HalfVector" | "Halfvec" => Some(Self::HalfVector(param)),
169            "SparseVector" | "Sparsevec" => Some(Self::SparseVector(param)),
170            "Bit" => Some(Self::Bit(param)),
171            _ => Self::from_str(name),
172        }
173    }
174
175    /// Get the type name as a string.
176    pub fn as_str(&self) -> &'static str {
177        match self {
178            Self::Int => "Int",
179            Self::BigInt => "BigInt",
180            Self::Float => "Float",
181            Self::Decimal => "Decimal",
182            Self::String => "String",
183            Self::Boolean => "Boolean",
184            Self::DateTime => "DateTime",
185            Self::Date => "Date",
186            Self::Time => "Time",
187            Self::Json => "Json",
188            Self::Bytes => "Bytes",
189            Self::Uuid => "Uuid",
190            Self::Cuid => "Cuid",
191            Self::Cuid2 => "Cuid2",
192            Self::NanoId => "NanoId",
193            Self::Ulid => "Ulid",
194            Self::Vector(_) => "Vector",
195            Self::HalfVector(_) => "HalfVector",
196            Self::SparseVector(_) => "SparseVector",
197            Self::Bit(_) => "Bit",
198        }
199    }
200
201    /// Check if this type is an identifier type (UUID, CUID, etc.).
202    pub fn is_id_type(&self) -> bool {
203        matches!(
204            self,
205            Self::Uuid | Self::Cuid | Self::Cuid2 | Self::NanoId | Self::Ulid
206        )
207    }
208
209    /// Check if this type requires the `vector` PostgreSQL extension.
210    pub fn requires_vector_extension(&self) -> bool {
211        matches!(
212            self,
213            Self::Vector(_) | Self::HalfVector(_) | Self::SparseVector(_) | Self::Bit(_)
214        )
215    }
216
217    /// Get the dimension for vector types, if specified.
218    pub fn dimension(&self) -> Option<u32> {
219        match self {
220            Self::Vector(d) | Self::HalfVector(d) | Self::SparseVector(d) | Self::Bit(d) => *d,
221            _ => None,
222        }
223    }
224
225    /// Get the PostgreSQL type name for this scalar type.
226    pub fn postgres_type(&self) -> String {
227        match self {
228            Self::Int => "INTEGER".to_string(),
229            Self::BigInt => "BIGINT".to_string(),
230            Self::Float => "DOUBLE PRECISION".to_string(),
231            Self::Decimal => "DECIMAL".to_string(),
232            Self::String => "TEXT".to_string(),
233            Self::Boolean => "BOOLEAN".to_string(),
234            Self::DateTime => "TIMESTAMP WITH TIME ZONE".to_string(),
235            Self::Date => "DATE".to_string(),
236            Self::Time => "TIME".to_string(),
237            Self::Json => "JSONB".to_string(),
238            Self::Bytes => "BYTEA".to_string(),
239            Self::Uuid => "UUID".to_string(),
240            Self::Cuid | Self::Cuid2 | Self::NanoId | Self::Ulid => "TEXT".to_string(),
241            Self::Vector(Some(dim)) => format!("vector({})", dim),
242            Self::Vector(None) => "vector".to_string(),
243            Self::HalfVector(Some(dim)) => format!("halfvec({})", dim),
244            Self::HalfVector(None) => "halfvec".to_string(),
245            Self::SparseVector(Some(dim)) => format!("sparsevec({})", dim),
246            Self::SparseVector(None) => "sparsevec".to_string(),
247            Self::Bit(Some(dim)) => format!("bit({})", dim),
248            Self::Bit(None) => "bit".to_string(),
249        }
250    }
251}
252
253impl std::fmt::Display for ScalarType {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(f, "{}", self.as_str())
256    }
257}
258
259/// A field type in the schema.
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261pub enum FieldType {
262    /// A scalar type (Int, String, etc.).
263    Scalar(ScalarType),
264    /// A reference to an enum defined in the schema.
265    Enum(SmolStr),
266    /// A reference to a model (relation).
267    Model(SmolStr),
268    /// A reference to a composite type.
269    Composite(SmolStr),
270    /// An unsupported/unknown type (for forward compatibility).
271    Unsupported(SmolStr),
272}
273
274impl FieldType {
275    /// Check if this is a scalar type.
276    pub fn is_scalar(&self) -> bool {
277        matches!(self, Self::Scalar(_))
278    }
279
280    /// Check if this is a relation to another model.
281    pub fn is_relation(&self) -> bool {
282        matches!(self, Self::Model(_))
283    }
284
285    /// Check if this is an enum type.
286    pub fn is_enum(&self) -> bool {
287        matches!(self, Self::Enum(_))
288    }
289
290    /// Get the type name as a string.
291    pub fn type_name(&self) -> &str {
292        match self {
293            Self::Scalar(s) => s.as_str(),
294            Self::Enum(name)
295            | Self::Model(name)
296            | Self::Composite(name)
297            | Self::Unsupported(name) => name.as_str(),
298        }
299    }
300}
301
302impl std::fmt::Display for FieldType {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        write!(f, "{}", self.type_name())
305    }
306}
307
308/// Modifier for field types.
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
310pub enum TypeModifier {
311    /// Required field (no modifier).
312    Required,
313    /// Optional field (`?` suffix).
314    Optional,
315    /// List/Array field (`[]` suffix).
316    List,
317    /// Optional list field (`[]?` suffix - rare but supported).
318    OptionalList,
319}
320
321impl TypeModifier {
322    /// Check if the field is optional.
323    pub fn is_optional(&self) -> bool {
324        matches!(self, Self::Optional | Self::OptionalList)
325    }
326
327    /// Check if the field is a list.
328    pub fn is_list(&self) -> bool {
329        matches!(self, Self::List | Self::OptionalList)
330    }
331}
332
333/// A documentation comment.
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335pub struct Documentation {
336    /// The documentation text (without `///` prefix).
337    pub text: String,
338    /// Source location.
339    pub span: Span,
340}
341
342impl Documentation {
343    /// Create new documentation.
344    pub fn new(text: impl Into<String>, span: Span) -> Self {
345        Self {
346            text: text.into(),
347            span,
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    // ==================== Span Tests ====================
357
358    #[test]
359    fn test_span_new() {
360        let span = Span::new(10, 20);
361        assert_eq!(span.start, 10);
362        assert_eq!(span.end, 20);
363    }
364
365    #[test]
366    fn test_span_len() {
367        let span = Span::new(5, 15);
368        assert_eq!(span.len(), 10);
369    }
370
371    #[test]
372    fn test_span_len_zero() {
373        let span = Span::new(10, 10);
374        assert_eq!(span.len(), 0);
375    }
376
377    #[test]
378    fn test_span_is_empty_true() {
379        let span = Span::new(5, 5);
380        assert!(span.is_empty());
381    }
382
383    #[test]
384    fn test_span_is_empty_false() {
385        let span = Span::new(5, 10);
386        assert!(!span.is_empty());
387    }
388
389    #[test]
390    fn test_span_merge_adjacent() {
391        let span1 = Span::new(0, 10);
392        let span2 = Span::new(10, 20);
393        let merged = span1.merge(span2);
394        assert_eq!(merged.start, 0);
395        assert_eq!(merged.end, 20);
396    }
397
398    #[test]
399    fn test_span_merge_overlapping() {
400        let span1 = Span::new(5, 15);
401        let span2 = Span::new(10, 25);
402        let merged = span1.merge(span2);
403        assert_eq!(merged.start, 5);
404        assert_eq!(merged.end, 25);
405    }
406
407    #[test]
408    fn test_span_merge_disjoint() {
409        let span1 = Span::new(0, 5);
410        let span2 = Span::new(20, 30);
411        let merged = span1.merge(span2);
412        assert_eq!(merged.start, 0);
413        assert_eq!(merged.end, 30);
414    }
415
416    #[test]
417    fn test_span_from_tuple() {
418        let span: Span = (10, 20).into();
419        assert_eq!(span.start, 10);
420        assert_eq!(span.end, 20);
421    }
422
423    #[test]
424    fn test_span_equality() {
425        let span1 = Span::new(10, 20);
426        let span2 = Span::new(10, 20);
427        let span3 = Span::new(10, 25);
428        assert_eq!(span1, span2);
429        assert_ne!(span1, span3);
430    }
431
432    #[test]
433    fn test_span_clone() {
434        let span1 = Span::new(10, 20);
435        let span2 = span1;
436        assert_eq!(span1, span2);
437    }
438
439    // ==================== Ident Tests ====================
440
441    #[test]
442    fn test_ident_new() {
443        let ident = Ident::new("user_id", Span::new(0, 7));
444        assert_eq!(ident.name.as_str(), "user_id");
445        assert_eq!(ident.span.start, 0);
446        assert_eq!(ident.span.end, 7);
447    }
448
449    #[test]
450    fn test_ident_as_str() {
451        let ident = Ident::new("field_name", Span::new(0, 10));
452        assert_eq!(ident.as_str(), "field_name");
453    }
454
455    #[test]
456    fn test_ident_display() {
457        let ident = Ident::new("MyModel", Span::new(0, 7));
458        assert_eq!(format!("{}", ident), "MyModel");
459    }
460
461    #[test]
462    fn test_ident_equality() {
463        let ident1 = Ident::new("name", Span::new(0, 4));
464        let ident2 = Ident::new("name", Span::new(0, 4));
465        let ident3 = Ident::new("name", Span::new(5, 9));
466        let ident4 = Ident::new("other", Span::new(0, 5));
467
468        assert_eq!(ident1, ident2);
469        assert_ne!(ident1, ident3); // different span
470        assert_ne!(ident1, ident4); // different name
471    }
472
473    #[test]
474    fn test_ident_from_string() {
475        let name = String::from("dynamic_name");
476        let ident = Ident::new(name, Span::new(0, 12));
477        assert_eq!(ident.as_str(), "dynamic_name");
478    }
479
480    // ==================== ScalarType Tests ====================
481
482    #[test]
483    fn test_scalar_type_from_str_int() {
484        assert_eq!(ScalarType::from_str("Int"), Some(ScalarType::Int));
485    }
486
487    #[test]
488    fn test_scalar_type_from_str_bigint() {
489        assert_eq!(ScalarType::from_str("BigInt"), Some(ScalarType::BigInt));
490    }
491
492    #[test]
493    fn test_scalar_type_from_str_float() {
494        assert_eq!(ScalarType::from_str("Float"), Some(ScalarType::Float));
495    }
496
497    #[test]
498    fn test_scalar_type_from_str_decimal() {
499        assert_eq!(ScalarType::from_str("Decimal"), Some(ScalarType::Decimal));
500    }
501
502    #[test]
503    fn test_scalar_type_from_str_string() {
504        assert_eq!(ScalarType::from_str("String"), Some(ScalarType::String));
505    }
506
507    #[test]
508    fn test_scalar_type_from_str_boolean() {
509        assert_eq!(ScalarType::from_str("Boolean"), Some(ScalarType::Boolean));
510    }
511
512    #[test]
513    fn test_scalar_type_from_str_bool_alias() {
514        assert_eq!(ScalarType::from_str("Bool"), Some(ScalarType::Boolean));
515    }
516
517    #[test]
518    fn test_scalar_type_from_str_datetime() {
519        assert_eq!(ScalarType::from_str("DateTime"), Some(ScalarType::DateTime));
520    }
521
522    #[test]
523    fn test_scalar_type_from_str_date() {
524        assert_eq!(ScalarType::from_str("Date"), Some(ScalarType::Date));
525    }
526
527    #[test]
528    fn test_scalar_type_from_str_time() {
529        assert_eq!(ScalarType::from_str("Time"), Some(ScalarType::Time));
530    }
531
532    #[test]
533    fn test_scalar_type_from_str_json() {
534        assert_eq!(ScalarType::from_str("Json"), Some(ScalarType::Json));
535    }
536
537    #[test]
538    fn test_scalar_type_from_str_bytes() {
539        assert_eq!(ScalarType::from_str("Bytes"), Some(ScalarType::Bytes));
540    }
541
542    #[test]
543    fn test_scalar_type_from_str_uuid() {
544        assert_eq!(ScalarType::from_str("Uuid"), Some(ScalarType::Uuid));
545    }
546
547    #[test]
548    fn test_scalar_type_from_str_uuid_uppercase() {
549        assert_eq!(ScalarType::from_str("UUID"), Some(ScalarType::Uuid));
550    }
551
552    #[test]
553    fn test_scalar_type_from_str_cuid() {
554        assert_eq!(ScalarType::from_str("Cuid"), Some(ScalarType::Cuid));
555        assert_eq!(ScalarType::from_str("CUID"), Some(ScalarType::Cuid));
556    }
557
558    #[test]
559    fn test_scalar_type_from_str_cuid2() {
560        assert_eq!(ScalarType::from_str("Cuid2"), Some(ScalarType::Cuid2));
561        assert_eq!(ScalarType::from_str("CUID2"), Some(ScalarType::Cuid2));
562    }
563
564    #[test]
565    fn test_scalar_type_from_str_nanoid() {
566        assert_eq!(ScalarType::from_str("NanoId"), Some(ScalarType::NanoId));
567        assert_eq!(ScalarType::from_str("NanoID"), Some(ScalarType::NanoId));
568        assert_eq!(ScalarType::from_str("Nanoid"), Some(ScalarType::NanoId));
569    }
570
571    #[test]
572    fn test_scalar_type_from_str_ulid() {
573        assert_eq!(ScalarType::from_str("Ulid"), Some(ScalarType::Ulid));
574        assert_eq!(ScalarType::from_str("ULID"), Some(ScalarType::Ulid));
575    }
576
577    #[test]
578    fn test_scalar_type_from_str_unknown() {
579        assert_eq!(ScalarType::from_str("Unknown"), None);
580        assert_eq!(ScalarType::from_str("int"), None); // case sensitive
581        assert_eq!(ScalarType::from_str(""), None);
582    }
583
584    #[test]
585    fn test_scalar_type_as_str() {
586        assert_eq!(ScalarType::Int.as_str(), "Int");
587        assert_eq!(ScalarType::BigInt.as_str(), "BigInt");
588        assert_eq!(ScalarType::Float.as_str(), "Float");
589        assert_eq!(ScalarType::Decimal.as_str(), "Decimal");
590        assert_eq!(ScalarType::String.as_str(), "String");
591        assert_eq!(ScalarType::Boolean.as_str(), "Boolean");
592        assert_eq!(ScalarType::DateTime.as_str(), "DateTime");
593        assert_eq!(ScalarType::Date.as_str(), "Date");
594        assert_eq!(ScalarType::Time.as_str(), "Time");
595        assert_eq!(ScalarType::Json.as_str(), "Json");
596        assert_eq!(ScalarType::Bytes.as_str(), "Bytes");
597        assert_eq!(ScalarType::Uuid.as_str(), "Uuid");
598        assert_eq!(ScalarType::Cuid.as_str(), "Cuid");
599        assert_eq!(ScalarType::Cuid2.as_str(), "Cuid2");
600        assert_eq!(ScalarType::NanoId.as_str(), "NanoId");
601        assert_eq!(ScalarType::Ulid.as_str(), "Ulid");
602    }
603
604    #[test]
605    fn test_scalar_type_is_id_type() {
606        assert!(ScalarType::Uuid.is_id_type());
607        assert!(ScalarType::Cuid.is_id_type());
608        assert!(ScalarType::Cuid2.is_id_type());
609        assert!(ScalarType::NanoId.is_id_type());
610        assert!(ScalarType::Ulid.is_id_type());
611        assert!(!ScalarType::Int.is_id_type());
612        assert!(!ScalarType::String.is_id_type());
613    }
614
615    #[test]
616    fn test_scalar_type_display() {
617        assert_eq!(format!("{}", ScalarType::Int), "Int");
618        assert_eq!(format!("{}", ScalarType::String), "String");
619        assert_eq!(format!("{}", ScalarType::DateTime), "DateTime");
620    }
621
622    #[test]
623    fn test_scalar_type_equality() {
624        assert_eq!(ScalarType::Int, ScalarType::Int);
625        assert_ne!(ScalarType::Int, ScalarType::String);
626    }
627
628    // ==================== FieldType Tests ====================
629
630    #[test]
631    fn test_field_type_scalar() {
632        let ft = FieldType::Scalar(ScalarType::Int);
633        assert!(ft.is_scalar());
634        assert!(!ft.is_relation());
635        assert!(!ft.is_enum());
636        assert_eq!(ft.type_name(), "Int");
637    }
638
639    #[test]
640    fn test_field_type_enum() {
641        let ft = FieldType::Enum("Role".into());
642        assert!(!ft.is_scalar());
643        assert!(!ft.is_relation());
644        assert!(ft.is_enum());
645        assert_eq!(ft.type_name(), "Role");
646    }
647
648    #[test]
649    fn test_field_type_model() {
650        let ft = FieldType::Model("User".into());
651        assert!(!ft.is_scalar());
652        assert!(ft.is_relation());
653        assert!(!ft.is_enum());
654        assert_eq!(ft.type_name(), "User");
655    }
656
657    #[test]
658    fn test_field_type_composite() {
659        let ft = FieldType::Composite("Address".into());
660        assert!(!ft.is_scalar());
661        assert!(!ft.is_relation());
662        assert!(!ft.is_enum());
663        assert_eq!(ft.type_name(), "Address");
664    }
665
666    #[test]
667    fn test_field_type_unsupported() {
668        let ft = FieldType::Unsupported("CustomType".into());
669        assert!(!ft.is_scalar());
670        assert!(!ft.is_relation());
671        assert!(!ft.is_enum());
672        assert_eq!(ft.type_name(), "CustomType");
673    }
674
675    #[test]
676    fn test_field_type_display() {
677        assert_eq!(
678            format!("{}", FieldType::Scalar(ScalarType::String)),
679            "String"
680        );
681        assert_eq!(format!("{}", FieldType::Enum("Status".into())), "Status");
682        assert_eq!(format!("{}", FieldType::Model("Post".into())), "Post");
683    }
684
685    #[test]
686    fn test_field_type_equality() {
687        let ft1 = FieldType::Scalar(ScalarType::Int);
688        let ft2 = FieldType::Scalar(ScalarType::Int);
689        let ft3 = FieldType::Scalar(ScalarType::String);
690
691        assert_eq!(ft1, ft2);
692        assert_ne!(ft1, ft3);
693    }
694
695    // ==================== TypeModifier Tests ====================
696
697    #[test]
698    fn test_type_modifier_required() {
699        let tm = TypeModifier::Required;
700        assert!(!tm.is_optional());
701        assert!(!tm.is_list());
702    }
703
704    #[test]
705    fn test_type_modifier_optional() {
706        let tm = TypeModifier::Optional;
707        assert!(tm.is_optional());
708        assert!(!tm.is_list());
709    }
710
711    #[test]
712    fn test_type_modifier_list() {
713        let tm = TypeModifier::List;
714        assert!(!tm.is_optional());
715        assert!(tm.is_list());
716    }
717
718    #[test]
719    fn test_type_modifier_optional_list() {
720        let tm = TypeModifier::OptionalList;
721        assert!(tm.is_optional());
722        assert!(tm.is_list());
723    }
724
725    #[test]
726    fn test_type_modifier_equality() {
727        assert_eq!(TypeModifier::Required, TypeModifier::Required);
728        assert_eq!(TypeModifier::Optional, TypeModifier::Optional);
729        assert_ne!(TypeModifier::Required, TypeModifier::Optional);
730    }
731
732    // ==================== Documentation Tests ====================
733
734    #[test]
735    fn test_documentation_new() {
736        let doc = Documentation::new("This is a doc comment", Span::new(0, 21));
737        assert_eq!(doc.text, "This is a doc comment");
738        assert_eq!(doc.span.start, 0);
739        assert_eq!(doc.span.end, 21);
740    }
741
742    #[test]
743    fn test_documentation_from_string() {
744        let text = String::from("Dynamic documentation");
745        let doc = Documentation::new(text, Span::new(0, 21));
746        assert_eq!(doc.text, "Dynamic documentation");
747    }
748
749    #[test]
750    fn test_documentation_equality() {
751        let doc1 = Documentation::new("Same text", Span::new(0, 9));
752        let doc2 = Documentation::new("Same text", Span::new(0, 9));
753        let doc3 = Documentation::new("Different", Span::new(0, 9));
754
755        assert_eq!(doc1, doc2);
756        assert_ne!(doc1, doc3);
757    }
758
759    #[test]
760    fn test_documentation_multiline() {
761        let doc = Documentation::new("Line 1\nLine 2\nLine 3", Span::new(0, 20));
762        assert!(doc.text.contains('\n'));
763        assert!(doc.text.starts_with("Line 1"));
764        assert!(doc.text.ends_with("Line 3"));
765    }
766}