Skip to main content

alef_codegen/conversions/
helpers.rs

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