Skip to main content

alef_codegen/
config_gen.rs

1use alef_core::ir::{DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
2use heck::{ToPascalCase, ToShoutySnakeCase, ToSnakeCase};
3
4/// Returns true if a field is a tuple struct positional field (e.g., `_0`, `_1`, `0`, `1`).
5/// These fields have no meaningful name and must be skipped in languages requiring named fields.
6fn is_tuple_field(field: &FieldDef) -> bool {
7    (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
8        || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
9}
10
11/// Returns true if the Rust default value for a field is its type's inherent default,
12/// meaning `.unwrap_or_default()` can be used instead of `.unwrap_or(value)`.
13/// This avoids clippy::unwrap_or_default warnings.
14fn use_unwrap_or_default(field: &FieldDef) -> bool {
15    if let Some(typed_default) = &field.typed_default {
16        return matches!(typed_default, DefaultValue::Empty | DefaultValue::None);
17    }
18    // No typed_default — the fallback default_value_for_field generates type-based zero values
19    // which are the same as Default::default() for the type.
20    // Named types may not implement Default in some bindings (e.g. Magnus), so they
21    // fall through to the explicit default path.
22    field.default.is_none() && !matches!(&field.ty, TypeRef::Named(_))
23}
24
25/// Generate a PyO3 `#[new]` constructor with kwargs for a type with `has_default`.
26/// All fields become keyword args with their defaults in `#[pyo3(signature = (...))]`.
27pub fn gen_pyo3_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
28    let mut lines = Vec::new();
29    lines.push("#[new]".to_string());
30
31    // Build the signature line with defaults
32    let mut sig_parts = Vec::new();
33    for field in &typ.fields {
34        let default_str = default_value_for_field(field, "python");
35        sig_parts.push(format!("{}={}", field.name, default_str));
36    }
37    let signature = format!("#[pyo3(signature = ({}))]", sig_parts.join(", "));
38    lines.push(signature);
39
40    // Function signature
41    lines.push("fn new(".to_string());
42    for (i, field) in typ.fields.iter().enumerate() {
43        let type_str = type_mapper(&field.ty);
44        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
45        lines.push(format!("    {}: {}{}", field.name, type_str, comma));
46    }
47    lines.push(") -> Self {".to_string());
48
49    // Body
50    lines.push("    Self {".to_string());
51    for field in &typ.fields {
52        lines.push(format!("        {},", field.name));
53    }
54    lines.push("    }".to_string());
55    lines.push("}".to_string());
56
57    lines.join("\n")
58}
59
60/// Generate NAPI constructor that applies defaults for missing optional fields.
61pub fn gen_napi_defaults_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
62    let mut lines = Vec::new();
63    lines.push("pub fn new(mut env: napi::Env, obj: napi::Object) -> napi::Result<Self> {".to_string());
64
65    // Field assignments with defaults
66    for field in &typ.fields {
67        let type_str = type_mapper(&field.ty);
68        let default_str = default_value_for_field(field, "rust");
69        lines.push(format!(
70            "    let {}: {} = obj.get(\"{}\").unwrap_or({})?;",
71            field.name, type_str, field.name, default_str
72        ));
73    }
74
75    lines.push("    Ok(Self {".to_string());
76    for field in &typ.fields {
77        lines.push(format!("        {},", field.name));
78    }
79    lines.push("    })".to_string());
80    lines.push("}".to_string());
81
82    lines.join("\n")
83}
84
85/// Generate Go functional options pattern for a type with `has_default`.
86/// Returns: type definition + Option type + WithField functions + NewConfig constructor
87pub fn gen_go_functional_options(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
88    let mut lines = Vec::new();
89
90    // Type definition
91    lines.push(format!("// {} is a configuration type.", typ.name));
92    lines.push(format!("type {} struct {{", typ.name));
93    for field in &typ.fields {
94        if is_tuple_field(field) {
95            continue;
96        }
97        let go_type = type_mapper(&field.ty);
98        lines.push(format!("    {} {}", field.name.to_pascal_case(), go_type));
99    }
100    lines.push("}".to_string());
101    lines.push("".to_string());
102
103    // Option function type
104    lines.push(format!(
105        "// {}Option is a functional option for {}.",
106        typ.name, typ.name
107    ));
108    lines.push(format!("type {}Option func(*{})", typ.name, typ.name));
109    lines.push("".to_string());
110
111    // WithField functions
112    for field in &typ.fields {
113        if is_tuple_field(field) {
114            continue;
115        }
116        let option_name = format!("With{}{}", typ.name, field.name.to_pascal_case());
117        let go_type = type_mapper(&field.ty);
118        lines.push(format!("// {} sets the {}.", option_name, field.name));
119        lines.push(format!("func {}(val {}) {}Option {{", option_name, go_type, typ.name));
120        lines.push(format!("    return func(c *{}) {{", typ.name));
121        lines.push(format!("        c.{} = val", field.name.to_pascal_case()));
122        lines.push("    }".to_string());
123        lines.push("}".to_string());
124        lines.push("".to_string());
125    }
126
127    // New constructor
128    lines.push(format!(
129        "// New{} creates a new {} with default values and applies options.",
130        typ.name, typ.name
131    ));
132    lines.push(format!(
133        "func New{}(opts ...{}Option) *{} {{",
134        typ.name, typ.name, typ.name
135    ));
136    lines.push(format!("    c := &{} {{", typ.name));
137    for field in &typ.fields {
138        if is_tuple_field(field) {
139            continue;
140        }
141        let default_str = default_value_for_field(field, "go");
142        lines.push(format!("        {}: {},", field.name.to_pascal_case(), default_str));
143    }
144    lines.push("    }".to_string());
145    lines.push("    for _, opt := range opts {".to_string());
146    lines.push("        opt(c)".to_string());
147    lines.push("    }".to_string());
148    lines.push("    return c".to_string());
149    lines.push("}".to_string());
150
151    lines.join("\n")
152}
153
154/// Generate Java builder pattern for a type with `has_default`.
155/// Returns: Builder inner class with withField methods + build() method
156pub fn gen_java_builder(typ: &TypeDef, package: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
157    let mut lines = Vec::new();
158
159    lines.push(format!(
160        "// DO NOT EDIT - auto-generated by alef\npackage {};\n",
161        package
162    ));
163    lines.push("/// Builder for creating instances of {} with sensible defaults".to_string());
164    lines.push(format!("public class {}Builder {{", typ.name));
165
166    // Fields
167    for field in &typ.fields {
168        let java_type = type_mapper(&field.ty);
169        lines.push(format!("    private {} {};", java_type, field.name.to_lowercase()));
170    }
171    lines.push("".to_string());
172
173    // Constructor
174    lines.push(format!("    public {}Builder() {{", typ.name));
175    for field in &typ.fields {
176        let default_str = default_value_for_field(field, "java");
177        lines.push(format!("        this.{} = {};", field.name.to_lowercase(), default_str));
178    }
179    lines.push("    }".to_string());
180    lines.push("".to_string());
181
182    // withField methods
183    for field in &typ.fields {
184        let java_type = type_mapper(&field.ty);
185        let method_name = format!("with{}", field.name.to_pascal_case());
186        lines.push(format!(
187            "    public {}Builder {}({} value) {{",
188            typ.name, method_name, java_type
189        ));
190        lines.push(format!("        this.{} = value;", field.name.to_lowercase()));
191        lines.push("        return this;".to_string());
192        lines.push("    }".to_string());
193        lines.push("".to_string());
194    }
195
196    // build() method
197    lines.push(format!("    public {} build() {{", typ.name));
198    lines.push(format!("        return new {}(", typ.name));
199    for (i, field) in typ.fields.iter().enumerate() {
200        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
201        lines.push(format!("            this.{}{}", field.name.to_lowercase(), comma));
202    }
203    lines.push("        );".to_string());
204    lines.push("    }".to_string());
205    lines.push("}".to_string());
206
207    lines.join("\n")
208}
209
210/// Generate C# record with init properties for a type with `has_default`.
211pub fn gen_csharp_record(typ: &TypeDef, namespace: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
212    let mut lines = Vec::new();
213
214    lines.push("// This file is auto-generated by alef. DO NOT EDIT.".to_string());
215    lines.push("using System;".to_string());
216    lines.push("".to_string());
217    lines.push(format!("namespace {};\n", namespace));
218
219    lines.push(format!("/// Configuration record: {}", typ.name));
220    lines.push(format!("public record {} {{", typ.name));
221
222    for field in &typ.fields {
223        // Skip tuple struct internals (e.g., _0, _1, etc.)
224        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
225            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
226        {
227            continue;
228        }
229
230        let cs_type = type_mapper(&field.ty);
231        let default_str = default_value_for_field(field, "csharp");
232        lines.push(format!(
233            "    public {} {} {{ get; init; }} = {};",
234            cs_type,
235            field.name.to_pascal_case(),
236            default_str
237        ));
238    }
239
240    lines.push("}".to_string());
241
242    lines.join("\n")
243}
244
245/// Get a language-appropriate default value string for a field.
246/// Uses `typed_default` if available, falls back to `default` string, or type-based zero value.
247pub fn default_value_for_field(field: &FieldDef, language: &str) -> String {
248    // First try typed_default if it exists
249    if let Some(typed_default) = &field.typed_default {
250        return match typed_default {
251            DefaultValue::BoolLiteral(b) => match language {
252                "python" => {
253                    if *b {
254                        "True".to_string()
255                    } else {
256                        "False".to_string()
257                    }
258                }
259                "ruby" => {
260                    if *b {
261                        "true".to_string()
262                    } else {
263                        "false".to_string()
264                    }
265                }
266                "go" => {
267                    if *b {
268                        "true".to_string()
269                    } else {
270                        "false".to_string()
271                    }
272                }
273                "java" => {
274                    if *b {
275                        "true".to_string()
276                    } else {
277                        "false".to_string()
278                    }
279                }
280                "csharp" => {
281                    if *b {
282                        "true".to_string()
283                    } else {
284                        "false".to_string()
285                    }
286                }
287                "php" => {
288                    if *b {
289                        "true".to_string()
290                    } else {
291                        "false".to_string()
292                    }
293                }
294                "r" => {
295                    if *b {
296                        "TRUE".to_string()
297                    } else {
298                        "FALSE".to_string()
299                    }
300                }
301                "rust" => {
302                    if *b {
303                        "true".to_string()
304                    } else {
305                        "false".to_string()
306                    }
307                }
308                _ => {
309                    if *b {
310                        "true".to_string()
311                    } else {
312                        "false".to_string()
313                    }
314                }
315            },
316            DefaultValue::StringLiteral(s) => match language {
317                "rust" => format!("\"{}\".to_string()", s.replace('"', "\\\"")),
318                _ => format!("\"{}\"", s.replace('"', "\\\"")),
319            },
320            DefaultValue::IntLiteral(n) => n.to_string(),
321            DefaultValue::FloatLiteral(f) => {
322                let s = f.to_string();
323                if !s.contains('.') { format!("{}.0", s) } else { s }
324            }
325            DefaultValue::EnumVariant(v) => {
326                // When the field's original enum type was excluded/sanitized and mapped to
327                // String, we must emit a string literal rather than an enum type path.
328                // Example: OutputFormat::Plain → "plain".to_string() (Rust), "plain" (others).
329                if matches!(field.ty, TypeRef::String) {
330                    let snake = v.to_snake_case();
331                    return match language {
332                        "rust" => format!("\"{}\".to_string()", snake),
333                        _ => format!("\"{}\"", snake),
334                    };
335                }
336                match language {
337                    "python" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
338                    "ruby" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
339                    "go" => format!("{}{}", field.ty.type_name(), v.to_pascal_case()),
340                    "java" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
341                    "csharp" => format!("{}.{}", field.ty.type_name(), v.to_pascal_case()),
342                    "php" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
343                    "r" => format!("{}${}", field.ty.type_name(), v.to_pascal_case()),
344                    "rust" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
345                    _ => v.clone(),
346                }
347            }
348            DefaultValue::Empty => {
349                // Empty means "type's default" — check field type to pick the right zero value
350                match &field.ty {
351                    TypeRef::Vec(_) => match language {
352                        "python" | "ruby" | "csharp" => "[]".to_string(),
353                        "go" => "nil".to_string(),
354                        "java" => "List.of()".to_string(),
355                        "php" => "[]".to_string(),
356                        "r" => "c()".to_string(),
357                        "rust" => "vec![]".to_string(),
358                        _ => "null".to_string(),
359                    },
360                    TypeRef::Map(_, _) => match language {
361                        "python" => "{}".to_string(),
362                        "go" => "nil".to_string(),
363                        "java" => "Map.of()".to_string(),
364                        "rust" => "Default::default()".to_string(),
365                        _ => "null".to_string(),
366                    },
367                    TypeRef::Primitive(p) => match p {
368                        PrimitiveType::Bool => match language {
369                            "python" => "False".to_string(),
370                            "ruby" => "false".to_string(),
371                            _ => "false".to_string(),
372                        },
373                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
374                        _ => "0".to_string(),
375                    },
376                    TypeRef::String | TypeRef::Char | TypeRef::Path => match language {
377                        "rust" => "String::new()".to_string(),
378                        _ => "\"\"".to_string(),
379                    },
380                    TypeRef::Json => match language {
381                        "python" | "ruby" => "{}".to_string(),
382                        "go" => "json.RawMessage(nil)".to_string(),
383                        "java" => "new com.fasterxml.jackson.databind.node.ObjectNode(null)".to_string(),
384                        "csharp" => "JObject.Parse(\"{}\")".to_string(),
385                        "php" => "[]".to_string(),
386                        "r" => "list()".to_string(),
387                        "rust" => "serde_json::json!({})".to_string(),
388                        _ => "{}".to_string(),
389                    },
390                    TypeRef::Duration => "0".to_string(),
391                    TypeRef::Bytes => match language {
392                        "python" => "b\"\"".to_string(),
393                        "go" => "[]byte{}".to_string(),
394                        "rust" => "vec![]".to_string(),
395                        _ => "\"\"".to_string(),
396                    },
397                    _ => match language {
398                        "python" => "None".to_string(),
399                        "ruby" => "nil".to_string(),
400                        "go" => "nil".to_string(),
401                        "rust" => "Default::default()".to_string(),
402                        _ => "null".to_string(),
403                    },
404                }
405            }
406            DefaultValue::None => match language {
407                "python" => "None".to_string(),
408                "ruby" => "nil".to_string(),
409                "go" => "nil".to_string(),
410                "java" => "null".to_string(),
411                "csharp" => "null".to_string(),
412                "php" => "null".to_string(),
413                "r" => "NULL".to_string(),
414                "rust" => "None".to_string(),
415                _ => "null".to_string(),
416            },
417        };
418    }
419
420    // Fall back to string default if it exists
421    if let Some(default_str) = &field.default {
422        return default_str.clone();
423    }
424
425    // Final fallback: type-based zero value
426    match &field.ty {
427        TypeRef::Primitive(p) => match p {
428            alef_core::ir::PrimitiveType::Bool => match language {
429                "python" => "False".to_string(),
430                "ruby" => "false".to_string(),
431                "csharp" => "false".to_string(),
432                "java" => "false".to_string(),
433                "php" => "false".to_string(),
434                "r" => "FALSE".to_string(),
435                _ => "false".to_string(),
436            },
437            alef_core::ir::PrimitiveType::U8
438            | alef_core::ir::PrimitiveType::U16
439            | alef_core::ir::PrimitiveType::U32
440            | alef_core::ir::PrimitiveType::U64
441            | alef_core::ir::PrimitiveType::I8
442            | alef_core::ir::PrimitiveType::I16
443            | alef_core::ir::PrimitiveType::I32
444            | alef_core::ir::PrimitiveType::I64
445            | alef_core::ir::PrimitiveType::Usize
446            | alef_core::ir::PrimitiveType::Isize => "0".to_string(),
447            alef_core::ir::PrimitiveType::F32 | alef_core::ir::PrimitiveType::F64 => "0.0".to_string(),
448        },
449        TypeRef::String | TypeRef::Char => match language {
450            "python" => "\"\"".to_string(),
451            "ruby" => "\"\"".to_string(),
452            "go" => "\"\"".to_string(),
453            "java" => "\"\"".to_string(),
454            "csharp" => "\"\"".to_string(),
455            "php" => "\"\"".to_string(),
456            "r" => "\"\"".to_string(),
457            "rust" => "String::new()".to_string(),
458            _ => "\"\"".to_string(),
459        },
460        TypeRef::Bytes => match language {
461            "python" => "b\"\"".to_string(),
462            "ruby" => "\"\"".to_string(),
463            "go" => "[]byte{}".to_string(),
464            "java" => "new byte[]{}".to_string(),
465            "csharp" => "new byte[]{}".to_string(),
466            "php" => "\"\"".to_string(),
467            "r" => "raw()".to_string(),
468            "rust" => "vec![]".to_string(),
469            _ => "[]".to_string(),
470        },
471        TypeRef::Optional(_) => match language {
472            "python" => "None".to_string(),
473            "ruby" => "nil".to_string(),
474            "go" => "nil".to_string(),
475            "java" => "null".to_string(),
476            "csharp" => "null".to_string(),
477            "php" => "null".to_string(),
478            "r" => "NULL".to_string(),
479            "rust" => "None".to_string(),
480            _ => "null".to_string(),
481        },
482        TypeRef::Vec(_) => match language {
483            "python" => "[]".to_string(),
484            "ruby" => "[]".to_string(),
485            "go" => "[]interface{}{}".to_string(),
486            "java" => "new java.util.ArrayList<>()".to_string(),
487            "csharp" => "[]".to_string(),
488            "php" => "[]".to_string(),
489            "r" => "c()".to_string(),
490            "rust" => "vec![]".to_string(),
491            _ => "[]".to_string(),
492        },
493        TypeRef::Map(_, _) => match language {
494            "python" => "{}".to_string(),
495            "ruby" => "{}".to_string(),
496            "go" => "make(map[string]interface{})".to_string(),
497            "java" => "new java.util.HashMap<>()".to_string(),
498            "csharp" => "new Dictionary<string, object>()".to_string(),
499            "php" => "[]".to_string(),
500            "r" => "list()".to_string(),
501            "rust" => "std::collections::HashMap::new()".to_string(),
502            _ => "{}".to_string(),
503        },
504        TypeRef::Json => match language {
505            "python" => "{}".to_string(),
506            "ruby" => "{}".to_string(),
507            "go" => "json.RawMessage(nil)".to_string(),
508            "java" => "new com.fasterxml.jackson.databind.JsonNode()".to_string(),
509            "csharp" => "JObject.Parse(\"{}\")".to_string(),
510            "php" => "[]".to_string(),
511            "r" => "list()".to_string(),
512            "rust" => "serde_json::json!({})".to_string(),
513            _ => "{}".to_string(),
514        },
515        TypeRef::Named(name) => match language {
516            "rust" => format!("{name}::default()"),
517            "python" => "None".to_string(),
518            "ruby" => "nil".to_string(),
519            "go" => "nil".to_string(),
520            "java" => "null".to_string(),
521            "csharp" => "null".to_string(),
522            "php" => "null".to_string(),
523            "r" => "NULL".to_string(),
524            _ => "null".to_string(),
525        },
526        _ => match language {
527            "python" => "None".to_string(),
528            "ruby" => "nil".to_string(),
529            "go" => "nil".to_string(),
530            "java" => "null".to_string(),
531            "csharp" => "null".to_string(),
532            "php" => "null".to_string(),
533            "r" => "NULL".to_string(),
534            "rust" => "Default::default()".to_string(),
535            _ => "null".to_string(),
536        },
537    }
538}
539
540// Helper trait extension for TypeRef to get type name
541trait TypeRefExt {
542    fn type_name(&self) -> String;
543}
544
545impl TypeRefExt for TypeRef {
546    fn type_name(&self) -> String {
547        match self {
548            TypeRef::Named(n) => n.clone(),
549            TypeRef::Primitive(p) => format!("{:?}", p),
550            TypeRef::String | TypeRef::Char => "String".to_string(),
551            TypeRef::Bytes => "Bytes".to_string(),
552            TypeRef::Optional(inner) => format!("Option<{}>", inner.type_name()),
553            TypeRef::Vec(inner) => format!("Vec<{}>", inner.type_name()),
554            TypeRef::Map(k, v) => format!("Map<{}, {}>", k.type_name(), v.type_name()),
555            TypeRef::Path => "Path".to_string(),
556            TypeRef::Unit => "()".to_string(),
557            TypeRef::Json => "Json".to_string(),
558            TypeRef::Duration => "Duration".to_string(),
559        }
560    }
561}
562
563/// The maximum arity supported by Magnus `function!` macro.
564const MAGNUS_MAX_ARITY: usize = 15;
565
566/// Generate a Magnus (Ruby) kwargs constructor for a type with `has_default`.
567///
568/// For types with <=15 fields, generates a positional `Option<T>` parameter constructor.
569/// For types with >15 fields (exceeding Magnus arity limit), generates a hash-based constructor
570/// using `RHash` that extracts fields by name, applying defaults for missing keys.
571pub fn gen_magnus_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
572    if typ.fields.len() > MAGNUS_MAX_ARITY {
573        gen_magnus_hash_constructor(typ, type_mapper)
574    } else {
575        gen_magnus_positional_constructor(typ, type_mapper)
576    }
577}
578
579/// Wrap a type string for use as a type-path prefix in Rust.
580///
581/// Types containing `<` (generics like `Vec<String>`, `Option<T>`) cannot be used as
582/// `Vec<String>::try_convert(v)` — that's a parse error. They must use the UFCS form
583/// `<Vec<String>>::try_convert(v)` instead. Simple names like `String`, `bool` can use
584/// `String::try_convert(v)` directly.
585fn as_type_path_prefix(type_str: &str) -> String {
586    if type_str.contains('<') {
587        format!("<{type_str}>")
588    } else {
589        type_str.to_string()
590    }
591}
592
593/// Generate a hash-based Magnus constructor for types with many fields.
594/// Accepts `(kwargs: RHash)` and extracts each field by symbol name, applying defaults.
595fn gen_magnus_hash_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
596    use std::fmt::Write;
597    let mut out = String::with_capacity(1024);
598
599    writeln!(out, "fn new(kwargs: magnus::RHash) -> Result<Self, magnus::Error> {{").ok();
600    writeln!(out, "    let ruby = unsafe {{ magnus::Ruby::get_unchecked() }};").ok();
601    writeln!(out, "    Ok(Self {{").ok();
602
603    for field in &typ.fields {
604        let is_optional = field_is_optional_in_rust(field);
605        // Use inner type for try_convert, since the hash value is T, not Option<T>.
606        // When field.ty is already Optional(T) and field.optional is true, strip one layer so we
607        // call <T>::try_convert, not <Option<T>>::try_convert (which would yield Option<Option<T>>).
608        let effective_inner_ty = match &field.ty {
609            TypeRef::Optional(inner) if is_optional => inner.as_ref(),
610            ty => ty,
611        };
612        let inner_type = type_mapper(effective_inner_ty);
613        let type_prefix = as_type_path_prefix(&inner_type);
614        if is_optional {
615            // Field is Option<T>: extract from hash, wrap in Some, default to None
616            writeln!(
617                out,
618                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()),",
619                name = field.name,
620                type_prefix = type_prefix,
621            ).ok();
622        } else if use_unwrap_or_default(field) {
623            writeln!(
624                out,
625                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()).unwrap_or_default(),",
626                name = field.name,
627                type_prefix = type_prefix,
628            ).ok();
629        } else {
630            // When the binding maps the field type to String (e.g. an excluded enum), but the
631            // original default is an EnumVariant, `default_value_for_field` would emit
632            // `TypeName::Variant` which is invalid for a `String` field. Fall back to the
633            // string-literal form in that case.
634            let default_str = if inner_type == "String" {
635                if let Some(DefaultValue::EnumVariant(variant)) = &field.typed_default {
636                    use heck::ToSnakeCase;
637                    format!("\"{}\".to_string()", variant.to_snake_case())
638                } else {
639                    default_value_for_field(field, "rust")
640                }
641            } else {
642                default_value_for_field(field, "rust")
643            };
644            writeln!(
645                out,
646                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()).unwrap_or({default}),",
647                name = field.name,
648                type_prefix = type_prefix,
649                default = default_str,
650            ).ok();
651        }
652    }
653
654    writeln!(out, "    }})").ok();
655    writeln!(out, "}}").ok();
656
657    out
658}
659
660/// Returns true if the generated Rust field type is already `Option<T>`.
661/// This covers both:
662/// - Fields with `optional: true` (the Rust field type becomes `Option<inner_type>`)
663/// - Fields whose `TypeRef` is explicitly `Optional(_)` (rare, for nested Option types)
664fn field_is_optional_in_rust(field: &FieldDef) -> bool {
665    field.optional || matches!(&field.ty, TypeRef::Optional(_))
666}
667
668/// Generate a positional Magnus constructor for types with <=15 fields.
669/// Uses `Option<T>` parameters and applies defaults in the body.
670fn gen_magnus_positional_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
671    use std::fmt::Write;
672    let mut out = String::with_capacity(512);
673
674    writeln!(out, "fn new(").ok();
675
676    // All params are Option<T> so Ruby users can pass nil for any field.
677    // If the Rust field type is already Option<T> (via optional:true or TypeRef::Optional),
678    // use that type directly (avoids Option<Option<T>>).
679    for (i, field) in typ.fields.iter().enumerate() {
680        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
681        let is_optional = field_is_optional_in_rust(field);
682        if is_optional {
683            // Strip one Optional wrapper when ty is Optional(T) AND field is marked optional,
684            // to avoid emitting Option<Option<T>>. The param represents Option<inner>, not
685            // Option<Option<inner>>.
686            let effective_inner_ty = match &field.ty {
687                TypeRef::Optional(inner) => inner.as_ref(),
688                ty => ty,
689            };
690            let inner_type = type_mapper(effective_inner_ty);
691            writeln!(out, "    {}: Option<{}>{}", field.name, inner_type, comma).ok();
692        } else {
693            let field_type = type_mapper(&field.ty);
694            writeln!(out, "    {}: Option<{}>{}", field.name, field_type, comma).ok();
695        }
696    }
697
698    writeln!(out, ") -> Self {{").ok();
699    writeln!(out, "    Self {{").ok();
700
701    for field in &typ.fields {
702        let is_optional = field_is_optional_in_rust(field);
703        if is_optional {
704            // The Rust field is Option<T>; param is Option<T>; assign directly.
705            writeln!(out, "        {},", field.name).ok();
706        } else if use_unwrap_or_default(field) {
707            writeln!(out, "        {}: {}.unwrap_or_default(),", field.name, field.name).ok();
708        } else {
709            let default_str = default_value_for_field(field, "rust");
710            writeln!(
711                out,
712                "        {}: {}.unwrap_or({}),",
713                field.name, field.name, default_str
714            )
715            .ok();
716        }
717    }
718
719    writeln!(out, "    }}").ok();
720    writeln!(out, "}}").ok();
721
722    out
723}
724
725/// Generate a PHP kwargs constructor for a type with `has_default`.
726/// All fields become `Option<T>` parameters so PHP users can omit any field.
727/// Assignments wrap non-Optional fields in `Some()` and apply defaults.
728pub fn gen_php_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
729    use std::fmt::Write;
730    let mut out = String::with_capacity(512);
731
732    writeln!(out, "pub fn __construct(").ok();
733
734    // All params are Option<MappedType> — PHP users can omit any field
735    for (i, field) in typ.fields.iter().enumerate() {
736        let mapped = type_mapper(&field.ty);
737        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
738        writeln!(out, "    {}: Option<{}>{}", field.name, mapped, comma).ok();
739    }
740
741    writeln!(out, ") -> Self {{").ok();
742    writeln!(out, "    Self {{").ok();
743
744    for field in &typ.fields {
745        let is_optional_field = field.optional || matches!(&field.ty, TypeRef::Optional(_));
746        if is_optional_field {
747            // Struct field is Option<T>, param is Option<T> — pass through directly
748            writeln!(out, "        {},", field.name).ok();
749        } else if use_unwrap_or_default(field) {
750            // Struct field is T, param is Option<T> — unwrap with type's default
751            writeln!(out, "        {}: {}.unwrap_or_default(),", field.name, field.name).ok();
752        } else {
753            // Struct field is T, param is Option<T> — unwrap with explicit default
754            let default_str = default_value_for_field(field, "rust");
755            writeln!(
756                out,
757                "        {}: {}.unwrap_or({}),",
758                field.name, field.name, default_str
759            )
760            .ok();
761        }
762    }
763
764    writeln!(out, "    }}").ok();
765    writeln!(out, "}}").ok();
766
767    out
768}
769
770/// Generate a Rustler (Elixir) kwargs constructor for a type with `has_default`.
771/// Accepts keyword list or map, applies defaults for missing fields.
772pub fn gen_rustler_kwargs_constructor(typ: &TypeDef, _type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
773    use std::fmt::Write;
774    let mut out = String::with_capacity(512);
775
776    // NifStruct already handles keyword list conversion, but we generate
777    // an explicit constructor wrapper that applies defaults.
778    writeln!(
779        out,
780        "pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {{"
781    )
782    .ok();
783    writeln!(out, "    Self {{").ok();
784
785    // Field assignments with defaults from opts.
786    // Optional fields (Option<T>) need special handling: decode the inner type
787    // directly so we get Option<T> from and_then, with no unwrap_or needed.
788    for field in &typ.fields {
789        if field.optional {
790            // Field type is Option<T>. Decode inner T from the Term, yielding Option<T>.
791            writeln!(
792                out,
793                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()),",
794                field.name, field.name
795            )
796            .ok();
797        } else if use_unwrap_or_default(field) {
798            writeln!(
799                out,
800                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
801                field.name, field.name
802            )
803            .ok();
804        } else {
805            let default_str = default_value_for_field(field, "rust");
806            // Check if the default value looks like an enum variant (e.g., "OutputFormat::Plain")
807            // which wouldn't work for String types. If so, use unwrap_or_default() instead.
808            let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
809
810            if is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char) {
811                // Use default for String types with enum-like defaults
812                writeln!(
813                    out,
814                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
815                    field.name, field.name
816                )
817                .ok();
818            } else if matches!(&field.ty, TypeRef::Named(_)) {
819                // For other Named types, use unwrap_or_default() since the binding type may differ
820                // from the core type (e.g., excluded enums become String).
821                writeln!(
822                    out,
823                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
824                    field.name, field.name
825                )
826                .ok();
827            } else {
828                writeln!(
829                    out,
830                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
831                    field.name, field.name, default_str
832                )
833                .ok();
834            }
835        }
836    }
837
838    writeln!(out, "    }}").ok();
839    writeln!(out, "}}").ok();
840
841    out
842}
843
844/// Generate an extendr (R) kwargs constructor for a type with `has_default`.
845///
846/// Rust does not support function-parameter defaults, and extendr 0.9 only allows
847/// defaults via the per-parameter `#[extendr(default = "...")]` attribute (not via
848/// `param: T = expr` syntax).  Rather than encode every default in attribute form,
849/// we accept each field as `Option<T>` and unwrap it via `T::default()` (or via the
850/// type's own `Default::default()` for the whole struct as the base) inside the body.
851/// The R-side wrapper generated in `generate_public_api` already supplies named
852/// arguments with `NULL` defaults, so callers see ergonomic kwargs at the R level.
853pub fn gen_extendr_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
854    use std::fmt::Write;
855    let mut out = String::with_capacity(512);
856
857    writeln!(out, "#[extendr]").ok();
858    writeln!(out, "pub fn new_{}(", typ.name.to_lowercase()).ok();
859
860    // Add all fields as Option<T> parameters — extendr passes NULL → None which lets
861    // each field fall through to the struct's Default value when omitted.
862    for (i, field) in typ.fields.iter().enumerate() {
863        let field_type = type_mapper(&field.ty);
864        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
865        writeln!(out, "    {}: Option<{}>{}", field.name, field_type, comma).ok();
866    }
867
868    writeln!(out, ") -> {} {{", typ.name).ok();
869    // Use the type's Default impl as a base so unspecified fields keep their natural
870    // defaults rather than `Default::default()` for each individual field type (which
871    // would, for example, give an empty String for fields whose true default is
872    // `"utf-8"`).  Then overlay any caller-provided values.
873    writeln!(out, "    let mut __out = <{}>::default();", typ.name).ok();
874    for field in &typ.fields {
875        writeln!(
876            out,
877            "    if let Some(v) = {name} {{ __out.{name} = v; }}",
878            name = field.name
879        )
880        .ok();
881    }
882    writeln!(out, "    __out").ok();
883    writeln!(out, "}}").ok();
884
885    out
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891    use alef_core::ir::{CoreWrapper, FieldDef, PrimitiveType, TypeRef};
892
893    fn make_test_type() -> TypeDef {
894        TypeDef {
895            name: "Config".to_string(),
896            rust_path: "my_crate::Config".to_string(),
897            original_rust_path: String::new(),
898            fields: vec![
899                FieldDef {
900                    name: "timeout".to_string(),
901                    ty: TypeRef::Primitive(PrimitiveType::U64),
902                    optional: false,
903                    default: Some("30".to_string()),
904                    doc: "Timeout in seconds".to_string(),
905                    sanitized: false,
906                    is_boxed: false,
907                    type_rust_path: None,
908                    cfg: None,
909                    typed_default: Some(DefaultValue::IntLiteral(30)),
910                    core_wrapper: CoreWrapper::None,
911                    vec_inner_core_wrapper: CoreWrapper::None,
912                    newtype_wrapper: None,
913                },
914                FieldDef {
915                    name: "enabled".to_string(),
916                    ty: TypeRef::Primitive(PrimitiveType::Bool),
917                    optional: false,
918                    default: None,
919                    doc: "Enable feature".to_string(),
920                    sanitized: false,
921                    is_boxed: false,
922                    type_rust_path: None,
923                    cfg: None,
924                    typed_default: Some(DefaultValue::BoolLiteral(true)),
925                    core_wrapper: CoreWrapper::None,
926                    vec_inner_core_wrapper: CoreWrapper::None,
927                    newtype_wrapper: None,
928                },
929                FieldDef {
930                    name: "name".to_string(),
931                    ty: TypeRef::String,
932                    optional: false,
933                    default: None,
934                    doc: "Config name".to_string(),
935                    sanitized: false,
936                    is_boxed: false,
937                    type_rust_path: None,
938                    cfg: None,
939                    typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
940                    core_wrapper: CoreWrapper::None,
941                    vec_inner_core_wrapper: CoreWrapper::None,
942                    newtype_wrapper: None,
943                },
944            ],
945            methods: vec![],
946            is_opaque: false,
947            is_clone: true,
948            is_copy: false,
949            doc: "Configuration type".to_string(),
950            cfg: None,
951            is_trait: false,
952            has_default: true,
953            has_stripped_cfg_fields: false,
954            is_return_type: false,
955            serde_rename_all: None,
956            has_serde: false,
957            super_traits: vec![],
958        }
959    }
960
961    #[test]
962    fn test_default_value_bool_true_python() {
963        let field = FieldDef {
964            name: "enabled".to_string(),
965            ty: TypeRef::Primitive(PrimitiveType::Bool),
966            optional: false,
967            default: None,
968            doc: String::new(),
969            sanitized: false,
970            is_boxed: false,
971            type_rust_path: None,
972            cfg: None,
973            typed_default: Some(DefaultValue::BoolLiteral(true)),
974            core_wrapper: CoreWrapper::None,
975            vec_inner_core_wrapper: CoreWrapper::None,
976            newtype_wrapper: None,
977        };
978        assert_eq!(default_value_for_field(&field, "python"), "True");
979    }
980
981    #[test]
982    fn test_default_value_bool_false_go() {
983        let field = FieldDef {
984            name: "enabled".to_string(),
985            ty: TypeRef::Primitive(PrimitiveType::Bool),
986            optional: false,
987            default: None,
988            doc: String::new(),
989            sanitized: false,
990            is_boxed: false,
991            type_rust_path: None,
992            cfg: None,
993            typed_default: Some(DefaultValue::BoolLiteral(false)),
994            core_wrapper: CoreWrapper::None,
995            vec_inner_core_wrapper: CoreWrapper::None,
996            newtype_wrapper: None,
997        };
998        assert_eq!(default_value_for_field(&field, "go"), "false");
999    }
1000
1001    #[test]
1002    fn test_default_value_string_literal() {
1003        let field = FieldDef {
1004            name: "name".to_string(),
1005            ty: TypeRef::String,
1006            optional: false,
1007            default: None,
1008            doc: String::new(),
1009            sanitized: false,
1010            is_boxed: false,
1011            type_rust_path: None,
1012            cfg: None,
1013            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1014            core_wrapper: CoreWrapper::None,
1015            vec_inner_core_wrapper: CoreWrapper::None,
1016            newtype_wrapper: None,
1017        };
1018        assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
1019        assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
1020    }
1021
1022    #[test]
1023    fn test_default_value_int_literal() {
1024        let field = FieldDef {
1025            name: "timeout".to_string(),
1026            ty: TypeRef::Primitive(PrimitiveType::U64),
1027            optional: false,
1028            default: None,
1029            doc: String::new(),
1030            sanitized: false,
1031            is_boxed: false,
1032            type_rust_path: None,
1033            cfg: None,
1034            typed_default: Some(DefaultValue::IntLiteral(42)),
1035            core_wrapper: CoreWrapper::None,
1036            vec_inner_core_wrapper: CoreWrapper::None,
1037            newtype_wrapper: None,
1038        };
1039        let result = default_value_for_field(&field, "python");
1040        assert_eq!(result, "42");
1041    }
1042
1043    #[test]
1044    fn test_default_value_none() {
1045        let field = FieldDef {
1046            name: "maybe".to_string(),
1047            ty: TypeRef::Optional(Box::new(TypeRef::String)),
1048            optional: true,
1049            default: None,
1050            doc: String::new(),
1051            sanitized: false,
1052            is_boxed: false,
1053            type_rust_path: None,
1054            cfg: None,
1055            typed_default: Some(DefaultValue::None),
1056            core_wrapper: CoreWrapper::None,
1057            vec_inner_core_wrapper: CoreWrapper::None,
1058            newtype_wrapper: None,
1059        };
1060        assert_eq!(default_value_for_field(&field, "python"), "None");
1061        assert_eq!(default_value_for_field(&field, "go"), "nil");
1062        assert_eq!(default_value_for_field(&field, "java"), "null");
1063        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1064    }
1065
1066    #[test]
1067    fn test_default_value_fallback_string() {
1068        let field = FieldDef {
1069            name: "name".to_string(),
1070            ty: TypeRef::String,
1071            optional: false,
1072            default: Some("\"custom\"".to_string()),
1073            doc: String::new(),
1074            sanitized: false,
1075            is_boxed: false,
1076            type_rust_path: None,
1077            cfg: None,
1078            typed_default: None,
1079            core_wrapper: CoreWrapper::None,
1080            vec_inner_core_wrapper: CoreWrapper::None,
1081            newtype_wrapper: None,
1082        };
1083        assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1084    }
1085
1086    #[test]
1087    fn test_gen_pyo3_kwargs_constructor() {
1088        let typ = make_test_type();
1089        let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1090            TypeRef::Primitive(p) => format!("{:?}", p),
1091            TypeRef::String | TypeRef::Char => "str".to_string(),
1092            _ => "Any".to_string(),
1093        });
1094
1095        assert!(output.contains("#[new]"));
1096        assert!(output.contains("#[pyo3(signature = ("));
1097        assert!(output.contains("timeout=30"));
1098        assert!(output.contains("enabled=True"));
1099        assert!(output.contains("name=\"default\""));
1100        assert!(output.contains("fn new("));
1101    }
1102
1103    #[test]
1104    fn test_gen_napi_defaults_constructor() {
1105        let typ = make_test_type();
1106        let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1107            TypeRef::Primitive(p) => format!("{:?}", p),
1108            TypeRef::String | TypeRef::Char => "String".to_string(),
1109            _ => "Value".to_string(),
1110        });
1111
1112        assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1113        assert!(output.contains("timeout"));
1114        assert!(output.contains("enabled"));
1115        assert!(output.contains("name"));
1116    }
1117
1118    #[test]
1119    fn test_gen_go_functional_options() {
1120        let typ = make_test_type();
1121        let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1122            TypeRef::Primitive(p) => match p {
1123                PrimitiveType::U64 => "uint64".to_string(),
1124                PrimitiveType::Bool => "bool".to_string(),
1125                _ => "interface{}".to_string(),
1126            },
1127            TypeRef::String | TypeRef::Char => "string".to_string(),
1128            _ => "interface{}".to_string(),
1129        });
1130
1131        assert!(output.contains("type Config struct {"));
1132        assert!(output.contains("type ConfigOption func(*Config)"));
1133        assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1134        assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1135        assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1136        assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1137    }
1138
1139    #[test]
1140    fn test_gen_java_builder() {
1141        let typ = make_test_type();
1142        let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1143            TypeRef::Primitive(p) => match p {
1144                PrimitiveType::U64 => "long".to_string(),
1145                PrimitiveType::Bool => "boolean".to_string(),
1146                _ => "int".to_string(),
1147            },
1148            TypeRef::String | TypeRef::Char => "String".to_string(),
1149            _ => "Object".to_string(),
1150        });
1151
1152        assert!(output.contains("package dev.test;"));
1153        assert!(output.contains("public class ConfigBuilder"));
1154        assert!(output.contains("withTimeout"));
1155        assert!(output.contains("withEnabled"));
1156        assert!(output.contains("withName"));
1157        assert!(output.contains("public Config build()"));
1158    }
1159
1160    #[test]
1161    fn test_gen_csharp_record() {
1162        let typ = make_test_type();
1163        let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1164            TypeRef::Primitive(p) => match p {
1165                PrimitiveType::U64 => "ulong".to_string(),
1166                PrimitiveType::Bool => "bool".to_string(),
1167                _ => "int".to_string(),
1168            },
1169            TypeRef::String | TypeRef::Char => "string".to_string(),
1170            _ => "object".to_string(),
1171        });
1172
1173        assert!(output.contains("namespace MyNamespace;"));
1174        assert!(output.contains("public record Config"));
1175        assert!(output.contains("public ulong Timeout"));
1176        assert!(output.contains("public bool Enabled"));
1177        assert!(output.contains("public string Name"));
1178        assert!(output.contains("init;"));
1179    }
1180
1181    #[test]
1182    fn test_default_value_float_literal() {
1183        let field = FieldDef {
1184            name: "ratio".to_string(),
1185            ty: TypeRef::Primitive(PrimitiveType::F64),
1186            optional: false,
1187            default: None,
1188            doc: String::new(),
1189            sanitized: false,
1190            is_boxed: false,
1191            type_rust_path: None,
1192            cfg: None,
1193            typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1194            core_wrapper: CoreWrapper::None,
1195            vec_inner_core_wrapper: CoreWrapper::None,
1196            newtype_wrapper: None,
1197        };
1198        let result = default_value_for_field(&field, "python");
1199        assert!(result.contains("1.5"));
1200    }
1201
1202    #[test]
1203    fn test_default_value_no_typed_no_default() {
1204        let field = FieldDef {
1205            name: "count".to_string(),
1206            ty: TypeRef::Primitive(PrimitiveType::U32),
1207            optional: false,
1208            default: None,
1209            doc: String::new(),
1210            sanitized: false,
1211            is_boxed: false,
1212            type_rust_path: None,
1213            cfg: None,
1214            typed_default: None,
1215            core_wrapper: CoreWrapper::None,
1216            vec_inner_core_wrapper: CoreWrapper::None,
1217            newtype_wrapper: None,
1218        };
1219        // Should fall back to type-based zero value
1220        assert_eq!(default_value_for_field(&field, "python"), "0");
1221        assert_eq!(default_value_for_field(&field, "go"), "0");
1222    }
1223
1224    fn make_field(name: &str, ty: TypeRef) -> FieldDef {
1225        FieldDef {
1226            name: name.to_string(),
1227            ty,
1228            optional: false,
1229            default: None,
1230            doc: String::new(),
1231            sanitized: false,
1232            is_boxed: false,
1233            type_rust_path: None,
1234            cfg: None,
1235            typed_default: None,
1236            core_wrapper: CoreWrapper::None,
1237            vec_inner_core_wrapper: CoreWrapper::None,
1238            newtype_wrapper: None,
1239        }
1240    }
1241
1242    fn simple_type_mapper(tr: &TypeRef) -> String {
1243        match tr {
1244            TypeRef::Primitive(p) => match p {
1245                PrimitiveType::U64 => "u64".to_string(),
1246                PrimitiveType::Bool => "bool".to_string(),
1247                PrimitiveType::U32 => "u32".to_string(),
1248                _ => "i64".to_string(),
1249            },
1250            TypeRef::String | TypeRef::Char => "String".to_string(),
1251            TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
1252            TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
1253            TypeRef::Named(n) => n.clone(),
1254            _ => "Value".to_string(),
1255        }
1256    }
1257
1258    // -------------------------------------------------------------------------
1259    // default_value_for_field — untested branches
1260    // -------------------------------------------------------------------------
1261
1262    #[test]
1263    fn test_default_value_bool_literal_ruby() {
1264        let field = FieldDef {
1265            name: "flag".to_string(),
1266            ty: TypeRef::Primitive(PrimitiveType::Bool),
1267            optional: false,
1268            default: None,
1269            doc: String::new(),
1270            sanitized: false,
1271            is_boxed: false,
1272            type_rust_path: None,
1273            cfg: None,
1274            typed_default: Some(DefaultValue::BoolLiteral(true)),
1275            core_wrapper: CoreWrapper::None,
1276            vec_inner_core_wrapper: CoreWrapper::None,
1277            newtype_wrapper: None,
1278        };
1279        assert_eq!(default_value_for_field(&field, "ruby"), "true");
1280        assert_eq!(default_value_for_field(&field, "php"), "true");
1281        assert_eq!(default_value_for_field(&field, "csharp"), "true");
1282        assert_eq!(default_value_for_field(&field, "java"), "true");
1283        assert_eq!(default_value_for_field(&field, "rust"), "true");
1284    }
1285
1286    #[test]
1287    fn test_default_value_bool_literal_r() {
1288        let field = FieldDef {
1289            name: "flag".to_string(),
1290            ty: TypeRef::Primitive(PrimitiveType::Bool),
1291            optional: false,
1292            default: None,
1293            doc: String::new(),
1294            sanitized: false,
1295            is_boxed: false,
1296            type_rust_path: None,
1297            cfg: None,
1298            typed_default: Some(DefaultValue::BoolLiteral(false)),
1299            core_wrapper: CoreWrapper::None,
1300            vec_inner_core_wrapper: CoreWrapper::None,
1301            newtype_wrapper: None,
1302        };
1303        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1304    }
1305
1306    #[test]
1307    fn test_default_value_string_literal_rust() {
1308        let field = FieldDef {
1309            name: "label".to_string(),
1310            ty: TypeRef::String,
1311            optional: false,
1312            default: None,
1313            doc: String::new(),
1314            sanitized: false,
1315            is_boxed: false,
1316            type_rust_path: None,
1317            cfg: None,
1318            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1319            core_wrapper: CoreWrapper::None,
1320            vec_inner_core_wrapper: CoreWrapper::None,
1321            newtype_wrapper: None,
1322        };
1323        assert_eq!(default_value_for_field(&field, "rust"), "\"hello\".to_string()");
1324    }
1325
1326    #[test]
1327    fn test_default_value_string_literal_escapes_quotes() {
1328        let field = FieldDef {
1329            name: "label".to_string(),
1330            ty: TypeRef::String,
1331            optional: false,
1332            default: None,
1333            doc: String::new(),
1334            sanitized: false,
1335            is_boxed: false,
1336            type_rust_path: None,
1337            cfg: None,
1338            typed_default: Some(DefaultValue::StringLiteral("say \"hi\"".to_string())),
1339            core_wrapper: CoreWrapper::None,
1340            vec_inner_core_wrapper: CoreWrapper::None,
1341            newtype_wrapper: None,
1342        };
1343        assert_eq!(default_value_for_field(&field, "python"), "\"say \\\"hi\\\"\"");
1344    }
1345
1346    #[test]
1347    fn test_default_value_float_literal_whole_number() {
1348        // A whole-number float should be rendered with ".0" suffix.
1349        let field = FieldDef {
1350            name: "scale".to_string(),
1351            ty: TypeRef::Primitive(PrimitiveType::F32),
1352            optional: false,
1353            default: None,
1354            doc: String::new(),
1355            sanitized: false,
1356            is_boxed: false,
1357            type_rust_path: None,
1358            cfg: None,
1359            typed_default: Some(DefaultValue::FloatLiteral(2.0)),
1360            core_wrapper: CoreWrapper::None,
1361            vec_inner_core_wrapper: CoreWrapper::None,
1362            newtype_wrapper: None,
1363        };
1364        let result = default_value_for_field(&field, "python");
1365        assert!(result.contains('.'), "whole-number float should contain '.': {result}");
1366    }
1367
1368    #[test]
1369    fn test_default_value_enum_variant_per_language() {
1370        let field = FieldDef {
1371            name: "format".to_string(),
1372            ty: TypeRef::Named("OutputFormat".to_string()),
1373            optional: false,
1374            default: None,
1375            doc: String::new(),
1376            sanitized: false,
1377            is_boxed: false,
1378            type_rust_path: None,
1379            cfg: None,
1380            typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
1381            core_wrapper: CoreWrapper::None,
1382            vec_inner_core_wrapper: CoreWrapper::None,
1383            newtype_wrapper: None,
1384        };
1385        assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
1386        assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
1387        assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
1388        assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
1389        assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
1390        assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
1391        assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
1392        assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
1393    }
1394
1395    #[test]
1396    fn test_default_value_empty_vec_per_language() {
1397        let field = FieldDef {
1398            name: "items".to_string(),
1399            ty: TypeRef::Vec(Box::new(TypeRef::String)),
1400            optional: false,
1401            default: None,
1402            doc: String::new(),
1403            sanitized: false,
1404            is_boxed: false,
1405            type_rust_path: None,
1406            cfg: None,
1407            typed_default: Some(DefaultValue::Empty),
1408            core_wrapper: CoreWrapper::None,
1409            vec_inner_core_wrapper: CoreWrapper::None,
1410            newtype_wrapper: None,
1411        };
1412        assert_eq!(default_value_for_field(&field, "python"), "[]");
1413        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1414        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1415        assert_eq!(default_value_for_field(&field, "go"), "nil");
1416        assert_eq!(default_value_for_field(&field, "java"), "List.of()");
1417        assert_eq!(default_value_for_field(&field, "php"), "[]");
1418        assert_eq!(default_value_for_field(&field, "r"), "c()");
1419        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1420    }
1421
1422    #[test]
1423    fn test_default_value_empty_map_per_language() {
1424        let field = FieldDef {
1425            name: "meta".to_string(),
1426            ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1427            optional: false,
1428            default: None,
1429            doc: String::new(),
1430            sanitized: false,
1431            is_boxed: false,
1432            type_rust_path: None,
1433            cfg: None,
1434            typed_default: Some(DefaultValue::Empty),
1435            core_wrapper: CoreWrapper::None,
1436            vec_inner_core_wrapper: CoreWrapper::None,
1437            newtype_wrapper: None,
1438        };
1439        assert_eq!(default_value_for_field(&field, "python"), "{}");
1440        assert_eq!(default_value_for_field(&field, "go"), "nil");
1441        assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
1442        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1443    }
1444
1445    #[test]
1446    fn test_default_value_empty_bool_primitive() {
1447        let field = FieldDef {
1448            name: "flag".to_string(),
1449            ty: TypeRef::Primitive(PrimitiveType::Bool),
1450            optional: false,
1451            default: None,
1452            doc: String::new(),
1453            sanitized: false,
1454            is_boxed: false,
1455            type_rust_path: None,
1456            cfg: None,
1457            typed_default: Some(DefaultValue::Empty),
1458            core_wrapper: CoreWrapper::None,
1459            vec_inner_core_wrapper: CoreWrapper::None,
1460            newtype_wrapper: None,
1461        };
1462        assert_eq!(default_value_for_field(&field, "python"), "False");
1463        assert_eq!(default_value_for_field(&field, "ruby"), "false");
1464        assert_eq!(default_value_for_field(&field, "go"), "false");
1465    }
1466
1467    #[test]
1468    fn test_default_value_empty_float_primitive() {
1469        let field = FieldDef {
1470            name: "ratio".to_string(),
1471            ty: TypeRef::Primitive(PrimitiveType::F64),
1472            optional: false,
1473            default: None,
1474            doc: String::new(),
1475            sanitized: false,
1476            is_boxed: false,
1477            type_rust_path: None,
1478            cfg: None,
1479            typed_default: Some(DefaultValue::Empty),
1480            core_wrapper: CoreWrapper::None,
1481            vec_inner_core_wrapper: CoreWrapper::None,
1482            newtype_wrapper: None,
1483        };
1484        assert_eq!(default_value_for_field(&field, "python"), "0.0");
1485    }
1486
1487    #[test]
1488    fn test_default_value_empty_string_type() {
1489        let field = FieldDef {
1490            name: "label".to_string(),
1491            ty: TypeRef::String,
1492            optional: false,
1493            default: None,
1494            doc: String::new(),
1495            sanitized: false,
1496            is_boxed: false,
1497            type_rust_path: None,
1498            cfg: None,
1499            typed_default: Some(DefaultValue::Empty),
1500            core_wrapper: CoreWrapper::None,
1501            vec_inner_core_wrapper: CoreWrapper::None,
1502            newtype_wrapper: None,
1503        };
1504        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1505        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1506    }
1507
1508    #[test]
1509    fn test_default_value_empty_bytes_type() {
1510        let field = FieldDef {
1511            name: "data".to_string(),
1512            ty: TypeRef::Bytes,
1513            optional: false,
1514            default: None,
1515            doc: String::new(),
1516            sanitized: false,
1517            is_boxed: false,
1518            type_rust_path: None,
1519            cfg: None,
1520            typed_default: Some(DefaultValue::Empty),
1521            core_wrapper: CoreWrapper::None,
1522            vec_inner_core_wrapper: CoreWrapper::None,
1523            newtype_wrapper: None,
1524        };
1525        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1526        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1527        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1528    }
1529
1530    #[test]
1531    fn test_default_value_empty_json_type() {
1532        let field = FieldDef {
1533            name: "payload".to_string(),
1534            ty: TypeRef::Json,
1535            optional: false,
1536            default: None,
1537            doc: String::new(),
1538            sanitized: false,
1539            is_boxed: false,
1540            type_rust_path: None,
1541            cfg: None,
1542            typed_default: Some(DefaultValue::Empty),
1543            core_wrapper: CoreWrapper::None,
1544            vec_inner_core_wrapper: CoreWrapper::None,
1545            newtype_wrapper: None,
1546        };
1547        assert_eq!(default_value_for_field(&field, "python"), "{}");
1548        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1549        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1550        assert_eq!(default_value_for_field(&field, "r"), "list()");
1551        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1552    }
1553
1554    #[test]
1555    fn test_default_value_none_ruby_php_r() {
1556        let field = FieldDef {
1557            name: "maybe".to_string(),
1558            ty: TypeRef::Optional(Box::new(TypeRef::String)),
1559            optional: true,
1560            default: None,
1561            doc: String::new(),
1562            sanitized: false,
1563            is_boxed: false,
1564            type_rust_path: None,
1565            cfg: None,
1566            typed_default: Some(DefaultValue::None),
1567            core_wrapper: CoreWrapper::None,
1568            vec_inner_core_wrapper: CoreWrapper::None,
1569            newtype_wrapper: None,
1570        };
1571        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1572        assert_eq!(default_value_for_field(&field, "php"), "null");
1573        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1574        assert_eq!(default_value_for_field(&field, "rust"), "None");
1575    }
1576
1577    // -------------------------------------------------------------------------
1578    // Fallback (no typed_default, no default) — type-based zero values
1579    // -------------------------------------------------------------------------
1580
1581    #[test]
1582    fn test_default_value_fallback_bool_all_languages() {
1583        let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
1584        assert_eq!(default_value_for_field(&field, "python"), "False");
1585        assert_eq!(default_value_for_field(&field, "ruby"), "false");
1586        assert_eq!(default_value_for_field(&field, "csharp"), "false");
1587        assert_eq!(default_value_for_field(&field, "java"), "false");
1588        assert_eq!(default_value_for_field(&field, "php"), "false");
1589        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1590        assert_eq!(default_value_for_field(&field, "rust"), "false");
1591    }
1592
1593    #[test]
1594    fn test_default_value_fallback_float() {
1595        let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
1596        assert_eq!(default_value_for_field(&field, "python"), "0.0");
1597        assert_eq!(default_value_for_field(&field, "rust"), "0.0");
1598    }
1599
1600    #[test]
1601    fn test_default_value_fallback_string_all_languages() {
1602        let field = make_field("name", TypeRef::String);
1603        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1604        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1605        assert_eq!(default_value_for_field(&field, "go"), "\"\"");
1606        assert_eq!(default_value_for_field(&field, "java"), "\"\"");
1607        assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
1608        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1609        assert_eq!(default_value_for_field(&field, "r"), "\"\"");
1610        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1611    }
1612
1613    #[test]
1614    fn test_default_value_fallback_bytes_all_languages() {
1615        let field = make_field("data", TypeRef::Bytes);
1616        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1617        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1618        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1619        assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
1620        assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
1621        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1622        assert_eq!(default_value_for_field(&field, "r"), "raw()");
1623        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1624    }
1625
1626    #[test]
1627    fn test_default_value_fallback_optional() {
1628        let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
1629        assert_eq!(default_value_for_field(&field, "python"), "None");
1630        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1631        assert_eq!(default_value_for_field(&field, "go"), "nil");
1632        assert_eq!(default_value_for_field(&field, "java"), "null");
1633        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1634        assert_eq!(default_value_for_field(&field, "php"), "null");
1635        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1636        assert_eq!(default_value_for_field(&field, "rust"), "None");
1637    }
1638
1639    #[test]
1640    fn test_default_value_fallback_vec_all_languages() {
1641        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
1642        assert_eq!(default_value_for_field(&field, "python"), "[]");
1643        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1644        assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
1645        assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
1646        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1647        assert_eq!(default_value_for_field(&field, "php"), "[]");
1648        assert_eq!(default_value_for_field(&field, "r"), "c()");
1649        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1650    }
1651
1652    #[test]
1653    fn test_default_value_fallback_map_all_languages() {
1654        let field = make_field(
1655            "meta",
1656            TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1657        );
1658        assert_eq!(default_value_for_field(&field, "python"), "{}");
1659        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1660        assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
1661        assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
1662        assert_eq!(
1663            default_value_for_field(&field, "csharp"),
1664            "new Dictionary<string, object>()"
1665        );
1666        assert_eq!(default_value_for_field(&field, "php"), "[]");
1667        assert_eq!(default_value_for_field(&field, "r"), "list()");
1668        assert_eq!(
1669            default_value_for_field(&field, "rust"),
1670            "std::collections::HashMap::new()"
1671        );
1672    }
1673
1674    #[test]
1675    fn test_default_value_fallback_json_all_languages() {
1676        let field = make_field("payload", TypeRef::Json);
1677        assert_eq!(default_value_for_field(&field, "python"), "{}");
1678        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1679        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1680        assert_eq!(default_value_for_field(&field, "r"), "list()");
1681        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1682    }
1683
1684    #[test]
1685    fn test_default_value_fallback_named_type() {
1686        let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
1687        assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
1688        assert_eq!(default_value_for_field(&field, "python"), "None");
1689        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1690        assert_eq!(default_value_for_field(&field, "go"), "nil");
1691        assert_eq!(default_value_for_field(&field, "java"), "null");
1692        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1693        assert_eq!(default_value_for_field(&field, "php"), "null");
1694        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1695    }
1696
1697    #[test]
1698    fn test_default_value_fallback_duration() {
1699        // Duration falls through to the wildcard arm
1700        let field = make_field("timeout", TypeRef::Duration);
1701        assert_eq!(default_value_for_field(&field, "python"), "None");
1702        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1703    }
1704
1705    // -------------------------------------------------------------------------
1706    // gen_magnus_kwargs_constructor — positional (≤15 fields)
1707    // -------------------------------------------------------------------------
1708
1709    #[test]
1710    fn test_gen_magnus_kwargs_constructor_positional_basic() {
1711        let typ = make_test_type();
1712        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1713
1714        assert!(output.contains("fn new("), "should have fn new");
1715        // All params are Option<T>
1716        assert!(output.contains("Option<u64>"), "timeout should be Option<u64>");
1717        assert!(output.contains("Option<bool>"), "enabled should be Option<bool>");
1718        assert!(output.contains("Option<String>"), "name should be Option<String>");
1719        assert!(output.contains("-> Self {"), "should return Self");
1720        // timeout has IntLiteral(30), use_unwrap_or_default is false for Named → uses unwrap_or
1721        assert!(
1722            output.contains("timeout: timeout.unwrap_or(30),"),
1723            "should apply int default"
1724        );
1725        // enabled has BoolLiteral(true), not unwrap_or_default
1726        assert!(
1727            output.contains("enabled: enabled.unwrap_or(true),"),
1728            "should apply bool default"
1729        );
1730        // name has StringLiteral, not unwrap_or_default
1731        assert!(
1732            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1733            "should apply string default"
1734        );
1735    }
1736
1737    #[test]
1738    fn test_gen_magnus_kwargs_constructor_positional_optional_field() {
1739        // A field with optional=true should be assigned directly (no unwrap)
1740        let mut typ = make_test_type();
1741        typ.fields.push(FieldDef {
1742            name: "extra".to_string(),
1743            ty: TypeRef::String,
1744            optional: true,
1745            default: None,
1746            doc: String::new(),
1747            sanitized: false,
1748            is_boxed: false,
1749            type_rust_path: None,
1750            cfg: None,
1751            typed_default: None,
1752            core_wrapper: CoreWrapper::None,
1753            vec_inner_core_wrapper: CoreWrapper::None,
1754            newtype_wrapper: None,
1755        });
1756        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1757        // Optional field param is Option<String> and assigned directly
1758        assert!(output.contains("extra,"), "optional field should be assigned directly");
1759        assert!(!output.contains("extra.unwrap"), "optional field should not use unwrap");
1760    }
1761
1762    #[test]
1763    fn test_gen_magnus_kwargs_constructor_unwrap_or_default() {
1764        // A primitive field with no typed_default and no default should use unwrap_or_default()
1765        let mut typ = make_test_type();
1766        typ.fields.push(FieldDef {
1767            name: "count".to_string(),
1768            ty: TypeRef::Primitive(PrimitiveType::U32),
1769            optional: false,
1770            default: None,
1771            doc: String::new(),
1772            sanitized: false,
1773            is_boxed: false,
1774            type_rust_path: None,
1775            cfg: None,
1776            typed_default: None,
1777            core_wrapper: CoreWrapper::None,
1778            vec_inner_core_wrapper: CoreWrapper::None,
1779            newtype_wrapper: None,
1780        });
1781        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1782        assert!(
1783            output.contains("count: count.unwrap_or_default(),"),
1784            "plain primitive with no default should use unwrap_or_default"
1785        );
1786    }
1787
1788    #[test]
1789    fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
1790        // Build a type with 16 fields (> MAGNUS_MAX_ARITY = 15) to force hash path
1791        let mut fields: Vec<FieldDef> = (0..16)
1792            .map(|i| FieldDef {
1793                name: format!("field_{i}"),
1794                ty: TypeRef::Primitive(PrimitiveType::U32),
1795                optional: false,
1796                default: None,
1797                doc: String::new(),
1798                sanitized: false,
1799                is_boxed: false,
1800                type_rust_path: None,
1801                cfg: None,
1802                typed_default: None,
1803                core_wrapper: CoreWrapper::None,
1804                vec_inner_core_wrapper: CoreWrapper::None,
1805                newtype_wrapper: None,
1806            })
1807            .collect();
1808        // Make one field optional to exercise that branch in the hash constructor
1809        fields[0].optional = true;
1810
1811        let typ = TypeDef {
1812            name: "BigConfig".to_string(),
1813            rust_path: "crate::BigConfig".to_string(),
1814            original_rust_path: String::new(),
1815            fields,
1816            methods: vec![],
1817            is_opaque: false,
1818            is_clone: true,
1819            is_copy: false,
1820            doc: String::new(),
1821            cfg: None,
1822            is_trait: false,
1823            has_default: true,
1824            has_stripped_cfg_fields: false,
1825            is_return_type: false,
1826            serde_rename_all: None,
1827            has_serde: false,
1828            super_traits: vec![],
1829        };
1830        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1831
1832        assert!(output.contains("kwargs: magnus::RHash"), "should accept RHash");
1833        assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
1834        // Optional field uses and_then without unwrap_or
1835        assert!(
1836            output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
1837            "optional field should use and_then"
1838        );
1839        assert!(
1840            output.contains("field_0:").then_some(()).is_some(),
1841            "field_0 should appear in output"
1842        );
1843    }
1844
1845    // -------------------------------------------------------------------------
1846    // gen_php_kwargs_constructor
1847    // -------------------------------------------------------------------------
1848
1849    #[test]
1850    fn test_gen_php_kwargs_constructor_basic() {
1851        let typ = make_test_type();
1852        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1853
1854        assert!(
1855            output.contains("pub fn __construct("),
1856            "should use PHP constructor name"
1857        );
1858        // All params are Option<T>
1859        assert!(
1860            output.contains("timeout: Option<u64>"),
1861            "timeout param should be Option<u64>"
1862        );
1863        assert!(
1864            output.contains("enabled: Option<bool>"),
1865            "enabled param should be Option<bool>"
1866        );
1867        assert!(
1868            output.contains("name: Option<String>"),
1869            "name param should be Option<String>"
1870        );
1871        assert!(output.contains("-> Self {"), "should return Self");
1872        assert!(
1873            output.contains("timeout: timeout.unwrap_or(30),"),
1874            "should apply int default for timeout"
1875        );
1876        assert!(
1877            output.contains("enabled: enabled.unwrap_or(true),"),
1878            "should apply bool default for enabled"
1879        );
1880        assert!(
1881            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1882            "should apply string default for name"
1883        );
1884    }
1885
1886    #[test]
1887    fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
1888        let mut typ = make_test_type();
1889        typ.fields.push(FieldDef {
1890            name: "tag".to_string(),
1891            ty: TypeRef::String,
1892            optional: true,
1893            default: None,
1894            doc: String::new(),
1895            sanitized: false,
1896            is_boxed: false,
1897            type_rust_path: None,
1898            cfg: None,
1899            typed_default: None,
1900            core_wrapper: CoreWrapper::None,
1901            vec_inner_core_wrapper: CoreWrapper::None,
1902            newtype_wrapper: None,
1903        });
1904        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1905        assert!(
1906            output.contains("tag,"),
1907            "optional field should be passed through directly"
1908        );
1909        assert!(!output.contains("tag.unwrap"), "optional field should not call unwrap");
1910    }
1911
1912    #[test]
1913    fn test_gen_php_kwargs_constructor_unwrap_or_default_for_primitive() {
1914        let mut typ = make_test_type();
1915        typ.fields.push(FieldDef {
1916            name: "retries".to_string(),
1917            ty: TypeRef::Primitive(PrimitiveType::U32),
1918            optional: false,
1919            default: None,
1920            doc: String::new(),
1921            sanitized: false,
1922            is_boxed: false,
1923            type_rust_path: None,
1924            cfg: None,
1925            typed_default: None,
1926            core_wrapper: CoreWrapper::None,
1927            vec_inner_core_wrapper: CoreWrapper::None,
1928            newtype_wrapper: None,
1929        });
1930        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1931        assert!(
1932            output.contains("retries: retries.unwrap_or_default(),"),
1933            "primitive with no default should use unwrap_or_default"
1934        );
1935    }
1936
1937    // -------------------------------------------------------------------------
1938    // gen_rustler_kwargs_constructor
1939    // -------------------------------------------------------------------------
1940
1941    #[test]
1942    fn test_gen_rustler_kwargs_constructor_basic() {
1943        let typ = make_test_type();
1944        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
1945
1946        assert!(
1947            output.contains("pub fn new(opts: std::collections::HashMap<String, rustler::Term>)"),
1948            "should accept HashMap of Terms"
1949        );
1950        assert!(output.contains("Self {"), "should construct Self");
1951        // timeout has IntLiteral(30) — explicit unwrap_or
1952        assert!(
1953            output.contains("timeout: opts.get(\"timeout\").and_then(|t| t.decode().ok()).unwrap_or(30),"),
1954            "should apply int default for timeout"
1955        );
1956        // enabled has BoolLiteral(true) — explicit unwrap_or
1957        assert!(
1958            output.contains("enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),"),
1959            "should apply bool default for enabled"
1960        );
1961    }
1962
1963    #[test]
1964    fn test_gen_rustler_kwargs_constructor_optional_field() {
1965        let mut typ = make_test_type();
1966        typ.fields.push(FieldDef {
1967            name: "extra".to_string(),
1968            ty: TypeRef::String,
1969            optional: true,
1970            default: None,
1971            doc: String::new(),
1972            sanitized: false,
1973            is_boxed: false,
1974            type_rust_path: None,
1975            cfg: None,
1976            typed_default: None,
1977            core_wrapper: CoreWrapper::None,
1978            vec_inner_core_wrapper: CoreWrapper::None,
1979            newtype_wrapper: None,
1980        });
1981        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
1982        assert!(
1983            output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
1984            "optional field should decode without unwrap"
1985        );
1986    }
1987
1988    #[test]
1989    fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
1990        let mut typ = make_test_type();
1991        typ.fields.push(FieldDef {
1992            name: "inner".to_string(),
1993            ty: TypeRef::Named("InnerConfig".to_string()),
1994            optional: false,
1995            default: None,
1996            doc: String::new(),
1997            sanitized: false,
1998            is_boxed: false,
1999            type_rust_path: None,
2000            cfg: None,
2001            typed_default: None,
2002            core_wrapper: CoreWrapper::None,
2003            vec_inner_core_wrapper: CoreWrapper::None,
2004            newtype_wrapper: None,
2005        });
2006        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2007        assert!(
2008            output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2009            "Named type with no default should use unwrap_or_default"
2010        );
2011    }
2012
2013    #[test]
2014    fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
2015        // A String field with a StringLiteral default contains "::", triggering the
2016        // is_enum_variant_default check — should fall back to unwrap_or_default().
2017        let mut typ = make_test_type();
2018        // 'name' field in make_test_type() has StringLiteral("default") — verify it
2019        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2020        assert!(
2021            output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2022            "String field with quoted default should use unwrap_or_default"
2023        );
2024        // Also verify a plain string field (no default) also falls through to unwrap_or_default
2025        typ.fields.push(FieldDef {
2026            name: "label".to_string(),
2027            ty: TypeRef::String,
2028            optional: false,
2029            default: None,
2030            doc: String::new(),
2031            sanitized: false,
2032            is_boxed: false,
2033            type_rust_path: None,
2034            cfg: None,
2035            typed_default: None,
2036            core_wrapper: CoreWrapper::None,
2037            vec_inner_core_wrapper: CoreWrapper::None,
2038            newtype_wrapper: None,
2039        });
2040        let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2041        assert!(
2042            output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2043            "String field with no default should use unwrap_or_default"
2044        );
2045    }
2046
2047    // -------------------------------------------------------------------------
2048    // gen_extendr_kwargs_constructor
2049    // -------------------------------------------------------------------------
2050
2051    #[test]
2052    fn test_gen_extendr_kwargs_constructor_basic() {
2053        let typ = make_test_type();
2054        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper);
2055
2056        assert!(output.contains("#[extendr]"), "should have extendr attribute");
2057        assert!(
2058            output.contains("pub fn new_config("),
2059            "function name should be lowercase type name"
2060        );
2061        // Fields appear as Option<T> parameters — Rust does not support param defaults.
2062        assert!(
2063            output.contains("timeout: Option<u64>"),
2064            "should accept timeout as Option<u64>: {output}"
2065        );
2066        assert!(
2067            output.contains("enabled: Option<bool>"),
2068            "should accept enabled as Option<bool>: {output}"
2069        );
2070        assert!(
2071            output.contains("name: Option<String>"),
2072            "should accept name as Option<String>: {output}"
2073        );
2074        assert!(output.contains("-> Config {"), "should return Config");
2075        assert!(
2076            output.contains("let mut __out = <Config>::default();"),
2077            "should base on Default impl: {output}"
2078        );
2079        assert!(
2080            output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
2081            "should overlay caller-provided timeout"
2082        );
2083        assert!(
2084            output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
2085            "should overlay caller-provided enabled"
2086        );
2087        assert!(
2088            output.contains("if let Some(v) = name { __out.name = v; }"),
2089            "should overlay caller-provided name"
2090        );
2091    }
2092
2093    #[test]
2094    fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
2095        // Rust function-parameter defaults (`x: T = expr`) are a syntax error and
2096        // extendr 0.9 only supports defaults via the `#[extendr(default = "...")]`
2097        // attribute.  Verify that no field is emitted with a Rust-syntax default.
2098        let typ = make_test_type();
2099        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper);
2100        assert!(
2101            !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
2102            "constructor must not use Rust-syntax param defaults: {output}"
2103        );
2104    }
2105
2106    // -------------------------------------------------------------------------
2107    // gen_go_functional_options — tuple-field filtering
2108    // -------------------------------------------------------------------------
2109
2110    #[test]
2111    fn test_gen_go_functional_options_skips_tuple_fields() {
2112        let mut typ = make_test_type();
2113        typ.fields.push(FieldDef {
2114            name: "_0".to_string(),
2115            ty: TypeRef::Primitive(PrimitiveType::U32),
2116            optional: false,
2117            default: None,
2118            doc: String::new(),
2119            sanitized: false,
2120            is_boxed: false,
2121            type_rust_path: None,
2122            cfg: None,
2123            typed_default: None,
2124            core_wrapper: CoreWrapper::None,
2125            vec_inner_core_wrapper: CoreWrapper::None,
2126            newtype_wrapper: None,
2127        });
2128        let output = gen_go_functional_options(&typ, &simple_type_mapper);
2129        assert!(
2130            !output.contains("_0"),
2131            "tuple field _0 should be filtered out from Go output"
2132        );
2133    }
2134
2135    // -------------------------------------------------------------------------
2136    // as_type_path_prefix — tested indirectly through hash constructor
2137    // -------------------------------------------------------------------------
2138
2139    #[test]
2140    fn test_gen_magnus_hash_constructor_generic_type_prefix() {
2141        // A field with a Vec type should use <Vec<...>>::try_convert UFCS form
2142        let fields: Vec<FieldDef> = (0..16)
2143            .map(|i| FieldDef {
2144                name: format!("field_{i}"),
2145                ty: if i == 0 {
2146                    TypeRef::Vec(Box::new(TypeRef::String))
2147                } else {
2148                    TypeRef::Primitive(PrimitiveType::U32)
2149                },
2150                optional: false,
2151                default: None,
2152                doc: String::new(),
2153                sanitized: false,
2154                is_boxed: false,
2155                type_rust_path: None,
2156                cfg: None,
2157                typed_default: None,
2158                core_wrapper: CoreWrapper::None,
2159                vec_inner_core_wrapper: CoreWrapper::None,
2160                newtype_wrapper: None,
2161            })
2162            .collect();
2163        let typ = TypeDef {
2164            name: "WideConfig".to_string(),
2165            rust_path: "crate::WideConfig".to_string(),
2166            original_rust_path: String::new(),
2167            fields,
2168            methods: vec![],
2169            is_opaque: false,
2170            is_clone: true,
2171            is_copy: false,
2172            doc: String::new(),
2173            cfg: None,
2174            is_trait: false,
2175            has_default: true,
2176            has_stripped_cfg_fields: false,
2177            is_return_type: false,
2178            serde_rename_all: None,
2179            has_serde: false,
2180            super_traits: vec![],
2181        };
2182        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2183        // Vec<String> is a generic type; must use <Vec<String>>::try_convert
2184        assert!(
2185            output.contains("<Vec<String>>::try_convert"),
2186            "generic types should use UFCS angle-bracket prefix: {output}"
2187        );
2188    }
2189
2190    // -------------------------------------------------------------------------
2191    // Bug B regression: Option<Option<T>> must not appear when field.optional==true
2192    // and field.ty==Optional(T). This happens for "Update" structs where the core
2193    // field is Option<Option<T>> — the binding flattens to Option<T>.
2194    // -------------------------------------------------------------------------
2195
2196    #[test]
2197    fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
2198        // field with optional=true AND ty=Optional(Usize) — represents a core Option<Option<usize>>
2199        // that should flatten to Option<usize> in the binding constructor.
2200        // simple_type_mapper maps Usize → "i64" (catch-all primitive arm).
2201        let field = FieldDef {
2202            name: "max_depth".to_string(),
2203            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2204            optional: true,
2205            default: None,
2206            doc: String::new(),
2207            sanitized: false,
2208            is_boxed: false,
2209            type_rust_path: None,
2210            cfg: None,
2211            typed_default: None,
2212            core_wrapper: CoreWrapper::None,
2213            vec_inner_core_wrapper: CoreWrapper::None,
2214            newtype_wrapper: None,
2215        };
2216        // Build a large type (>15 fields) so the hash constructor is used
2217        let mut fields: Vec<FieldDef> = (0..15)
2218            .map(|i| FieldDef {
2219                name: format!("field_{i}"),
2220                ty: TypeRef::Primitive(PrimitiveType::U32),
2221                optional: false,
2222                default: None,
2223                doc: String::new(),
2224                sanitized: false,
2225                is_boxed: false,
2226                type_rust_path: None,
2227                cfg: None,
2228                typed_default: None,
2229                core_wrapper: CoreWrapper::None,
2230                vec_inner_core_wrapper: CoreWrapper::None,
2231                newtype_wrapper: None,
2232            })
2233            .collect();
2234        fields.push(field);
2235        let typ = TypeDef {
2236            name: "UpdateConfig".to_string(),
2237            rust_path: "crate::UpdateConfig".to_string(),
2238            original_rust_path: String::new(),
2239            fields,
2240            methods: vec![],
2241            is_opaque: false,
2242            is_clone: true,
2243            is_copy: false,
2244            doc: String::new(),
2245            cfg: None,
2246            is_trait: false,
2247            has_default: true,
2248            has_stripped_cfg_fields: false,
2249            is_return_type: false,
2250            serde_rename_all: None,
2251            has_serde: false,
2252            super_traits: vec![],
2253        };
2254        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2255        // The try_convert call must be for the inner type (i64, as mapped by simple_type_mapper),
2256        // not Option<i64> (which would yield Option<Option<i64>>).
2257        assert!(
2258            !output.contains("Option<Option<"),
2259            "hash constructor must not emit double Option: {output}"
2260        );
2261        assert!(
2262            output.contains("i64::try_convert"),
2263            "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
2264        );
2265    }
2266
2267    #[test]
2268    fn test_magnus_positional_constructor_no_double_option_when_ty_is_optional() {
2269        // field with optional=true AND ty=Optional(Usize) — small type uses positional constructor
2270        // simple_type_mapper maps Usize → "i64"
2271        let field = FieldDef {
2272            name: "max_depth".to_string(),
2273            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2274            optional: true,
2275            default: None,
2276            doc: String::new(),
2277            sanitized: false,
2278            is_boxed: false,
2279            type_rust_path: None,
2280            cfg: None,
2281            typed_default: None,
2282            core_wrapper: CoreWrapper::None,
2283            vec_inner_core_wrapper: CoreWrapper::None,
2284            newtype_wrapper: None,
2285        };
2286        let typ = TypeDef {
2287            name: "SmallUpdate".to_string(),
2288            rust_path: "crate::SmallUpdate".to_string(),
2289            original_rust_path: String::new(),
2290            fields: vec![field],
2291            methods: vec![],
2292            is_opaque: false,
2293            is_clone: true,
2294            is_copy: false,
2295            doc: String::new(),
2296            cfg: None,
2297            is_trait: false,
2298            has_default: true,
2299            has_stripped_cfg_fields: false,
2300            is_return_type: false,
2301            serde_rename_all: None,
2302            has_serde: false,
2303            super_traits: vec![],
2304        };
2305        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2306        // simple_type_mapper maps Usize → "i64", so Optional(Usize) → "Option<i64>"
2307        // The param must be Option<i64>, never Option<Option<i64>>.
2308        assert!(
2309            !output.contains("Option<Option<"),
2310            "positional constructor must not emit double Option: {output}"
2311        );
2312        assert!(
2313            output.contains("Option<i64>"),
2314            "positional constructor should emit Option<inner> for optional Optional(T): {output}"
2315        );
2316    }
2317}