Skip to main content

alef_codegen/conversions/
helpers.rs

1use ahash::{AHashMap, AHashSet};
2use alef_core::ir::{ApiSurface, EnumDef, FieldDef, PrimitiveType, TypeDef, TypeRef};
3
4/// Collect all Named type names that appear as function/method input parameters.
5/// These are types that flow from the binding layer INTO the core — they need
6/// binding→core `From` impls. Types that only appear as return values do NOT
7/// need reverse conversions.
8///
9/// The result includes transitive dependencies: if `CrawlConfig` is an input
10/// and it has a field `browser: BrowserConfig`, then `BrowserConfig` is also
11/// included.
12pub fn input_type_names(surface: &ApiSurface) -> AHashSet<String> {
13    let mut names = AHashSet::new();
14
15    // Collect Named types from function params
16    for func in &surface.functions {
17        for param in &func.params {
18            collect_named_types(&param.ty, &mut names);
19        }
20    }
21    // Collect Named types from method params
22    for typ in &surface.types {
23        for method in &typ.methods {
24            for param in &method.params {
25                collect_named_types(&param.ty, &mut names);
26            }
27        }
28    }
29
30    // Transitive closure: if type A is an input and has field of type B, B is also an input
31    let mut changed = true;
32    while changed {
33        changed = false;
34        let snapshot: Vec<String> = names.iter().cloned().collect();
35        for name in &snapshot {
36            if let Some(typ) = surface.types.iter().find(|t| t.name == *name) {
37                for field in &typ.fields {
38                    let mut field_names = AHashSet::new();
39                    collect_named_types(&field.ty, &mut field_names);
40                    for n in field_names {
41                        if names.insert(n) {
42                            changed = true;
43                        }
44                    }
45                }
46            }
47        }
48    }
49
50    names
51}
52
53/// Recursively collect all `Named(name)` from a TypeRef.
54fn collect_named_types(ty: &TypeRef, out: &mut AHashSet<String>) {
55    match ty {
56        TypeRef::Named(name) => {
57            out.insert(name.clone());
58        }
59        TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_named_types(inner, out),
60        TypeRef::Map(k, v) => {
61            collect_named_types(k, out);
62            collect_named_types(v, out);
63        }
64        _ => {}
65    }
66}
67
68/// Check if a TypeRef references a Named type that is in the exclude list.
69/// Used to skip fields whose types were excluded from binding generation,
70/// preventing references to non-existent wrapper types (e.g. Js* in WASM).
71pub fn field_references_excluded_type(ty: &TypeRef, exclude_types: &[String]) -> bool {
72    match ty {
73        TypeRef::Named(name) => exclude_types.iter().any(|e| e == name),
74        TypeRef::Optional(inner) | TypeRef::Vec(inner) => field_references_excluded_type(inner, exclude_types),
75        TypeRef::Map(k, v) => {
76            field_references_excluded_type(k, exclude_types) || field_references_excluded_type(v, exclude_types)
77        }
78        _ => false,
79    }
80}
81
82/// Returns true if a primitive type needs i64 casting (NAPI/PHP — JS/PHP lack native u64).
83pub(crate) fn needs_i64_cast(p: &PrimitiveType) -> bool {
84    matches!(p, PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize)
85}
86
87/// Returns the core primitive type string for cast primitives.
88pub(crate) fn core_prim_str(p: &PrimitiveType) -> &'static str {
89    match p {
90        PrimitiveType::U64 => "u64",
91        PrimitiveType::Usize => "usize",
92        PrimitiveType::Isize => "isize",
93        PrimitiveType::F32 => "f32",
94        PrimitiveType::Bool => "bool",
95        PrimitiveType::U8 => "u8",
96        PrimitiveType::U16 => "u16",
97        PrimitiveType::U32 => "u32",
98        PrimitiveType::I8 => "i8",
99        PrimitiveType::I16 => "i16",
100        PrimitiveType::I32 => "i32",
101        PrimitiveType::I64 => "i64",
102        PrimitiveType::F64 => "f64",
103    }
104}
105
106/// Returns the binding primitive type string for cast primitives (core→binding direction).
107pub(crate) fn binding_prim_str(p: &PrimitiveType) -> &'static str {
108    match p {
109        PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "i64",
110        PrimitiveType::F32 => "f64",
111        PrimitiveType::Bool => "bool",
112        PrimitiveType::U8 | PrimitiveType::U16 | PrimitiveType::U32 => "i32",
113        PrimitiveType::I8 | PrimitiveType::I16 | PrimitiveType::I32 => "i32",
114        PrimitiveType::I64 => "i64",
115        PrimitiveType::F64 => "f64",
116    }
117}
118
119/// Build the set of types that can have core→binding From safely generated.
120/// More permissive than binding→core: allows sanitized fields (uses format!("{:?}"))
121/// and accepts data enums (data discarded with `..` in match arms).
122pub fn core_to_binding_convertible_types(surface: &ApiSurface) -> AHashSet<String> {
123    let convertible_enums: AHashSet<&str> = surface
124        .enums
125        .iter()
126        .filter(|e| can_generate_enum_conversion_from_core(e))
127        .map(|e| e.name.as_str())
128        .collect();
129
130    let opaque_type_names: AHashSet<&str> = surface
131        .types
132        .iter()
133        .filter(|t| t.is_opaque)
134        .map(|t| t.name.as_str())
135        .collect();
136
137    // Build rust_path maps for detecting type_rust_path mismatches.
138    let (enum_paths, type_paths) = build_rust_path_maps(surface);
139
140    // All non-opaque types are candidates (sanitized fields use format!("{:?}"))
141    let mut convertible: AHashSet<String> = surface
142        .types
143        .iter()
144        .filter(|t| !t.is_opaque)
145        .map(|t| t.name.clone())
146        .collect();
147
148    let mut changed = true;
149    while changed {
150        changed = false;
151        let snapshot: Vec<String> = convertible.iter().cloned().collect();
152        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
153        known.extend(&opaque_type_names);
154        let mut to_remove = Vec::new();
155        for type_name in &snapshot {
156            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
157                let ok = typ.fields.iter().all(|f| {
158                    if f.sanitized {
159                        true
160                    } else if field_has_path_mismatch(f, &enum_paths, &type_paths) {
161                        false
162                    } else {
163                        is_field_convertible(&f.ty, &convertible_enums, &known)
164                    }
165                });
166                if !ok {
167                    to_remove.push(type_name.clone());
168                }
169            }
170        }
171        for name in to_remove {
172            if convertible.remove(&name) {
173                changed = true;
174            }
175        }
176    }
177    convertible
178}
179
180/// Build the set of types that can have binding→core From safely generated.
181/// Strict: excludes types with sanitized fields (lossy conversion).
182/// This is transitive: a type is convertible only if all its Named field types
183/// are also convertible (or are enums with From/Into support).
184pub fn convertible_types(surface: &ApiSurface) -> AHashSet<String> {
185    // Build set of enums that have From/Into impls (unit-variant enums only)
186    let convertible_enums: AHashSet<&str> = surface
187        .enums
188        .iter()
189        .filter(|e| can_generate_enum_conversion(e))
190        .map(|e| e.name.as_str())
191        .collect();
192
193    // Build set of all known type names (including opaques) — opaque Named fields
194    // are convertible because we wrap/unwrap them via Arc.
195    let _all_type_names: AHashSet<&str> = surface.types.iter().map(|t| t.name.as_str()).collect();
196
197    // Build set of Named types that implement Default — sanitized fields referencing
198    // Named types without Default would cause a compile error in the generated From impl.
199    let default_type_names: AHashSet<&str> = surface
200        .types
201        .iter()
202        .filter(|t| t.has_default)
203        .map(|t| t.name.as_str())
204        .collect();
205
206    // Start with all non-opaque types as candidates.
207    // Types with sanitized fields use Default::default() for the sanitized field
208    // in the binding→core direction — but only if the field type implements Default.
209    let mut convertible: AHashSet<String> = surface
210        .types
211        .iter()
212        .filter(|t| !t.is_opaque)
213        .map(|t| t.name.clone())
214        .collect();
215
216    // Set of opaque type names — Named fields referencing opaques are always convertible
217    // (they use Arc wrap/unwrap), so include them in the known-types check.
218    let opaque_type_names: AHashSet<&str> = surface
219        .types
220        .iter()
221        .filter(|t| t.is_opaque)
222        .map(|t| t.name.as_str())
223        .collect();
224
225    // Build rust_path maps for detecting type_rust_path mismatches.
226    let (enum_paths, type_paths) = build_rust_path_maps(surface);
227
228    // Iteratively remove types whose fields reference non-convertible Named types.
229    // We check against `convertible ∪ opaque_types` so that types referencing
230    // excluded types (e.g. types with sanitized fields) are transitively removed,
231    // while opaque Named fields remain valid.
232    let mut changed = true;
233    while changed {
234        changed = false;
235        let snapshot: Vec<String> = convertible.iter().cloned().collect();
236        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
237        known.extend(&opaque_type_names);
238        let mut to_remove = Vec::new();
239        for type_name in &snapshot {
240            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
241                let ok = typ.fields.iter().all(|f| {
242                    if f.sanitized {
243                        sanitized_field_has_default(&f.ty, &default_type_names)
244                    } else if field_has_path_mismatch(f, &enum_paths, &type_paths) {
245                        false
246                    } else {
247                        is_field_convertible(&f.ty, &convertible_enums, &known)
248                    }
249                });
250                if !ok {
251                    to_remove.push(type_name.clone());
252                }
253            }
254        }
255        for name in to_remove {
256            if convertible.remove(&name) {
257                changed = true;
258            }
259        }
260    }
261    convertible
262}
263
264/// Check if a sanitized field's type can produce a valid `Default::default()` expression.
265/// Primitive types, strings, collections, Options, and Named types with `has_default` are fine.
266/// Named types without `has_default` are not — generating `Default::default()` for them would
267/// fail to compile.
268fn sanitized_field_has_default(ty: &TypeRef, default_types: &AHashSet<&str>) -> bool {
269    match ty {
270        TypeRef::Primitive(_)
271        | TypeRef::String
272        | TypeRef::Char
273        | TypeRef::Bytes
274        | TypeRef::Path
275        | TypeRef::Unit
276        | TypeRef::Duration
277        | TypeRef::Json => true,
278        // Option<T> defaults to None regardless of T
279        TypeRef::Optional(_) => true,
280        // Vec<T> defaults to empty vec regardless of T
281        TypeRef::Vec(_) => true,
282        // Map<K, V> defaults to empty map regardless of K/V
283        TypeRef::Map(_, _) => true,
284        TypeRef::Named(name) => {
285            if is_tuple_type_name(name) {
286                // Tuple types are always passthrough
287                true
288            } else {
289                // Named type must have has_default to be safely used via Default::default()
290                default_types.contains(name.as_str())
291            }
292        }
293    }
294}
295
296/// Check if a specific type is in the convertible set.
297pub fn can_generate_conversion(typ: &TypeDef, convertible: &AHashSet<String>) -> bool {
298    convertible.contains(&typ.name)
299}
300
301pub(crate) fn is_field_convertible(
302    ty: &TypeRef,
303    convertible_enums: &AHashSet<&str>,
304    known_types: &AHashSet<&str>,
305) -> bool {
306    match ty {
307        TypeRef::Primitive(_)
308        | TypeRef::String
309        | TypeRef::Char
310        | TypeRef::Bytes
311        | TypeRef::Path
312        | TypeRef::Unit
313        | TypeRef::Duration => true,
314        TypeRef::Json => true,
315        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_field_convertible(inner, convertible_enums, known_types),
316        TypeRef::Map(k, v) => {
317            is_field_convertible(k, convertible_enums, known_types)
318                && is_field_convertible(v, convertible_enums, known_types)
319        }
320        // Tuple types are passthrough — always convertible
321        TypeRef::Named(name) if is_tuple_type_name(name) => true,
322        // Unit-variant enums and known types (including opaques, which use Arc wrap/unwrap) are convertible.
323        TypeRef::Named(name) => convertible_enums.contains(name.as_str()) || known_types.contains(name.as_str()),
324    }
325}
326
327/// Check if a field's `type_rust_path` is compatible with the known type/enum rust_paths.
328///
329/// When a struct field has a `type_rust_path` that differs from the `rust_path` of the
330/// enum or type with the same short name, the `.into()` conversion will fail because
331/// the `From` impl targets a different type. This detects such mismatches.
332fn field_has_path_mismatch(
333    field: &FieldDef,
334    enum_rust_paths: &AHashMap<&str, &str>,
335    type_rust_paths: &AHashMap<&str, &str>,
336) -> bool {
337    let name = match &field.ty {
338        TypeRef::Named(n) => n.as_str(),
339        TypeRef::Optional(inner) | TypeRef::Vec(inner) => match inner.as_ref() {
340            TypeRef::Named(n) => n.as_str(),
341            _ => return false,
342        },
343        _ => return false,
344    };
345
346    if let Some(field_path) = &field.type_rust_path {
347        if let Some(enum_path) = enum_rust_paths.get(name) {
348            if !paths_compatible(field_path, enum_path) {
349                return true;
350            }
351        }
352        if let Some(type_path) = type_rust_paths.get(name) {
353            if !paths_compatible(field_path, type_path) {
354                return true;
355            }
356        }
357    }
358    false
359}
360
361/// Check if two rust paths refer to the same type.
362///
363/// Handles re-exports: `crate::module::Type` and `crate::Type` are compatible
364/// when they share the same crate root and type name (the type is re-exported).
365fn paths_compatible(a: &str, b: &str) -> bool {
366    if a == b {
367        return true;
368    }
369    // Direct suffix match (e.g., "foo::Bar" ends_with "Bar")
370    if a.ends_with(b) || b.ends_with(a) {
371        return true;
372    }
373    // Same crate root + same short name → likely a re-export
374    let a_root = a.split("::").next().unwrap_or("");
375    let b_root = b.split("::").next().unwrap_or("");
376    let a_name = a.rsplit("::").next().unwrap_or("");
377    let b_name = b.rsplit("::").next().unwrap_or("");
378    a_root == b_root && a_name == b_name
379}
380
381/// Build maps of name -> rust_path for enums and types in the API surface.
382fn build_rust_path_maps(surface: &ApiSurface) -> (AHashMap<&str, &str>, AHashMap<&str, &str>) {
383    let enum_paths: AHashMap<&str, &str> = surface
384        .enums
385        .iter()
386        .map(|e| (e.name.as_str(), e.rust_path.as_str()))
387        .collect();
388    let type_paths: AHashMap<&str, &str> = surface
389        .types
390        .iter()
391        .map(|t| (t.name.as_str(), t.rust_path.as_str()))
392        .collect();
393    (enum_paths, type_paths)
394}
395
396/// Check if an enum can have From/Into safely generated (both directions).
397/// All enums are allowed — data variants use Default::default() for non-simple fields
398/// in the binding→core direction.
399pub fn can_generate_enum_conversion(enum_def: &EnumDef) -> bool {
400    !enum_def.variants.is_empty()
401}
402
403/// Check if an enum can have core→binding From safely generated.
404/// This is always possible: unit variants map 1:1, data variants discard data with `..`.
405pub fn can_generate_enum_conversion_from_core(enum_def: &EnumDef) -> bool {
406    // Always possible — data variants are handled by pattern matching with `..`
407    !enum_def.variants.is_empty()
408}
409
410/// Returns true if fields represent a tuple variant (positional: _0, _1, ...).
411pub fn is_tuple_variant(fields: &[FieldDef]) -> bool {
412    !fields.is_empty()
413        && fields[0]
414            .name
415            .strip_prefix('_')
416            .is_some_and(|rest: &str| rest.chars().all(|c: char| c.is_ascii_digit()))
417}
418
419/// Returns true if a TypeDef represents a newtype struct (single unnamed field `_0`).
420pub fn is_newtype(typ: &TypeDef) -> bool {
421    typ.fields.len() == 1 && typ.fields[0].name == "_0"
422}
423
424/// Returns true if a type name looks like a tuple (starts with `(`).
425/// Tuple types are passthrough — no conversion needed.
426pub(crate) fn is_tuple_type_name(name: &str) -> bool {
427    name.starts_with('(')
428}
429
430/// Derive the Rust import path from rust_path, replacing hyphens with underscores.
431pub fn core_type_path(typ: &TypeDef, core_import: &str) -> String {
432    // rust_path is like "liter-llm::tower::RateLimitConfig"
433    // We need "liter_llm::tower::RateLimitConfig"
434    let path = typ.rust_path.replace('-', "_");
435    // If the path starts with the core_import, use it directly
436    if path.starts_with(core_import) {
437        path
438    } else {
439        // Fallback: just use core_import::name
440        format!("{core_import}::{}", typ.name)
441    }
442}
443
444/// Check if a type has any sanitized fields (binding→core conversion is lossy).
445pub fn has_sanitized_fields(typ: &TypeDef) -> bool {
446    typ.fields.iter().any(|f| f.sanitized)
447}
448
449/// Derive the Rust import path for an enum, replacing hyphens with underscores.
450pub fn core_enum_path(enum_def: &EnumDef, core_import: &str) -> String {
451    let path = enum_def.rust_path.replace('-', "_");
452    if path.starts_with(core_import) {
453        path
454    } else {
455        format!("{core_import}::{}", enum_def.name)
456    }
457}
458
459/// Build a map from type/enum short name to full rust_path.
460///
461/// Used by backends to resolve `TypeRef::Named(name)` to the correct qualified path
462/// instead of assuming `core_import::name` (which fails for types not re-exported at crate root).
463pub fn build_type_path_map(surface: &ApiSurface, core_import: &str) -> AHashMap<String, String> {
464    let mut map = AHashMap::new();
465    for typ in &surface.types {
466        let path = typ.rust_path.replace('-', "_");
467        let resolved = if path.starts_with(core_import) {
468            path
469        } else {
470            format!("{core_import}::{}", typ.name)
471        };
472        map.insert(typ.name.clone(), resolved);
473    }
474    for en in &surface.enums {
475        let path = en.rust_path.replace('-', "_");
476        let resolved = if path.starts_with(core_import) {
477            path
478        } else {
479            format!("{core_import}::{}", en.name)
480        };
481        map.insert(en.name.clone(), resolved);
482    }
483    map
484}
485
486/// Resolve a `TypeRef::Named` short name to its full qualified path.
487///
488/// If the name is in the path map, returns the full path; otherwise falls back
489/// to `core_import::name`.
490pub fn resolve_named_path(name: &str, core_import: &str, path_map: &AHashMap<String, String>) -> String {
491    if let Some(path) = path_map.get(name) {
492        path.clone()
493    } else {
494        format!("{core_import}::{name}")
495    }
496}
497
498/// Generate a match arm for binding -> core direction.
499/// Binding enums are always unit-variant-only. Core enums may have data variants.
500/// For data variants: `BindingEnum::Variant => CoreEnum::Variant(Default::default(), ...)`
501pub fn binding_to_core_match_arm(binding_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
502    binding_to_core_match_arm_ext(binding_prefix, variant_name, fields, false)
503}
504
505/// Like `binding_to_core_match_arm` but `binding_has_data` controls whether the binding
506/// enum has the variant's fields (true) or is unit-only (false, e.g. Rustler/Elixir).
507pub fn binding_to_core_match_arm_ext(
508    binding_prefix: &str,
509    variant_name: &str,
510    fields: &[FieldDef],
511    binding_has_data: bool,
512) -> String {
513    if fields.is_empty() {
514        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
515    } else if !binding_has_data {
516        // Binding is unit-only: use Default for core fields
517        if is_tuple_variant(fields) {
518            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
519            format!(
520                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
521                defaults.join(", ")
522            )
523        } else {
524            let defaults: Vec<String> = fields
525                .iter()
526                .map(|f| format!("{}: Default::default()", f.name))
527                .collect();
528            format!(
529                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
530                defaults.join(", ")
531            )
532        }
533    } else if is_tuple_variant(fields) {
534        // Binding uses struct syntax with _0, _1 etc., core uses tuple syntax
535        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
536        let binding_pattern = field_names.join(", ");
537        // Wrap boxed fields with Box::new() and convert Named types with .into()
538        let core_args: Vec<String> = fields
539            .iter()
540            .map(|f| {
541                let name = &f.name;
542                let expr = if matches!(&f.ty, TypeRef::Named(_)) {
543                    format!("{name}.into()")
544                } else {
545                    name.clone()
546                };
547                if f.is_boxed { format!("Box::new({expr})") } else { expr }
548            })
549            .collect();
550        format!(
551            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
552            core_args.join(", ")
553        )
554    } else {
555        // Destructure binding named fields and pass to core, with .into() for Named types
556        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
557        let pattern = field_names.join(", ");
558        let core_fields: Vec<String> = fields
559            .iter()
560            .map(|f| {
561                if matches!(&f.ty, TypeRef::Named(_)) {
562                    format!("{}: {}.into()", f.name, f.name)
563                } else {
564                    format!("{0}: {0}", f.name)
565                }
566            })
567            .collect();
568        format!(
569            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
570            core_fields.join(", ")
571        )
572    }
573}
574
575/// Generate a match arm for core -> binding direction.
576/// When the binding also has data variants, destructure and forward fields.
577/// When the binding is unit-variant-only, discard core data with `..`.
578pub fn core_to_binding_match_arm(core_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
579    core_to_binding_match_arm_ext(core_prefix, variant_name, fields, false)
580}
581
582/// Like `core_to_binding_match_arm` but `binding_has_data` controls whether the binding
583/// enum has the variant's fields (true) or is unit-only (false).
584pub fn core_to_binding_match_arm_ext(
585    core_prefix: &str,
586    variant_name: &str,
587    fields: &[FieldDef],
588    binding_has_data: bool,
589) -> String {
590    if fields.is_empty() {
591        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
592    } else if !binding_has_data {
593        // Binding is unit-only: discard core data
594        if is_tuple_variant(fields) {
595            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
596        } else {
597            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
598        }
599    } else if is_tuple_variant(fields) {
600        // Core uses tuple syntax, binding uses struct syntax with _0, _1 etc.
601        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
602        let core_pattern = field_names.join(", ");
603        // Unbox and convert Named types with .into()
604        let binding_fields: Vec<String> = fields
605            .iter()
606            .map(|f| {
607                let name = &f.name;
608                let expr = if f.is_boxed && matches!(&f.ty, TypeRef::Named(_)) {
609                    format!("(*{name}).into()")
610                } else if f.is_boxed {
611                    format!("*{name}")
612                } else if matches!(&f.ty, TypeRef::Named(_)) {
613                    format!("{name}.into()")
614                } else if f.sanitized {
615                    format!("serde_json::to_string(&{name}).unwrap_or_default()")
616                } else {
617                    name.clone()
618                };
619                format!("{name}: {expr}")
620            })
621            .collect();
622        format!(
623            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
624            binding_fields.join(", ")
625        )
626    } else {
627        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
628        let pattern = field_names.join(", ");
629        let binding_fields: Vec<String> = fields
630            .iter()
631            .map(|f| {
632                if matches!(&f.ty, TypeRef::Named(_)) {
633                    format!("{}: {}.into()", f.name, f.name)
634                } else if f.sanitized {
635                    // Sanitized fields have a simplified type in the binding (e.g. String)
636                    // but the core type is complex (e.g. Vec<(String,String)>).
637                    // Serialize to JSON string for the conversion.
638                    format!("{}: serde_json::to_string(&{}).unwrap_or_default()", f.name, f.name)
639                } else {
640                    format!("{0}: {0}", f.name)
641                }
642            })
643            .collect();
644        format!(
645            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
646            binding_fields.join(", ")
647        )
648    }
649}