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.
495pub fn core_type_path(typ: &TypeDef, core_import: &str) -> String {
496    // rust_path is like "liter-llm::tower::RateLimitConfig"
497    // We need "liter_llm::tower::RateLimitConfig"
498    let path = typ.rust_path.replace('-', "_");
499    // If the path starts with the core_import, use it directly
500    if path.starts_with(core_import) {
501        path
502    } else {
503        // Fallback: just use core_import::name
504        format!("{core_import}::{}", typ.name)
505    }
506}
507
508/// Check if a type has any sanitized fields (binding→core conversion is lossy).
509pub fn has_sanitized_fields(typ: &TypeDef) -> bool {
510    typ.fields.iter().any(|f| f.sanitized)
511}
512
513/// Derive the Rust import path for an enum, replacing hyphens with underscores.
514pub fn core_enum_path(enum_def: &EnumDef, core_import: &str) -> String {
515    let path = enum_def.rust_path.replace('-', "_");
516    if path.starts_with(core_import) {
517        path
518    } else {
519        format!("{core_import}::{}", enum_def.name)
520    }
521}
522
523/// Build a map from type/enum short name to full rust_path.
524///
525/// Used by backends to resolve `TypeRef::Named(name)` to the correct qualified path
526/// instead of assuming `core_import::name` (which fails for types not re-exported at crate root).
527pub fn build_type_path_map(surface: &ApiSurface, core_import: &str) -> AHashMap<String, String> {
528    let mut map = AHashMap::new();
529    for typ in surface.types.iter().filter(|typ| !typ.is_trait) {
530        let path = typ.rust_path.replace('-', "_");
531        let resolved = if path.starts_with(core_import) {
532            path
533        } else {
534            format!("{core_import}::{}", typ.name)
535        };
536        map.insert(typ.name.clone(), resolved);
537    }
538    for en in &surface.enums {
539        let path = en.rust_path.replace('-', "_");
540        let resolved = if path.starts_with(core_import) {
541            path
542        } else {
543            format!("{core_import}::{}", en.name)
544        };
545        map.insert(en.name.clone(), resolved);
546    }
547    map
548}
549
550/// Resolve a `TypeRef::Named` short name to its full qualified path.
551///
552/// If the name is in the path map, returns the full path; otherwise falls back
553/// to `core_import::name`.
554pub fn resolve_named_path(name: &str, core_import: &str, path_map: &AHashMap<String, String>) -> String {
555    if let Some(path) = path_map.get(name) {
556        path.clone()
557    } else {
558        format!("{core_import}::{name}")
559    }
560}
561
562/// Generate a match arm for binding -> core direction.
563/// Binding enums are always unit-variant-only. Core enums may have data variants.
564/// For data variants: `BindingEnum::Variant => CoreEnum::Variant(Default::default(), ...)`
565pub fn binding_to_core_match_arm(binding_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
566    binding_to_core_match_arm_ext(binding_prefix, variant_name, fields, false)
567}
568
569/// Like `binding_to_core_match_arm` but `binding_has_data` controls whether the binding
570/// enum has the variant's fields (true) or is unit-only (false, e.g. Rustler/Elixir).
571/// Generate match arm for binding->core conversion with config (handles type conversions).
572pub fn binding_to_core_match_arm_ext_cfg(
573    binding_prefix: &str,
574    variant_name: &str,
575    fields: &[FieldDef],
576    binding_has_data: bool,
577    config: &ConversionConfig,
578) -> String {
579    use super::binding_to_core::field_conversion_to_core_cfg;
580
581    if fields.is_empty() {
582        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
583    } else if !binding_has_data {
584        // Binding is unit-only: use Default for core fields
585        if is_tuple_variant(fields) {
586            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
587            format!(
588                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
589                defaults.join(", ")
590            )
591        } else {
592            let defaults: Vec<String> = fields
593                .iter()
594                .map(|f| format!("{}: Default::default()", f.name))
595                .collect();
596            format!(
597                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
598                defaults.join(", ")
599            )
600        }
601    } else if is_tuple_variant(fields) {
602        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
603        let binding_pattern = field_names.join(", ");
604        let core_args: Vec<String> = fields
605            .iter()
606            .map(|f| {
607                // Use the conversion logic from field_conversion_to_core_cfg.
608                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
609                // so replace `val.{name}` with just `{name}` in the generated expression.
610                let conv = field_conversion_to_core_cfg(&f.name, &f.ty, f.optional, config);
611                // Extract the RHS from "name: expr" format
612                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
613                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
614                    expr.to_string()
615                } else {
616                    conv
617                }
618            })
619            .collect();
620        format!(
621            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
622            core_args.join(", ")
623        )
624    } else {
625        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
626        let pattern = field_names.join(", ");
627        let core_fields: Vec<String> = fields
628            .iter()
629            .map(|f| {
630                // Use the conversion logic from field_conversion_to_core_cfg.
631                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
632                // so replace `val.{name}` with just `{name}` in the generated expression.
633                let conv = field_conversion_to_core_cfg(&f.name, &f.ty, f.optional, config);
634                // Extract the RHS from "name: expr" format
635                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
636                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
637                    format!("{}: {}", f.name, expr)
638                } else {
639                    conv
640                }
641            })
642            .collect();
643        format!(
644            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
645            core_fields.join(", ")
646        )
647    }
648}
649
650pub fn binding_to_core_match_arm_ext(
651    binding_prefix: &str,
652    variant_name: &str,
653    fields: &[FieldDef],
654    binding_has_data: bool,
655) -> String {
656    if fields.is_empty() {
657        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
658    } else if !binding_has_data {
659        // Binding is unit-only: use Default for core fields
660        if is_tuple_variant(fields) {
661            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
662            format!(
663                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
664                defaults.join(", ")
665            )
666        } else {
667            let defaults: Vec<String> = fields
668                .iter()
669                .map(|f| format!("{}: Default::default()", f.name))
670                .collect();
671            format!(
672                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
673                defaults.join(", ")
674            )
675        }
676    } else if is_tuple_variant(fields) {
677        // Binding uses struct syntax with _0, _1 etc., core uses tuple syntax
678        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
679        let binding_pattern = field_names.join(", ");
680        // Wrap boxed fields with Box::new() and convert Named types with .into()
681        let core_args: Vec<String> = fields
682            .iter()
683            .map(|f| {
684                let name = &f.name;
685                let expr = if matches!(&f.ty, TypeRef::Named(_)) {
686                    format!("{name}.into()")
687                } else if f.sanitized {
688                    format!("serde_json::from_str(&{name}).unwrap_or_default()")
689                } else {
690                    name.clone()
691                };
692                if f.is_boxed { format!("Box::new({expr})") } else { expr }
693            })
694            .collect();
695        format!(
696            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
697            core_args.join(", ")
698        )
699    } else {
700        // Destructure binding named fields and pass to core, with .into() for Named types
701        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
702        let pattern = field_names.join(", ");
703        let core_fields: Vec<String> = fields
704            .iter()
705            .map(|f| {
706                if matches!(&f.ty, TypeRef::Named(_)) {
707                    format!("{}: {}.into()", f.name, f.name)
708                } else if f.sanitized {
709                    // Sanitized fields have a simplified type in the binding (e.g. String)
710                    // but the core type is complex (e.g. Vec<(String,String)>).
711                    // Deserialize from JSON string for the binding→core conversion.
712                    format!("{}: serde_json::from_str(&{}).unwrap_or_default()", f.name, f.name)
713                } else {
714                    format!("{0}: {0}", f.name)
715                }
716            })
717            .collect();
718        format!(
719            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
720            core_fields.join(", ")
721        )
722    }
723}
724
725/// Generate a match arm for core -> binding direction.
726/// When the binding also has data variants, destructure and forward fields.
727/// When the binding is unit-variant-only, discard core data with `..`.
728pub fn core_to_binding_match_arm(core_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
729    core_to_binding_match_arm_ext(core_prefix, variant_name, fields, false)
730}
731
732/// Like `core_to_binding_match_arm` but `binding_has_data` controls whether the binding
733/// enum has the variant's fields (true) or is unit-only (false).
734/// Generate match arm for core->binding conversion with config (handles type conversions).
735pub fn core_to_binding_match_arm_ext_cfg(
736    core_prefix: &str,
737    variant_name: &str,
738    fields: &[FieldDef],
739    binding_has_data: bool,
740    config: &ConversionConfig,
741) -> String {
742    use super::core_to_binding::field_conversion_from_core_cfg;
743    use ahash::AHashSet;
744
745    if fields.is_empty() {
746        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
747    } else if !binding_has_data {
748        // Binding is unit-only: discard core data
749        if is_tuple_variant(fields) {
750            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
751        } else {
752            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
753        }
754    } else if is_tuple_variant(fields) {
755        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
756        let core_pattern = field_names.join(", ");
757        let binding_fields: Vec<String> = fields
758            .iter()
759            .map(|f| {
760                // Use the conversion logic from field_conversion_from_core_cfg.
761                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
762                // so replace `val.{name}` with just `{name}` in the generated expression.
763                let conv =
764                    field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
765                // Extract the RHS from "name: expr" format
766                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
767                    let mut expr = expr.replace(&format!("val.{}", f.name), &f.name);
768                    // Boxed fields in core tuple variants need dereferencing before conversion
769                    if f.is_boxed {
770                        expr = expr.replace(&format!("{}.into()", f.name), &format!("(*{}).into()", f.name));
771                    }
772                    format!("{}: {}", f.name, expr)
773                } else {
774                    conv
775                }
776            })
777            .collect();
778        format!(
779            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
780            binding_fields.join(", ")
781        )
782    } else {
783        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
784        let pattern = field_names.join(", ");
785        let binding_fields: Vec<String> = fields
786            .iter()
787            .map(|f| {
788                // Use the conversion logic from field_conversion_from_core_cfg.
789                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
790                // so replace `val.{name}` with just `{name}` in the generated expression.
791                let conv =
792                    field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
793                // Extract the RHS from "name: expr" format
794                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
795                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
796                    format!("{}: {}", f.name, expr)
797                } else {
798                    conv
799                }
800            })
801            .collect();
802        format!(
803            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
804            binding_fields.join(", ")
805        )
806    }
807}
808
809pub fn core_to_binding_match_arm_ext(
810    core_prefix: &str,
811    variant_name: &str,
812    fields: &[FieldDef],
813    binding_has_data: bool,
814) -> String {
815    if fields.is_empty() {
816        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
817    } else if !binding_has_data {
818        // Binding is unit-only: discard core data
819        if is_tuple_variant(fields) {
820            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
821        } else {
822            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
823        }
824    } else if is_tuple_variant(fields) {
825        // Core uses tuple syntax, binding uses struct syntax with _0, _1 etc.
826        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
827        let core_pattern = field_names.join(", ");
828        // Unbox and convert Named types with .into()
829        let binding_fields: Vec<String> = fields
830            .iter()
831            .map(|f| {
832                let name = &f.name;
833                let expr = if f.is_boxed && matches!(&f.ty, TypeRef::Named(_)) {
834                    format!("(*{name}).into()")
835                } else if f.is_boxed {
836                    format!("*{name}")
837                } else if matches!(&f.ty, TypeRef::Named(_)) {
838                    format!("{name}.into()")
839                } else if f.sanitized {
840                    format!("serde_json::to_string(&{name}).unwrap_or_default()")
841                } else {
842                    name.clone()
843                };
844                format!("{name}: {expr}")
845            })
846            .collect();
847        format!(
848            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
849            binding_fields.join(", ")
850        )
851    } else {
852        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
853        let pattern = field_names.join(", ");
854        let binding_fields: Vec<String> = fields
855            .iter()
856            .map(|f| {
857                if matches!(&f.ty, TypeRef::Named(_)) {
858                    format!("{}: {}.into()", f.name, f.name)
859                } else if f.sanitized {
860                    // Sanitized fields have a simplified type in the binding (e.g. String)
861                    // but the core type is complex (e.g. Vec<(String,String)>).
862                    // Serialize to JSON string for the conversion.
863                    format!("{}: serde_json::to_string(&{}).unwrap_or_default()", f.name, f.name)
864                } else {
865                    format!("{0}: {0}", f.name)
866                }
867            })
868            .collect();
869        format!(
870            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
871            binding_fields.join(", ")
872        )
873    }
874}