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    // Data enums (enums with data variants) are generated as opaque wrappers in most
195    // backends. They don't have From conversions but are still "known" types — structs
196    // containing them can still generate core→binding conversions (the field uses the
197    // opaque wrapper's From impl or format!("{:?}")).
198    let data_enum_names: AHashSet<&str> = surface
199        .enums
200        .iter()
201        .filter(|e| e.variants.iter().any(|v| !v.fields.is_empty()))
202        .map(|e| e.name.as_str())
203        .collect();
204
205    // Build rust_path maps for detecting type_rust_path mismatches.
206    let (enum_paths, type_paths) = build_rust_path_maps(surface);
207
208    // All non-opaque types are candidates (sanitized fields use format!("{:?}"))
209    let mut convertible: AHashSet<String> = surface
210        .types
211        .iter()
212        .filter(|t| !t.is_opaque)
213        .map(|t| t.name.clone())
214        .collect();
215
216    let mut changed = true;
217    while changed {
218        changed = false;
219        let snapshot: Vec<String> = convertible.iter().cloned().collect();
220        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
221        known.extend(&opaque_type_names);
222        known.extend(&data_enum_names);
223        let mut to_remove = Vec::new();
224        for type_name in &snapshot {
225            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
226                let ok = typ.fields.iter().all(|f| {
227                    if f.sanitized {
228                        true
229                    } else if field_has_path_mismatch(f, &enum_paths, &type_paths) {
230                        false
231                    } else {
232                        is_field_convertible(&f.ty, &convertible_enums, &known)
233                    }
234                });
235                if !ok {
236                    to_remove.push(type_name.clone());
237                }
238            }
239        }
240        for name in to_remove {
241            if convertible.remove(&name) {
242                changed = true;
243            }
244        }
245    }
246    convertible
247}
248
249/// Build the set of types that can have binding→core From safely generated.
250/// Strict: excludes types with sanitized fields (lossy conversion).
251/// This is transitive: a type is convertible only if all its Named field types
252/// are also convertible (or are enums with From/Into support).
253pub fn convertible_types(surface: &ApiSurface) -> AHashSet<String> {
254    // Build set of enums that have From/Into impls (unit-variant enums only)
255    let convertible_enums: AHashSet<&str> = surface
256        .enums
257        .iter()
258        .filter(|e| can_generate_enum_conversion(e))
259        .map(|e| e.name.as_str())
260        .collect();
261
262    // Build set of all known type names (including opaques) — opaque Named fields
263    // are convertible because we wrap/unwrap them via Arc.
264    let _all_type_names: AHashSet<&str> = surface.types.iter().map(|t| t.name.as_str()).collect();
265
266    // Build set of Named types that implement Default — sanitized fields referencing
267    // Named types without Default would cause a compile error in the generated From impl.
268    let default_type_names: AHashSet<&str> = surface
269        .types
270        .iter()
271        .filter(|t| t.has_default)
272        .map(|t| t.name.as_str())
273        .collect();
274
275    // Start with all non-opaque types as candidates.
276    // Types with sanitized fields use Default::default() for the sanitized field
277    // in the binding→core direction — but only if the field type implements Default.
278    let mut convertible: AHashSet<String> = surface
279        .types
280        .iter()
281        .filter(|t| !t.is_opaque)
282        .map(|t| t.name.clone())
283        .collect();
284
285    // Set of opaque type names — Named fields referencing opaques are always convertible
286    // (they use Arc wrap/unwrap), so include them in the known-types check.
287    let opaque_type_names: AHashSet<&str> = surface
288        .types
289        .iter()
290        .filter(|t| t.is_opaque)
291        .map(|t| t.name.as_str())
292        .collect();
293
294    // Data enums (enums with data variants) are generated as opaque wrappers — include
295    // them in the known set so structs containing them remain convertible.
296    let data_enum_names: AHashSet<&str> = surface
297        .enums
298        .iter()
299        .filter(|e| e.variants.iter().any(|v| !v.fields.is_empty()))
300        .map(|e| e.name.as_str())
301        .collect();
302
303    // Build rust_path maps for detecting type_rust_path mismatches.
304    let (enum_paths, type_paths) = build_rust_path_maps(surface);
305
306    // Iteratively remove types whose fields reference non-convertible Named types.
307    // We check against `convertible ∪ opaque_types ∪ data_enums` so that types
308    // referencing excluded types are transitively removed, while opaque and data
309    // enum Named fields remain valid.
310    let mut changed = true;
311    while changed {
312        changed = false;
313        let snapshot: Vec<String> = convertible.iter().cloned().collect();
314        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
315        known.extend(&opaque_type_names);
316        known.extend(&data_enum_names);
317        let mut to_remove = Vec::new();
318        for type_name in &snapshot {
319            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
320                let ok = typ.fields.iter().all(|f| {
321                    if f.sanitized {
322                        // Optional sanitized fields are always valid: the binding stores
323                        // Option<T> which defaults to None regardless of T.
324                        f.optional || sanitized_field_has_default(&f.ty, &default_type_names)
325                    } else if field_has_path_mismatch(f, &enum_paths, &type_paths) {
326                        false
327                    } else {
328                        is_field_convertible(&f.ty, &convertible_enums, &known)
329                    }
330                });
331                if !ok {
332                    to_remove.push(type_name.clone());
333                }
334            }
335        }
336        for name in to_remove {
337            if convertible.remove(&name) {
338                changed = true;
339            }
340        }
341    }
342    convertible
343}
344
345/// Check if a sanitized field's type can produce a valid `Default::default()` expression.
346/// Primitive types, strings, collections, Options, and Named types with `has_default` are fine.
347/// Named types without `has_default` are not — generating `Default::default()` for them would
348/// fail to compile.
349fn sanitized_field_has_default(ty: &TypeRef, default_types: &AHashSet<&str>) -> bool {
350    match ty {
351        TypeRef::Primitive(_)
352        | TypeRef::String
353        | TypeRef::Char
354        | TypeRef::Bytes
355        | TypeRef::Path
356        | TypeRef::Unit
357        | TypeRef::Duration
358        | TypeRef::Json => true,
359        // Option<T> defaults to None regardless of T
360        TypeRef::Optional(_) => true,
361        // Vec<T> defaults to empty vec regardless of T
362        TypeRef::Vec(_) => true,
363        // Map<K, V> defaults to empty map regardless of K/V
364        TypeRef::Map(_, _) => true,
365        TypeRef::Named(name) => {
366            if is_tuple_type_name(name) {
367                // Tuple types are always passthrough
368                true
369            } else {
370                // Named type must have has_default to be safely used via Default::default()
371                default_types.contains(name.as_str())
372            }
373        }
374    }
375}
376
377/// Check if a specific type is in the convertible set.
378pub fn can_generate_conversion(typ: &TypeDef, convertible: &AHashSet<String>) -> bool {
379    convertible.contains(&typ.name)
380}
381
382pub(crate) fn is_field_convertible(
383    ty: &TypeRef,
384    convertible_enums: &AHashSet<&str>,
385    known_types: &AHashSet<&str>,
386) -> bool {
387    match ty {
388        TypeRef::Primitive(_)
389        | TypeRef::String
390        | TypeRef::Char
391        | TypeRef::Bytes
392        | TypeRef::Path
393        | TypeRef::Unit
394        | TypeRef::Duration => true,
395        TypeRef::Json => true,
396        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_field_convertible(inner, convertible_enums, known_types),
397        TypeRef::Map(k, v) => {
398            is_field_convertible(k, convertible_enums, known_types)
399                && is_field_convertible(v, convertible_enums, known_types)
400        }
401        // Tuple types are passthrough — always convertible
402        TypeRef::Named(name) if is_tuple_type_name(name) => true,
403        // Unit-variant enums and known types (including opaques, which use Arc wrap/unwrap) are convertible.
404        TypeRef::Named(name) => convertible_enums.contains(name.as_str()) || known_types.contains(name.as_str()),
405    }
406}
407
408/// Check if a field's `type_rust_path` is compatible with the known type/enum rust_paths.
409///
410/// When a struct field has a `type_rust_path` that differs from the `rust_path` of the
411/// enum or type with the same short name, the `.into()` conversion will fail because
412/// the `From` impl targets a different type. This detects such mismatches.
413fn field_has_path_mismatch(
414    field: &FieldDef,
415    enum_rust_paths: &AHashMap<&str, &str>,
416    type_rust_paths: &AHashMap<&str, &str>,
417) -> bool {
418    let name = match &field.ty {
419        TypeRef::Named(n) => n.as_str(),
420        TypeRef::Optional(inner) | TypeRef::Vec(inner) => match inner.as_ref() {
421            TypeRef::Named(n) => n.as_str(),
422            _ => return false,
423        },
424        _ => return false,
425    };
426
427    if let Some(field_path) = &field.type_rust_path {
428        if let Some(enum_path) = enum_rust_paths.get(name) {
429            if !paths_compatible(field_path, enum_path) {
430                return true;
431            }
432        }
433        if let Some(type_path) = type_rust_paths.get(name) {
434            if !paths_compatible(field_path, type_path) {
435                return true;
436            }
437        }
438    }
439    false
440}
441
442/// Check if two rust paths refer to the same type.
443///
444/// Handles re-exports: `crate::module::Type` and `crate::Type` are compatible
445/// when they share the same crate root and type name (the type is re-exported).
446fn paths_compatible(a: &str, b: &str) -> bool {
447    if a == b {
448        return true;
449    }
450    // Normalize dashes to underscores for crate name comparison
451    // (Cargo uses dashes in package names, Rust uses underscores in crate names)
452    let a_norm = a.replace('-', "_");
453    let b_norm = b.replace('-', "_");
454    if a_norm == b_norm {
455        return true;
456    }
457    // Direct suffix match (e.g., "foo::Bar" ends_with "Bar")
458    if a_norm.ends_with(&b_norm) || b_norm.ends_with(&a_norm) {
459        return true;
460    }
461    // Same crate root + same short name → likely a re-export
462    let a_root = a_norm.split("::").next().unwrap_or("");
463    let b_root = b_norm.split("::").next().unwrap_or("");
464    let a_name = a_norm.rsplit("::").next().unwrap_or("");
465    let b_name = b_norm.rsplit("::").next().unwrap_or("");
466    a_root == b_root && a_name == b_name
467}
468
469/// Build maps of name -> rust_path for enums and types in the API surface.
470fn build_rust_path_maps(surface: &ApiSurface) -> (AHashMap<&str, &str>, AHashMap<&str, &str>) {
471    let enum_paths: AHashMap<&str, &str> = surface
472        .enums
473        .iter()
474        .map(|e| (e.name.as_str(), e.rust_path.as_str()))
475        .collect();
476    let type_paths: AHashMap<&str, &str> = surface
477        .types
478        .iter()
479        .map(|t| (t.name.as_str(), t.rust_path.as_str()))
480        .collect();
481    (enum_paths, type_paths)
482}
483
484/// Check if an enum can have From/Into safely generated (both directions).
485/// All enums are allowed — data variants use Default::default() for non-simple fields
486/// in the binding→core direction.
487pub fn can_generate_enum_conversion(enum_def: &EnumDef) -> bool {
488    !enum_def.variants.is_empty()
489}
490
491/// Check if an enum can have core→binding From safely generated.
492/// This is always possible: unit variants map 1:1, data variants discard data with `..`.
493pub fn can_generate_enum_conversion_from_core(enum_def: &EnumDef) -> bool {
494    // Always possible — data variants are handled by pattern matching with `..`
495    !enum_def.variants.is_empty()
496}
497
498/// Returns true if fields represent a tuple variant (positional: _0, _1, ...).
499pub fn is_tuple_variant(fields: &[FieldDef]) -> bool {
500    !fields.is_empty()
501        && fields[0]
502            .name
503            .strip_prefix('_')
504            .is_some_and(|rest: &str| rest.chars().all(|c: char| c.is_ascii_digit()))
505}
506
507/// Returns true if a TypeDef represents a newtype struct (single unnamed field `_0`).
508pub fn is_newtype(typ: &TypeDef) -> bool {
509    typ.fields.len() == 1 && typ.fields[0].name == "_0"
510}
511
512/// Returns true if a type name looks like a tuple (starts with `(`).
513/// Tuple types are passthrough — no conversion needed.
514pub(crate) fn is_tuple_type_name(name: &str) -> bool {
515    name.starts_with('(')
516}
517
518/// Derive the Rust import path from rust_path, replacing hyphens with underscores.
519///
520/// Prefers `original_rust_path` (the path before auto_path_mappings rewriting)
521/// so that `From` impls reference the actual defining crate, avoiding orphan
522/// rule violations when `core_import` is a re-export facade.
523pub fn core_type_path(typ: &TypeDef, core_import: &str) -> String {
524    core_type_path_remapped(typ, core_import, &[])
525}
526
527/// Like [`core_type_path`] but rewrites the leading crate segment when it matches
528/// a known source→override mapping.
529///
530/// When `core_crate_override` is set for a language, IR `rust_path` values still
531/// contain the original source crate prefix (e.g. `mylib_core::Method`). The
532/// `remaps` slice contains `(original_crate, override_crate)` pairs; when the
533/// leading crate segment of `rust_path` matches `original_crate`, it is replaced
534/// with `override_crate`.
535pub fn core_type_path_remapped(typ: &TypeDef, core_import: &str, remaps: &[(&str, &str)]) -> String {
536    // Always use rust_path (post path-mapping) — this is the import name that
537    // binding crates can actually resolve. The original_rust_path preserves the
538    // pre-mapping crate name (e.g. "html_to_markdown") which may not be importable
539    // when the dependency is renamed (e.g. "html-to-markdown-rs" in Cargo.toml).
540    let path = typ.rust_path.replace('-', "_");
541    if path.contains("::") {
542        apply_crate_remaps(&path, remaps)
543    } else {
544        format!("{core_import}::{}", typ.name)
545    }
546}
547
548/// Apply source→override crate remaps to a fully-qualified Rust path.
549///
550/// If the leading crate segment of `path` (the part before the first `::`)
551/// matches any entry in `remaps`, that segment is replaced with the override.
552/// Returns `path` unchanged when no remap applies.
553pub fn apply_crate_remaps(path: &str, remaps: &[(&str, &str)]) -> String {
554    if remaps.is_empty() {
555        return path.to_string();
556    }
557    if let Some(sep) = path.find("::") {
558        let leading = &path[..sep];
559        if let Some(&(_, override_crate)) = remaps.iter().find(|(orig, _)| *orig == leading) {
560            return format!("{override_crate}{}", &path[sep..]);
561        }
562    }
563    path.to_string()
564}
565
566/// Check if a type has any sanitized fields (binding→core conversion is lossy).
567pub fn has_sanitized_fields(typ: &TypeDef) -> bool {
568    typ.fields.iter().any(|f| f.sanitized)
569}
570
571/// Derive the Rust import path for an enum, replacing hyphens with underscores.
572pub fn core_enum_path(enum_def: &EnumDef, core_import: &str) -> String {
573    core_enum_path_remapped(enum_def, core_import, &[])
574}
575
576/// Like [`core_enum_path`] but rewrites the leading crate segment when it matches
577/// a known source→override mapping. See [`core_type_path_remapped`] for details.
578pub fn core_enum_path_remapped(enum_def: &EnumDef, core_import: &str, remaps: &[(&str, &str)]) -> String {
579    let path = enum_def.rust_path.replace('-', "_");
580    if path.starts_with(core_import) || path.contains("::") {
581        // Path is already fully-qualified — apply remaps and return.
582        apply_crate_remaps(&path, remaps)
583    } else {
584        // Bare unqualified name: prefix with the facade crate.
585        format!("{core_import}::{}", enum_def.name)
586    }
587}
588
589/// Build a map from type/enum short name to full rust_path.
590///
591/// Used by backends to resolve `TypeRef::Named(name)` to the correct qualified path
592/// instead of assuming `core_import::name` (which fails for types not re-exported at crate root).
593pub fn build_type_path_map(surface: &ApiSurface, core_import: &str) -> AHashMap<String, String> {
594    let mut map = AHashMap::new();
595    for typ in surface.types.iter().filter(|typ| !typ.is_trait) {
596        let path = typ.rust_path.replace('-', "_");
597        let resolved = if path.starts_with(core_import) {
598            path
599        } else {
600            format!("{core_import}::{}", typ.name)
601        };
602        map.insert(typ.name.clone(), resolved);
603    }
604    for en in &surface.enums {
605        let path = en.rust_path.replace('-', "_");
606        let resolved = if path.starts_with(core_import) {
607            path
608        } else {
609            format!("{core_import}::{}", en.name)
610        };
611        map.insert(en.name.clone(), resolved);
612    }
613    map
614}
615
616/// Resolve a `TypeRef::Named` short name to its full qualified path.
617///
618/// If the name is in the path map, returns the full path; otherwise falls back
619/// to `core_import::name`.
620pub fn resolve_named_path(name: &str, core_import: &str, path_map: &AHashMap<String, String>) -> String {
621    if let Some(path) = path_map.get(name) {
622        path.clone()
623    } else {
624        format!("{core_import}::{name}")
625    }
626}
627
628/// Generate a match arm for binding -> core direction.
629/// Binding enums are always unit-variant-only. Core enums may have data variants.
630/// For data variants: `BindingEnum::Variant => CoreEnum::Variant(Default::default(), ...)`
631pub fn binding_to_core_match_arm(binding_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
632    binding_to_core_match_arm_ext(binding_prefix, variant_name, fields, false)
633}
634
635/// Like `binding_to_core_match_arm` but `binding_has_data` controls whether the binding
636/// enum has the variant's fields (true) or is unit-only (false, e.g. Rustler/Elixir).
637/// Generate match arm for binding->core conversion with config (handles type conversions).
638pub fn binding_to_core_match_arm_ext_cfg(
639    binding_prefix: &str,
640    variant_name: &str,
641    fields: &[FieldDef],
642    binding_has_data: bool,
643    config: &ConversionConfig,
644) -> String {
645    use super::binding_to_core::field_conversion_to_core_cfg;
646
647    if fields.is_empty() {
648        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
649    } else if !binding_has_data {
650        // Binding is unit-only: use Default for core fields
651        if is_tuple_variant(fields) {
652            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
653            format!(
654                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
655                defaults.join(", ")
656            )
657        } else {
658            let defaults: Vec<String> = fields
659                .iter()
660                .map(|f| format!("{}: Default::default()", f.name))
661                .collect();
662            format!(
663                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
664                defaults.join(", ")
665            )
666        }
667    } else if is_tuple_variant(fields) {
668        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
669        let binding_pattern = field_names.join(", ");
670        let core_args: Vec<String> = fields
671            .iter()
672            .map(|f| {
673                let name = &f.name;
674                // Sanitized fields: binding uses String, use serde_json to deserialize back.
675                if f.sanitized {
676                    let expr = if let TypeRef::Vec(_) = &f.ty {
677                        // Vec<String> sanitized: each element is a debug-formatted value,
678                        // parse each one individually.
679                        format!("{name}.iter().filter_map(|s| serde_json::from_str(s).ok()).collect()")
680                    } else {
681                        format!("serde_json::from_str(&{name}).unwrap_or_default()")
682                    };
683                    return if f.is_boxed { format!("Box::new({expr})") } else { expr };
684                }
685                // Fields referencing excluded types: they appear as String in the binding.
686                // Use serde_json deserialization to convert back to the core type.
687                if !config.exclude_types.is_empty() && field_references_excluded_type(&f.ty, config.exclude_types) {
688                    let expr = format!("serde_json::from_str(&{name}).unwrap_or_default()");
689                    return if f.is_boxed { format!("Box::new({expr})") } else { expr };
690                }
691                // Use the conversion logic from field_conversion_to_core_cfg.
692                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
693                // so replace `val.{name}` with just `{name}` in the generated expression.
694                let conv = field_conversion_to_core_cfg(name, &f.ty, f.optional, config);
695                // Extract the RHS from "name: expr" format
696                let expr = if let Some(expr) = conv.strip_prefix(&format!("{name}: ")) {
697                    let expr = expr.replace(&format!("val.{name}"), name);
698                    expr.to_string()
699                } else {
700                    conv
701                };
702                if f.is_boxed { format!("Box::new({expr})") } else { expr }
703            })
704            .collect();
705        format!(
706            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
707            core_args.join(", ")
708        )
709    } else {
710        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
711        let pattern = field_names.join(", ");
712        let core_fields: Vec<String> = fields
713            .iter()
714            .map(|f| {
715                // Sanitized fields: the binding stores a simplified type (String for any complex type
716                // like Vec<(String,String)>, Vec<Named>, etc.). Use serde_json to deserialize back.
717                if f.sanitized {
718                    // For Vec<String> sanitized (representing Vec<Tuple> like Vec<(String, String)>),
719                    // each element is a debug-formatted string. Cannot safely use serde_json::from_str
720                    // on Vec<String>. Default to empty collection.
721                    if let TypeRef::Vec(_) = &f.ty {
722                        return format!(
723                            "{}: {}.iter().filter_map(|s| serde_json::from_str(s).ok()).collect()",
724                            f.name, f.name
725                        );
726                    }
727                    return format!("{}: serde_json::from_str(&{}).unwrap_or_default()", f.name, f.name);
728                }
729                // Use the conversion logic from field_conversion_to_core_cfg.
730                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
731                // so replace `val.{name}` with just `{name}` in the generated expression.
732                let conv = field_conversion_to_core_cfg(&f.name, &f.ty, f.optional, config);
733                // Extract the RHS from "name: expr" format
734                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
735                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
736                    format!("{}: {}", f.name, expr)
737                } else {
738                    conv
739                }
740            })
741            .collect();
742        format!(
743            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
744            core_fields.join(", ")
745        )
746    }
747}
748
749pub fn binding_to_core_match_arm_ext(
750    binding_prefix: &str,
751    variant_name: &str,
752    fields: &[FieldDef],
753    binding_has_data: bool,
754) -> String {
755    if fields.is_empty() {
756        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
757    } else if !binding_has_data {
758        // Binding is unit-only: use Default for core fields
759        if is_tuple_variant(fields) {
760            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
761            format!(
762                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
763                defaults.join(", ")
764            )
765        } else {
766            let defaults: Vec<String> = fields
767                .iter()
768                .map(|f| format!("{}: Default::default()", f.name))
769                .collect();
770            format!(
771                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
772                defaults.join(", ")
773            )
774        }
775    } else if is_tuple_variant(fields) {
776        // Binding uses struct syntax with _0, _1 etc., core uses tuple syntax
777        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
778        let binding_pattern = field_names.join(", ");
779        // Wrap boxed fields with Box::new() and convert Named types with .into()
780        let core_args: Vec<String> = fields
781            .iter()
782            .map(|f| {
783                let name = &f.name;
784                let expr = if matches!(&f.ty, TypeRef::Named(_)) {
785                    format!("{name}.into()")
786                } else if f.sanitized {
787                    format!("serde_json::from_str(&{name}).unwrap_or_default()")
788                } else {
789                    name.clone()
790                };
791                if f.is_boxed { format!("Box::new({expr})") } else { expr }
792            })
793            .collect();
794        format!(
795            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
796            core_args.join(", ")
797        )
798    } else {
799        // Destructure binding named fields and pass to core, with .into() for Named types
800        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
801        let pattern = field_names.join(", ");
802        let core_fields: Vec<String> = fields
803            .iter()
804            .map(|f| {
805                if matches!(&f.ty, TypeRef::Named(_)) {
806                    format!("{}: {}.into()", f.name, f.name)
807                } else if f.sanitized {
808                    // Sanitized fields have a simplified type in the binding (e.g. String)
809                    // but the core type is complex (e.g. Vec<(String,String)>).
810                    // Deserialize from JSON string for the binding→core conversion.
811                    format!("{}: serde_json::from_str(&{}).unwrap_or_default()", f.name, f.name)
812                } else {
813                    format!("{0}: {0}", f.name)
814                }
815            })
816            .collect();
817        format!(
818            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
819            core_fields.join(", ")
820        )
821    }
822}
823
824/// Generate a match arm for core -> binding direction.
825/// When the binding also has data variants, destructure and forward fields.
826/// When the binding is unit-variant-only, discard core data with `..`.
827pub fn core_to_binding_match_arm(core_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
828    core_to_binding_match_arm_ext(core_prefix, variant_name, fields, false)
829}
830
831/// Like `core_to_binding_match_arm` but `binding_has_data` controls whether the binding
832/// enum has the variant's fields (true) or is unit-only (false).
833/// Generate match arm for core->binding conversion with config (handles type conversions).
834pub fn core_to_binding_match_arm_ext_cfg(
835    core_prefix: &str,
836    variant_name: &str,
837    fields: &[FieldDef],
838    binding_has_data: bool,
839    config: &ConversionConfig,
840) -> String {
841    use super::core_to_binding::field_conversion_from_core_cfg;
842    use ahash::AHashSet;
843
844    if fields.is_empty() {
845        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
846    } else if !binding_has_data {
847        // Binding is unit-only: discard core data
848        if is_tuple_variant(fields) {
849            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
850        } else {
851            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
852        }
853    } else if is_tuple_variant(fields) {
854        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
855        let core_pattern = field_names.join(", ");
856        let binding_fields: Vec<String> = fields
857            .iter()
858            .map(|f| {
859                // Use the conversion logic from field_conversion_from_core_cfg.
860                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
861                // so replace `val.{name}` with just `{name}` in the generated expression.
862                let conv =
863                    field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
864                // Extract the RHS from "name: expr" format
865                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
866                    let mut expr = expr.replace(&format!("val.{}", f.name), &f.name);
867                    // Boxed fields in core tuple variants need dereferencing before conversion
868                    if f.is_boxed {
869                        expr = expr.replace(&format!("{}.into()", f.name), &format!("(*{}).into()", f.name));
870                    }
871                    format!("{}: {}", f.name, expr)
872                } else {
873                    conv
874                }
875            })
876            .collect();
877        format!(
878            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
879            binding_fields.join(", ")
880        )
881    } else {
882        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
883        let pattern = field_names.join(", ");
884        let binding_fields: Vec<String> = fields
885            .iter()
886            .map(|f| {
887                // Use the conversion logic from field_conversion_from_core_cfg.
888                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
889                // so replace `val.{name}` with just `{name}` in the generated expression.
890                let conv =
891                    field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
892                // Extract the RHS from "name: expr" format
893                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
894                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
895                    format!("{}: {}", f.name, expr)
896                } else {
897                    conv
898                }
899            })
900            .collect();
901        format!(
902            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
903            binding_fields.join(", ")
904        )
905    }
906}
907
908pub fn core_to_binding_match_arm_ext(
909    core_prefix: &str,
910    variant_name: &str,
911    fields: &[FieldDef],
912    binding_has_data: bool,
913) -> String {
914    if fields.is_empty() {
915        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
916    } else if !binding_has_data {
917        // Binding is unit-only: discard core data
918        if is_tuple_variant(fields) {
919            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
920        } else {
921            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
922        }
923    } else if is_tuple_variant(fields) {
924        // Core uses tuple syntax, binding uses struct syntax with _0, _1 etc.
925        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
926        let core_pattern = field_names.join(", ");
927        // Unbox and convert Named types with .into()
928        let binding_fields: Vec<String> = fields
929            .iter()
930            .map(|f| {
931                let name = &f.name;
932                let expr = if f.is_boxed && matches!(&f.ty, TypeRef::Named(_)) {
933                    format!("(*{name}).into()")
934                } else if f.is_boxed {
935                    format!("*{name}")
936                } else if matches!(&f.ty, TypeRef::Named(_)) {
937                    format!("{name}.into()")
938                } else if f.sanitized {
939                    format!("serde_json::to_string(&{name}).unwrap_or_default()")
940                } else {
941                    name.clone()
942                };
943                format!("{name}: {expr}")
944            })
945            .collect();
946        format!(
947            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
948            binding_fields.join(", ")
949        )
950    } else {
951        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
952        let pattern = field_names.join(", ");
953        let binding_fields: Vec<String> = fields
954            .iter()
955            .map(|f| {
956                if matches!(&f.ty, TypeRef::Named(_)) {
957                    format!("{}: {}.into()", f.name, f.name)
958                } else if f.sanitized {
959                    // Sanitized fields have a simplified type in the binding (e.g. String)
960                    // but the core type is complex (e.g. Vec<(String,String)>).
961                    // Serialize to JSON string for the conversion.
962                    format!("{}: serde_json::to_string(&{}).unwrap_or_default()", f.name, f.name)
963                } else {
964                    format!("{0}: {0}", f.name)
965                }
966            })
967            .collect();
968        format!(
969            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
970            binding_fields.join(", ")
971        )
972    }
973}