Skip to main content

common/
enum_variant_parser.rs

1/// Parser and utilities for complex Rust enum variants in qleany manifests.
2///
3/// Supports three variant kinds:
4/// - Simple:  `Active`
5/// - Tuple:   `Text(String)` or `Pair(i64, Option<String>)`
6/// - Struct:  `Image { name: String, width: i64, quality: Option<f64> }`
7///
8/// Inner types use final Rust types: `bool`, `i32`, `i64`, `u32`, `u64`,
9/// `f32`, `f64`, `String`, etc. Three shorthands expand to qualified paths:
10/// `Uuid` → `uuid::Uuid`, `DateTime` → `chrono::DateTime<chrono::Utc>`,
11/// `EntityId` → `EntityId`.
12/// `Option<T>` and `Vec<T>` wrappers are supported.
13/// Any other PascalCase name is treated as an enum reference (used as-is).
14use anyhow::{Result, anyhow};
15
16// ─────────────────────────────────────────────────────────────────────────────
17// Known Rust scalar types accepted inside complex variants
18// ─────────────────────────────────────────────────────────────────────────────
19
20const RUST_SCALARS: &[&str] = &[
21    "bool", "i8", "i16", "i32", "i64", "i128", "u8", "u16", "u32", "u64", "u128", "f32", "f64",
22    "isize", "usize", "String",
23];
24
25fn is_rust_scalar(s: &str) -> bool {
26    RUST_SCALARS.contains(&s)
27}
28
29// ─────────────────────────────────────────────────────────────────────────────
30// Types
31// ─────────────────────────────────────────────────────────────────────────────
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum VariantFieldType {
35    /// A known Rust scalar type, emitted as-is (e.g. `"i64"`, `"String"`, `"bool"`).
36    Scalar(std::string::String),
37    /// Shorthand for `uuid::Uuid`.
38    Uuid,
39    /// Shorthand for `chrono::DateTime<chrono::Utc>`.
40    DateTime,
41    /// The `EntityId` type (alias for `u64`).
42    EntityId,
43    /// A PascalCase enum name — emitted as-is in generated code.
44    EnumRef(std::string::String),
45    /// `Option<T>`
46    Option(Box<VariantFieldType>),
47    /// `Vec<T>`
48    Vec(Box<VariantFieldType>),
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub enum EnumVariantKind {
53    Simple,
54    Tuple(Vec<VariantFieldType>),
55    Struct(Vec<(std::string::String, VariantFieldType)>),
56}
57
58#[derive(Debug, Clone)]
59pub struct ParsedEnumVariant {
60    pub name: std::string::String,
61    pub kind: EnumVariantKind,
62}
63
64// ─────────────────────────────────────────────────────────────────────────────
65// Parsing
66// ─────────────────────────────────────────────────────────────────────────────
67
68/// Parse a single enum variant string like `"Active"`, `"Text(String)"`,
69/// or `"Image { name: String, width: i64 }"`.
70pub fn parse_enum_variant(raw: &str) -> Result<ParsedEnumVariant> {
71    let raw = raw.trim();
72    if raw.is_empty() {
73        return Err(anyhow!("Empty enum variant"));
74    }
75
76    // Find where variant name ends (first '(' or '{')
77    let name_end = raw.find(['(', '{']).unwrap_or(raw.len());
78    let name = raw[..name_end].trim().to_string();
79
80    if name.is_empty() {
81        return Err(anyhow!("Enum variant name is empty"));
82    }
83
84    let rest = raw[name_end..].trim();
85
86    if rest.is_empty() {
87        return Ok(ParsedEnumVariant {
88            name,
89            kind: EnumVariantKind::Simple,
90        });
91    }
92
93    if rest.starts_with('(') {
94        let close = find_matching(rest, '(', ')')
95            .map_err(|_| anyhow!("Unmatched '(' in variant '{}'", name))?;
96        let trailing = rest[close + 1..].trim();
97        if !trailing.is_empty() {
98            return Err(anyhow!(
99                "Unexpected characters after ')' in variant '{}': {}",
100                name,
101                trailing
102            ));
103        }
104        let inner = rest[1..close].trim();
105        if inner.is_empty() {
106            return Err(anyhow!("Empty tuple in variant '{}'", name));
107        }
108        let fields = parse_comma_separated_types(inner)?;
109        Ok(ParsedEnumVariant {
110            name,
111            kind: EnumVariantKind::Tuple(fields),
112        })
113    } else if rest.starts_with('{') {
114        let close = find_matching(rest, '{', '}')
115            .map_err(|_| anyhow!("Unmatched '{{' in variant '{}'", name))?;
116        let trailing = rest[close + 1..].trim();
117        if !trailing.is_empty() {
118            return Err(anyhow!(
119                "Unexpected characters after '}}' in variant '{}': {}",
120                name,
121                trailing
122            ));
123        }
124        let inner = rest[1..close].trim();
125        if inner.is_empty() {
126            return Err(anyhow!("Empty struct in variant '{}'", name));
127        }
128        let fields = parse_comma_separated_named_fields(inner)?;
129        Ok(ParsedEnumVariant {
130            name,
131            kind: EnumVariantKind::Struct(fields),
132        })
133    } else {
134        Err(anyhow!(
135            "Unexpected characters after variant name '{}': {}",
136            name,
137            rest
138        ))
139    }
140}
141
142/// Find the index of the matching closing delimiter, respecting nested `<>`.
143fn find_matching(s: &str, open: char, close: char) -> Result<usize> {
144    let mut depth: i32 = 0;
145    let mut angle: i32 = 0;
146
147    for (i, c) in s.char_indices() {
148        if c == open && angle == 0 {
149            depth += 1;
150        } else if c == close && angle == 0 {
151            depth -= 1;
152            if depth == 0 {
153                return Ok(i);
154            }
155        } else if c == '<' {
156            angle += 1;
157        } else if c == '>' {
158            angle -= 1;
159        }
160    }
161    Err(anyhow!("Unmatched '{}'", open))
162}
163
164/// Split a string by commas, respecting `<>` nesting.
165fn split_respecting_angles(s: &str) -> Vec<std::string::String> {
166    let mut result = Vec::new();
167    let mut current = std::string::String::new();
168    let mut angle: i32 = 0;
169
170    for c in s.chars() {
171        match c {
172            '<' => {
173                angle += 1;
174                current.push(c);
175            }
176            '>' => {
177                angle -= 1;
178                current.push(c);
179            }
180            ',' if angle == 0 => {
181                let trimmed = current.trim().to_string();
182                if !trimmed.is_empty() {
183                    result.push(trimmed);
184                }
185                current.clear();
186            }
187            _ => {
188                current.push(c);
189            }
190        }
191    }
192    let trimmed = current.trim().to_string();
193    if !trimmed.is_empty() {
194        result.push(trimmed);
195    }
196    result
197}
198
199/// Parse a type string like `"i64"`, `"Option<String>"`, `"Vec<u32>"`, `"Uuid"`.
200fn parse_type(s: &str) -> Result<VariantFieldType> {
201    let s = s.trim();
202    if s.is_empty() {
203        return Err(anyhow!("Empty type"));
204    }
205
206    // Option<T> wrapper
207    if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
208        let inner_type = parse_type(inner)?;
209        return Ok(VariantFieldType::Option(Box::new(inner_type)));
210    }
211    // Vec<T> wrapper
212    if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
213        let inner_type = parse_type(inner)?;
214        return Ok(VariantFieldType::Vec(Box::new(inner_type)));
215    }
216
217    // Shorthands
218    match s {
219        "Uuid" => return Ok(VariantFieldType::Uuid),
220        "DateTime" => return Ok(VariantFieldType::DateTime),
221        "EntityId" => return Ok(VariantFieldType::EntityId),
222        _ => {}
223    }
224
225    // Known Rust scalar types
226    if is_rust_scalar(s) {
227        return Ok(VariantFieldType::Scalar(s.to_string()));
228    }
229
230    // Anything else must be a valid identifier — treated as an enum reference
231    if s.chars().all(|c| c.is_alphanumeric() || c == '_')
232        && s.chars().next().is_some_and(|c| c.is_alphabetic())
233    {
234        Ok(VariantFieldType::EnumRef(s.to_string()))
235    } else {
236        Err(anyhow!(
237            "Unknown type '{}': expected a Rust type (bool, i32, i64, u64, f64, String, ...), \
238             a shorthand (Uuid, DateTime, EntityId), or a PascalCase enum name",
239            s
240        ))
241    }
242}
243
244/// Parse comma-separated types: `"i64, String, Option<f64>"`
245fn parse_comma_separated_types(s: &str) -> Result<Vec<VariantFieldType>> {
246    let parts = split_respecting_angles(s);
247    let mut result = Vec::new();
248    for part in &parts {
249        result.push(parse_type(part)?);
250    }
251    Ok(result)
252}
253
254/// Parse comma-separated named fields: `"name: String, width: i64"`
255fn parse_comma_separated_named_fields(
256    s: &str,
257) -> Result<Vec<(std::string::String, VariantFieldType)>> {
258    let parts = split_respecting_angles(s);
259    let mut result = Vec::new();
260    for part in &parts {
261        let colon_pos = part
262            .find(':')
263            .ok_or_else(|| anyhow!("Struct field '{}' missing ':' separator", part))?;
264        let field_name = part[..colon_pos].trim().to_string();
265        let field_type_str = part[colon_pos + 1..].trim();
266        if field_name.is_empty() {
267            return Err(anyhow!("Empty field name in struct variant"));
268        }
269        let field_type = parse_type(field_type_str)?;
270        result.push((field_name, field_type));
271    }
272    Ok(result)
273}
274
275// ─────────────────────────────────────────────────────────────────────────────
276// Utility: collect references, check flags
277// ─────────────────────────────────────────────────────────────────────────────
278
279/// Collect all EnumRef names from a parsed variant.
280pub fn collect_references(variant: &ParsedEnumVariant) -> Vec<std::string::String> {
281    let mut refs = Vec::new();
282    match &variant.kind {
283        EnumVariantKind::Simple => {}
284        EnumVariantKind::Tuple(fields) => {
285            for f in fields {
286                collect_type_references(f, &mut refs);
287            }
288        }
289        EnumVariantKind::Struct(fields) => {
290            for (_, f) in fields {
291                collect_type_references(f, &mut refs);
292            }
293        }
294    }
295    refs
296}
297
298fn collect_type_references(vft: &VariantFieldType, out: &mut Vec<std::string::String>) {
299    match vft {
300        VariantFieldType::EnumRef(name) => out.push(name.clone()),
301        VariantFieldType::Option(inner) | VariantFieldType::Vec(inner) => {
302            collect_type_references(inner, out);
303        }
304        _ => {}
305    }
306}
307
308/// Check if a variant type tree contains Uuid anywhere.
309pub fn type_needs_uuid(vft: &VariantFieldType) -> bool {
310    match vft {
311        VariantFieldType::Uuid => true,
312        VariantFieldType::Option(inner) | VariantFieldType::Vec(inner) => type_needs_uuid(inner),
313        _ => false,
314    }
315}
316
317/// Check if a variant type tree contains DateTime anywhere.
318pub fn type_needs_chrono(vft: &VariantFieldType) -> bool {
319    match vft {
320        VariantFieldType::DateTime => true,
321        VariantFieldType::Option(inner) | VariantFieldType::Vec(inner) => type_needs_chrono(inner),
322        _ => false,
323    }
324}
325
326/// Check if a variant type tree contains EntityId anywhere.
327pub fn type_needs_entity_id(vft: &VariantFieldType) -> bool {
328    match vft {
329        VariantFieldType::EntityId => true,
330        VariantFieldType::Option(inner) | VariantFieldType::Vec(inner) => {
331            type_needs_entity_id(inner)
332        }
333        _ => false,
334    }
335}
336
337/// Check if a variant type tree contains a float type (f32/f64) anywhere.
338pub fn type_needs_float(vft: &VariantFieldType) -> bool {
339    match vft {
340        VariantFieldType::Scalar(s) => s == "f32" || s == "f64",
341        VariantFieldType::Option(inner) | VariantFieldType::Vec(inner) => type_needs_float(inner),
342        _ => false,
343    }
344}
345
346/// Check if a whole variant needs uuid/chrono/entity_id imports.
347pub fn variant_needs_uuid(variant: &ParsedEnumVariant) -> bool {
348    variant_fields_iter(&variant.kind).any(type_needs_uuid)
349}
350
351pub fn variant_needs_chrono(variant: &ParsedEnumVariant) -> bool {
352    variant_fields_iter(&variant.kind).any(type_needs_chrono)
353}
354
355pub fn variant_needs_entity_id(variant: &ParsedEnumVariant) -> bool {
356    variant_fields_iter(&variant.kind).any(type_needs_entity_id)
357}
358
359pub fn variant_needs_float(variant: &ParsedEnumVariant) -> bool {
360    variant_fields_iter(&variant.kind).any(type_needs_float)
361}
362
363fn variant_fields_iter(kind: &EnumVariantKind) -> Box<dyn Iterator<Item = &VariantFieldType> + '_> {
364    match kind {
365        EnumVariantKind::Simple => Box::new(std::iter::empty()),
366        EnumVariantKind::Tuple(fields) => Box::new(fields.iter()),
367        EnumVariantKind::Struct(fields) => Box::new(fields.iter().map(|(_, f)| f)),
368    }
369}
370
371// ─────────────────────────────────────────────────────────────────────────────
372// Rust type mapping (mostly identity — only shorthands expand)
373// ─────────────────────────────────────────────────────────────────────────────
374
375/// Convert a VariantFieldType to its Rust type string.
376/// Scalars and enum refs pass through; shorthands expand to qualified paths.
377pub fn type_to_rust(vft: &VariantFieldType) -> std::string::String {
378    match vft {
379        VariantFieldType::Scalar(s) => s.clone(),
380        VariantFieldType::Uuid => "uuid::Uuid".to_string(),
381        VariantFieldType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
382        VariantFieldType::EntityId => "EntityId".to_string(),
383        VariantFieldType::EnumRef(name) => name.clone(),
384        VariantFieldType::Option(inner) => {
385            format!("Option<{}>", type_to_rust(inner))
386        }
387        VariantFieldType::Vec(inner) => {
388            format!("Vec<{}>", type_to_rust(inner))
389        }
390    }
391}
392
393/// Convert an entire variant to its Rust definition line.
394/// E.g. `"Text(String)"`, `"Image { name: String, width: i64 }"`, `"Active"`.
395pub fn variant_to_rust_line(variant: &ParsedEnumVariant) -> std::string::String {
396    match &variant.kind {
397        EnumVariantKind::Simple => variant.name.clone(),
398        EnumVariantKind::Tuple(fields) => {
399            let types: Vec<std::string::String> = fields.iter().map(type_to_rust).collect();
400            format!("{}({})", variant.name, types.join(", "))
401        }
402        EnumVariantKind::Struct(fields) => {
403            let field_strs: Vec<std::string::String> = fields
404                .iter()
405                .map(|(name, typ)| format!("{}: {}", name, type_to_rust(typ)))
406                .collect();
407            format!("{} {{ {} }}", variant.name, field_strs.join(", "))
408        }
409    }
410}
411
412// ─────────────────────────────────────────────────────────────────────────────
413// Mobile type mapping (for UniFFI bridge)
414// ─────────────────────────────────────────────────────────────────────────────
415
416/// Convert a VariantFieldType to its mobile Rust type string.
417/// EntityId → u64, Uuid → String, DateTime → MobileDateTime, EnumRef → Mobile{Name}.
418pub fn type_to_mobile_rust(vft: &VariantFieldType) -> std::string::String {
419    match vft {
420        VariantFieldType::Scalar(s) => s.clone(),
421        VariantFieldType::Uuid => "String".to_string(),
422        VariantFieldType::DateTime => "MobileDateTime".to_string(),
423        VariantFieldType::EntityId => "u64".to_string(),
424        VariantFieldType::EnumRef(name) => format!("Mobile{}", name),
425        VariantFieldType::Option(inner) => {
426            format!("Option<{}>", type_to_mobile_rust(inner))
427        }
428        VariantFieldType::Vec(inner) => {
429            format!("Vec<{}>", type_to_mobile_rust(inner))
430        }
431    }
432}
433
434/// Convert an entire variant to its mobile definition line.
435pub fn variant_to_mobile_line(variant: &ParsedEnumVariant) -> std::string::String {
436    match &variant.kind {
437        EnumVariantKind::Simple => variant.name.clone(),
438        EnumVariantKind::Tuple(fields) => {
439            let types: Vec<std::string::String> = fields.iter().map(type_to_mobile_rust).collect();
440            format!("{}({})", variant.name, types.join(", "))
441        }
442        EnumVariantKind::Struct(fields) => {
443            let field_strs: Vec<std::string::String> = fields
444                .iter()
445                .map(|(name, typ)| format!("{}: {}", name, type_to_mobile_rust(typ)))
446                .collect();
447            format!("{} {{ {} }}", variant.name, field_strs.join(", "))
448        }
449    }
450}
451
452// ─────────────────────────────────────────────────────────────────────────────
453// Match pattern and conversion expressions (for From impls)
454// ─────────────────────────────────────────────────────────────────────────────
455
456/// Generate the destructuring match pattern for a variant.
457/// E.g. `"Active"`, `"Text(v0)"`, `"Image { name, width }"`.
458pub fn variant_match_pattern(variant: &ParsedEnumVariant) -> std::string::String {
459    match &variant.kind {
460        EnumVariantKind::Simple => variant.name.clone(),
461        EnumVariantKind::Tuple(fields) => {
462            let vars: Vec<std::string::String> =
463                (0..fields.len()).map(|i| format!("v{}", i)).collect();
464            format!("{}({})", variant.name, vars.join(", "))
465        }
466        EnumVariantKind::Struct(fields) => {
467            let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
468            format!("{} {{ {} }}", variant.name, names.join(", "))
469        }
470    }
471}
472
473/// Generate the forwarding construction for mobile-to-core From impl.
474/// Handles type conversions for Uuid (String → uuid::Uuid) and DateTime (MobileDateTime → chrono).
475pub fn variant_mobile_to_core_construct(variant: &ParsedEnumVariant) -> std::string::String {
476    match &variant.kind {
477        EnumVariantKind::Simple => variant.name.clone(),
478        EnumVariantKind::Tuple(fields) => {
479            let args: Vec<std::string::String> = fields
480                .iter()
481                .enumerate()
482                .map(|(i, f)| mobile_to_core_expr(&format!("v{}", i), f))
483                .collect();
484            format!("{}({})", variant.name, args.join(", "))
485        }
486        EnumVariantKind::Struct(fields) => {
487            let args: Vec<std::string::String> = fields
488                .iter()
489                .map(|(name, f)| {
490                    let expr = mobile_to_core_expr(name, f);
491                    if expr == *name {
492                        name.clone()
493                    } else {
494                        format!("{}: {}", name, expr)
495                    }
496                })
497                .collect();
498            format!("{} {{ {} }}", variant.name, args.join(", "))
499        }
500    }
501}
502
503/// Generate the forwarding construction for core-to-mobile From impl.
504pub fn variant_core_to_mobile_construct(variant: &ParsedEnumVariant) -> std::string::String {
505    match &variant.kind {
506        EnumVariantKind::Simple => variant.name.clone(),
507        EnumVariantKind::Tuple(fields) => {
508            let args: Vec<std::string::String> = fields
509                .iter()
510                .enumerate()
511                .map(|(i, f)| core_to_mobile_expr(&format!("v{}", i), f))
512                .collect();
513            format!("{}({})", variant.name, args.join(", "))
514        }
515        EnumVariantKind::Struct(fields) => {
516            let args: Vec<std::string::String> = fields
517                .iter()
518                .map(|(name, f)| {
519                    let expr = core_to_mobile_expr(name, f);
520                    if expr == *name {
521                        name.clone()
522                    } else {
523                        format!("{}: {}", name, expr)
524                    }
525                })
526                .collect();
527            format!("{} {{ {} }}", variant.name, args.join(", "))
528        }
529    }
530}
531
532/// Expression to convert a mobile value to core Rust type.
533fn mobile_to_core_expr(var: &str, vft: &VariantFieldType) -> std::string::String {
534    match vft {
535        VariantFieldType::Uuid => {
536            format!("uuid::Uuid::parse_str(&{}).unwrap_or_default()", var)
537        }
538        VariantFieldType::DateTime => format!("{}.0", var),
539        VariantFieldType::EnumRef(_) => {
540            format!("{}.into()", var)
541        }
542        VariantFieldType::Option(inner) => {
543            let inner_expr = mobile_to_core_expr("x", inner);
544            if inner_expr == "x" {
545                var.to_string()
546            } else {
547                format!("{}.map(|x| {})", var, inner_expr)
548            }
549        }
550        VariantFieldType::Vec(inner) => {
551            let inner_expr = mobile_to_core_expr("x", inner);
552            if inner_expr == "x" {
553                var.to_string()
554            } else {
555                format!("{}.into_iter().map(|x| {}).collect()", var, inner_expr)
556            }
557        }
558        _ => var.to_string(), // identity for Scalar and EntityId
559    }
560}
561
562/// Expression to convert a core Rust value to mobile type.
563fn core_to_mobile_expr(var: &str, vft: &VariantFieldType) -> std::string::String {
564    match vft {
565        VariantFieldType::Uuid => format!("{}.to_string()", var),
566        VariantFieldType::DateTime => format!("MobileDateTime({})", var),
567        VariantFieldType::EnumRef(_) => {
568            format!("{}.into()", var)
569        }
570        VariantFieldType::Option(inner) => {
571            let inner_expr = core_to_mobile_expr("x", inner);
572            if inner_expr == "x" {
573                var.to_string()
574            } else {
575                format!("{}.map(|x| {})", var, inner_expr)
576            }
577        }
578        VariantFieldType::Vec(inner) => {
579            let inner_expr = core_to_mobile_expr("x", inner);
580            if inner_expr == "x" {
581                var.to_string()
582            } else {
583                format!("{}.into_iter().map(|x| {}).collect()", var, inner_expr)
584            }
585        }
586        _ => var.to_string(),
587    }
588}
589
590// ─────────────────────────────────────────────────────────────────────────────
591// Tests
592// ─────────────────────────────────────────────────────────────────────────────
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    #[test]
599    fn test_parse_simple_variant() {
600        let v = parse_enum_variant("Active").unwrap();
601        assert_eq!(v.name, "Active");
602        assert_eq!(v.kind, EnumVariantKind::Simple);
603    }
604
605    #[test]
606    fn test_parse_tuple_variant_single() {
607        let v = parse_enum_variant("Text(String)").unwrap();
608        assert_eq!(v.name, "Text");
609        assert_eq!(
610            v.kind,
611            EnumVariantKind::Tuple(vec![VariantFieldType::Scalar("String".to_string())])
612        );
613    }
614
615    #[test]
616    fn test_parse_tuple_variant_multi() {
617        let v = parse_enum_variant("Pair(i64, String)").unwrap();
618        assert_eq!(v.name, "Pair");
619        assert_eq!(
620            v.kind,
621            EnumVariantKind::Tuple(vec![
622                VariantFieldType::Scalar("i64".to_string()),
623                VariantFieldType::Scalar("String".to_string()),
624            ])
625        );
626    }
627
628    #[test]
629    fn test_parse_struct_variant() {
630        let v = parse_enum_variant("Image { name: String, width: i64 }").unwrap();
631        assert_eq!(v.name, "Image");
632        assert_eq!(
633            v.kind,
634            EnumVariantKind::Struct(vec![
635                (
636                    "name".to_string(),
637                    VariantFieldType::Scalar("String".to_string())
638                ),
639                (
640                    "width".to_string(),
641                    VariantFieldType::Scalar("i64".to_string())
642                ),
643            ])
644        );
645    }
646
647    #[test]
648    fn test_parse_option_type() {
649        let v = parse_enum_variant("Note(Option<String>)").unwrap();
650        assert_eq!(
651            v.kind,
652            EnumVariantKind::Tuple(vec![VariantFieldType::Option(Box::new(
653                VariantFieldType::Scalar("String".to_string())
654            ))])
655        );
656    }
657
658    #[test]
659    fn test_parse_vec_type() {
660        let v = parse_enum_variant("Items(Vec<i32>)").unwrap();
661        assert_eq!(
662            v.kind,
663            EnumVariantKind::Tuple(vec![VariantFieldType::Vec(Box::new(
664                VariantFieldType::Scalar("i32".to_string())
665            ))])
666        );
667    }
668
669    #[test]
670    fn test_parse_option_vec() {
671        let v = parse_enum_variant("Data(Option<Vec<String>>)").unwrap();
672        assert_eq!(
673            v.kind,
674            EnumVariantKind::Tuple(vec![VariantFieldType::Option(Box::new(
675                VariantFieldType::Vec(Box::new(VariantFieldType::Scalar("String".to_string())))
676            ))])
677        );
678    }
679
680    #[test]
681    fn test_parse_shorthands() {
682        let v = parse_enum_variant("Stamped(Uuid, DateTime, EntityId)").unwrap();
683        assert_eq!(
684            v.kind,
685            EnumVariantKind::Tuple(vec![
686                VariantFieldType::Uuid,
687                VariantFieldType::DateTime,
688                VariantFieldType::EntityId,
689            ])
690        );
691    }
692
693    #[test]
694    fn test_parse_enum_reference() {
695        let v = parse_enum_variant("Tagged(ProjectStatus)").unwrap();
696        assert_eq!(
697            v.kind,
698            EnumVariantKind::Tuple(vec![VariantFieldType::EnumRef("ProjectStatus".to_string())])
699        );
700    }
701
702    #[test]
703    fn test_unmatched_paren() {
704        assert!(parse_enum_variant("Bad(String").is_err());
705    }
706
707    #[test]
708    fn test_unmatched_brace() {
709        assert!(parse_enum_variant("Bad { name: String").is_err());
710    }
711
712    #[test]
713    fn test_empty_tuple() {
714        assert!(parse_enum_variant("Bad()").is_err());
715    }
716
717    #[test]
718    fn test_empty_struct() {
719        assert!(parse_enum_variant("Bad {}").is_err());
720    }
721
722    #[test]
723    fn test_missing_colon_in_struct() {
724        assert!(parse_enum_variant("Bad { name String }").is_err());
725    }
726
727    #[test]
728    fn test_unknown_type_rejected() {
729        // Types with special chars are rejected
730        assert!(parse_enum_variant("Bad(foo::bar)").is_err());
731    }
732
733    #[test]
734    fn test_variant_to_rust_line_simple() {
735        let v = parse_enum_variant("Active").unwrap();
736        assert_eq!(variant_to_rust_line(&v), "Active");
737    }
738
739    #[test]
740    fn test_variant_to_rust_line_tuple() {
741        let v = parse_enum_variant("Text(i64)").unwrap();
742        assert_eq!(variant_to_rust_line(&v), "Text(i64)");
743    }
744
745    #[test]
746    fn test_variant_to_rust_shorthand_expansion() {
747        let v = parse_enum_variant("Stamped(Uuid, DateTime)").unwrap();
748        assert_eq!(
749            variant_to_rust_line(&v),
750            "Stamped(uuid::Uuid, chrono::DateTime<chrono::Utc>)"
751        );
752    }
753
754    #[test]
755    fn test_variant_to_rust_line_struct() {
756        let v = parse_enum_variant("Image { name: String, width: i64 }").unwrap();
757        assert_eq!(
758            variant_to_rust_line(&v),
759            "Image { name: String, width: i64 }"
760        );
761    }
762
763    #[test]
764    fn test_variant_to_rust_enum_ref() {
765        let v = parse_enum_variant("Tagged(ProjectStatus)").unwrap();
766        assert_eq!(variant_to_rust_line(&v), "Tagged(ProjectStatus)");
767    }
768
769    #[test]
770    fn test_match_pattern() {
771        let v = parse_enum_variant("Image { name: String, width: i64 }").unwrap();
772        assert_eq!(variant_match_pattern(&v), "Image { name, width }");
773
774        let v2 = parse_enum_variant("Text(String, i64)").unwrap();
775        assert_eq!(variant_match_pattern(&v2), "Text(v0, v1)");
776
777        let v3 = parse_enum_variant("Active").unwrap();
778        assert_eq!(variant_match_pattern(&v3), "Active");
779    }
780
781    #[test]
782    fn test_collect_references() {
783        let v = parse_enum_variant("Mixed(ProjectStatus, Option<TaskDifficulty>)").unwrap();
784        let refs = collect_references(&v);
785        assert_eq!(refs, vec!["ProjectStatus", "TaskDifficulty"]);
786    }
787
788    #[test]
789    fn test_variant_needs_flags() {
790        let v = parse_enum_variant("Data(Uuid, DateTime)").unwrap();
791        assert!(variant_needs_uuid(&v));
792        assert!(variant_needs_chrono(&v));
793
794        let v2 = parse_enum_variant("Simple(String)").unwrap();
795        assert!(!variant_needs_uuid(&v2));
796        assert!(!variant_needs_chrono(&v2));
797
798        let v3 = parse_enum_variant("HasId(EntityId)").unwrap();
799        assert!(variant_needs_entity_id(&v3));
800    }
801}