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