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    // Hash-based constructor accepts 0 or 1 arguments using scan_args
600    writeln!(out, "fn new(args: &[magnus::Value]) -> Result<Self, magnus::Error> {{").ok();
601    writeln!(out, "    let ruby = unsafe {{ magnus::Ruby::get_unchecked() }};").ok();
602    writeln!(
603        out,
604        "    let args = magnus::scan_args::scan_args::<(), (Option<magnus::RHash>,), (), (), (), ()>(args)?;"
605    )
606    .ok();
607    writeln!(out, "    let (kwargs_opt,) = args.optional;").ok();
608    writeln!(out, "    let kwargs = kwargs_opt.unwrap_or_else(|| ruby.hash_new());").ok();
609    writeln!(out, "    Ok(Self {{").ok();
610
611    for field in &typ.fields {
612        let is_optional = field_is_optional_in_rust(field);
613        // Use inner type for try_convert, since the hash value is T, not Option<T>.
614        // When field.ty is already Optional(T) and field.optional is true, strip one layer so we
615        // call <T>::try_convert, not <Option<T>>::try_convert (which would yield Option<Option<T>>).
616        let effective_inner_ty = match &field.ty {
617            TypeRef::Optional(inner) if is_optional => inner.as_ref(),
618            ty => ty,
619        };
620        let inner_type = type_mapper(effective_inner_ty);
621        let type_prefix = as_type_path_prefix(&inner_type);
622        if is_optional {
623            // Field is Option<T>: extract from hash, wrap in Some, default to None
624            writeln!(
625                out,
626                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()),",
627                name = field.name,
628                type_prefix = type_prefix,
629            ).ok();
630        } else if use_unwrap_or_default(field) {
631            writeln!(
632                out,
633                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()).unwrap_or_default(),",
634                name = field.name,
635                type_prefix = type_prefix,
636            ).ok();
637        } else {
638            // When the binding maps the field type to String (e.g. an excluded enum), but the
639            // original default is an EnumVariant, `default_value_for_field` would emit
640            // `TypeName::Variant` which is invalid for a `String` field. Fall back to the
641            // string-literal form in that case.
642            let default_str = if inner_type == "String" {
643                if let Some(DefaultValue::EnumVariant(variant)) = &field.typed_default {
644                    use heck::ToSnakeCase;
645                    format!("\"{}\".to_string()", variant.to_snake_case())
646                } else {
647                    default_value_for_field(field, "rust")
648                }
649            } else {
650                default_value_for_field(field, "rust")
651            };
652            writeln!(
653                out,
654                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()).unwrap_or({default}),",
655                name = field.name,
656                type_prefix = type_prefix,
657                default = default_str,
658            ).ok();
659        }
660    }
661
662    writeln!(out, "    }})").ok();
663    writeln!(out, "}}").ok();
664
665    out
666}
667
668/// Returns true if the generated Rust field type is already `Option<T>`.
669/// This covers both:
670/// - Fields with `optional: true` (the Rust field type becomes `Option<inner_type>`)
671/// - Fields whose `TypeRef` is explicitly `Optional(_)` (rare, for nested Option types)
672fn field_is_optional_in_rust(field: &FieldDef) -> bool {
673    field.optional || matches!(&field.ty, TypeRef::Optional(_))
674}
675
676/// Generate a positional Magnus constructor for types with <=15 fields.
677/// Uses `Option<T>` parameters and applies defaults in the body.
678fn gen_magnus_positional_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
679    use std::fmt::Write;
680    let mut out = String::with_capacity(512);
681
682    writeln!(out, "fn new(").ok();
683
684    // All params are Option<T> so Ruby users can pass nil for any field.
685    // If the Rust field type is already Option<T> (via optional:true or TypeRef::Optional),
686    // use that type directly (avoids Option<Option<T>>).
687    for (i, field) in typ.fields.iter().enumerate() {
688        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
689        let is_optional = field_is_optional_in_rust(field);
690        if is_optional {
691            // Strip one Optional wrapper when ty is Optional(T) AND field is marked optional,
692            // to avoid emitting Option<Option<T>>. The param represents Option<inner>, not
693            // Option<Option<inner>>.
694            let effective_inner_ty = match &field.ty {
695                TypeRef::Optional(inner) => inner.as_ref(),
696                ty => ty,
697            };
698            let inner_type = type_mapper(effective_inner_ty);
699            writeln!(out, "    {}: Option<{}>{}", field.name, inner_type, comma).ok();
700        } else {
701            let field_type = type_mapper(&field.ty);
702            writeln!(out, "    {}: Option<{}>{}", field.name, field_type, comma).ok();
703        }
704    }
705
706    writeln!(out, ") -> Self {{").ok();
707    writeln!(out, "    Self {{").ok();
708
709    for field in &typ.fields {
710        let is_optional = field_is_optional_in_rust(field);
711        if is_optional {
712            // The Rust field is Option<T>; param is Option<T>; assign directly.
713            writeln!(out, "        {},", field.name).ok();
714        } else if use_unwrap_or_default(field) {
715            writeln!(out, "        {}: {}.unwrap_or_default(),", field.name, field.name).ok();
716        } else {
717            let default_str = default_value_for_field(field, "rust");
718            writeln!(
719                out,
720                "        {}: {}.unwrap_or({}),",
721                field.name, field.name, default_str
722            )
723            .ok();
724        }
725    }
726
727    writeln!(out, "    }}").ok();
728    writeln!(out, "}}").ok();
729
730    out
731}
732
733/// Generate a PHP kwargs constructor for a type with `has_default`.
734/// All fields become `Option<T>` parameters so PHP users can omit any field.
735/// Assignments wrap non-Optional fields in `Some()` and apply defaults.
736pub fn gen_php_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
737    use std::fmt::Write;
738    let mut out = String::with_capacity(512);
739
740    writeln!(out, "pub fn __construct(").ok();
741
742    // All params are Option<MappedType> — PHP users can omit any field
743    for (i, field) in typ.fields.iter().enumerate() {
744        let mapped = type_mapper(&field.ty);
745        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
746        writeln!(out, "    {}: Option<{}>{}", field.name, mapped, comma).ok();
747    }
748
749    writeln!(out, ") -> Self {{").ok();
750    writeln!(out, "    Self {{").ok();
751
752    for field in &typ.fields {
753        let is_optional_field = field.optional || matches!(&field.ty, TypeRef::Optional(_));
754        if is_optional_field {
755            // Struct field is Option<T>, param is Option<T> — pass through directly
756            writeln!(out, "        {},", field.name).ok();
757        } else if use_unwrap_or_default(field) {
758            // Struct field is T, param is Option<T> — unwrap with type's default
759            writeln!(out, "        {}: {}.unwrap_or_default(),", field.name, field.name).ok();
760        } else {
761            // Struct field is T, param is Option<T> — unwrap with explicit default
762            let default_str = default_value_for_field(field, "rust");
763            writeln!(
764                out,
765                "        {}: {}.unwrap_or({}),",
766                field.name, field.name, default_str
767            )
768            .ok();
769        }
770    }
771
772    writeln!(out, "    }}").ok();
773    writeln!(out, "}}").ok();
774
775    out
776}
777
778/// Generate a Rustler (Elixir) kwargs constructor for a type with `has_default`.
779/// Accepts keyword list or map, applies defaults for missing fields.
780/// Fields in `exclude_fields` are skipped (used for bridge fields that cannot implement Encoder/Decoder).
781pub fn gen_rustler_kwargs_constructor_with_exclude(
782    typ: &TypeDef,
783    _type_mapper: &dyn Fn(&TypeRef) -> String,
784    exclude_fields: &std::collections::HashSet<String>,
785) -> String {
786    use std::fmt::Write;
787    let mut out = String::with_capacity(512);
788
789    writeln!(
790        out,
791        "pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {{"
792    )
793    .ok();
794    writeln!(out, "    Self {{").ok();
795
796    for field in &typ.fields {
797        if exclude_fields.contains(&field.name) {
798            continue;
799        }
800        if field.optional {
801            writeln!(
802                out,
803                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()),",
804                field.name, field.name
805            )
806            .ok();
807        } else if use_unwrap_or_default(field) {
808            writeln!(
809                out,
810                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
811                field.name, field.name
812            )
813            .ok();
814        } else {
815            let default_str = default_value_for_field(field, "rust");
816            let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
817
818            if (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
819                || matches!(&field.ty, TypeRef::Named(_))
820            {
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 a Rustler (Elixir) kwargs constructor for a type with `has_default`.
845/// Accepts keyword list or map, applies defaults for missing fields.
846pub fn gen_rustler_kwargs_constructor(typ: &TypeDef, _type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
847    use std::fmt::Write;
848    let mut out = String::with_capacity(512);
849
850    // NifStruct already handles keyword list conversion, but we generate
851    // an explicit constructor wrapper that applies defaults.
852    writeln!(
853        out,
854        "pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {{"
855    )
856    .ok();
857    writeln!(out, "    Self {{").ok();
858
859    // Field assignments with defaults from opts.
860    // Optional fields (Option<T>) need special handling: decode the inner type
861    // directly so we get Option<T> from and_then, with no unwrap_or needed.
862    for field in &typ.fields {
863        if field.optional {
864            // Field type is Option<T>. Decode inner T from the Term, yielding Option<T>.
865            writeln!(
866                out,
867                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()),",
868                field.name, field.name
869            )
870            .ok();
871        } else if use_unwrap_or_default(field) {
872            writeln!(
873                out,
874                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
875                field.name, field.name
876            )
877            .ok();
878        } else {
879            let default_str = default_value_for_field(field, "rust");
880            // Check if the default value looks like an enum variant (e.g., "OutputFormat::Plain")
881            // which wouldn't work for String types. If so, use unwrap_or_default() instead.
882            let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
883
884            if is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char) {
885                // Use default for String types with enum-like defaults
886                writeln!(
887                    out,
888                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
889                    field.name, field.name
890                )
891                .ok();
892            } else if matches!(&field.ty, TypeRef::Named(_)) {
893                // For other Named types, use unwrap_or_default() since the binding type may differ
894                // from the core type (e.g., excluded enums become String).
895                writeln!(
896                    out,
897                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
898                    field.name, field.name
899                )
900                .ok();
901            } else {
902                writeln!(
903                    out,
904                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
905                    field.name, field.name, default_str
906                )
907                .ok();
908            }
909        }
910    }
911
912    writeln!(out, "    }}").ok();
913    writeln!(out, "}}").ok();
914
915    out
916}
917
918/// Generate an extendr (R) kwargs constructor for a type with `has_default`.
919///
920/// Rust does not support function-parameter defaults, and extendr 0.9 only allows
921/// defaults via the per-parameter `#[extendr(default = "...")]` attribute (not via
922/// `param: T = expr` syntax).  Rather than encode every default in attribute form,
923/// we accept each field as `Option<T>` and unwrap it via `T::default()` (or via the
924/// type's own `Default::default()` for the whole struct as the base) inside the body.
925/// The R-side wrapper generated in `generate_public_api` already supplies named
926/// arguments with `NULL` defaults, so callers see ergonomic kwargs at the R level.
927///
928/// `enum_names` is the set of type names that are enums in this API surface.  For
929/// fields whose type resolves to a Named enum, the parameter is widened to
930/// `Option<String>` (extendr has no `TryFrom<&Robj>` for binding enums) and the body
931/// deserialises the string back to the enum via `serde_json::from_str`.
932pub fn gen_extendr_kwargs_constructor(
933    typ: &TypeDef,
934    type_mapper: &dyn Fn(&TypeRef) -> String,
935    enum_names: &ahash::AHashSet<String>,
936) -> String {
937    use std::fmt::Write;
938    let mut out = String::with_capacity(512);
939
940    writeln!(out, "#[extendr]").ok();
941    writeln!(out, "pub fn new_{}(", typ.name.to_lowercase()).ok();
942
943    // Add all fields as Option<T> parameters — extendr passes NULL → None which lets
944    // each field fall through to the struct's Default value when omitted.
945    // Fields whose inner type is a binding enum use Option<String> because extendr has
946    // no TryFrom<&Robj> impl for enum types; the string is parsed back in the body.
947    // Fields that are non-opaque, non-enum named types (structs) are mapped to Robj
948    // since extendr cannot convert Option<SomeStruct> from R automatically.
949    let is_named_enum = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if enum_names.contains(n.as_str())) };
950    let is_named_struct = |ty: &TypeRef| -> bool {
951        // A bare Named type that is not an enum — treated as Robj in params since
952        // extendr only generates TryFrom<&Robj> for &Foo (reference), not for Foo (owned).
953        matches!(ty, TypeRef::Named(n) if !enum_names.contains(n.as_str()))
954    };
955    let is_optional_named_struct = |ty: &TypeRef| -> bool {
956        if let TypeRef::Optional(inner) = ty {
957            is_named_struct(inner)
958        } else {
959            false
960        }
961    };
962    // Returns true if the field type is already Optional (TypeRef::Optional or has field.optional=true
963    // with a type that the mapper wraps in Option<>). Used to prevent double-wrapping.
964    let ty_is_optional = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Optional(_)) };
965
966    // Pre-collect emittable fields (skip struct-typed fields that extendr cannot convert).
967    let emittable_fields: Vec<&FieldDef> = typ
968        .fields
969        .iter()
970        .filter(|f| !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
971        .collect();
972
973    for (i, field) in emittable_fields.iter().enumerate() {
974        let comma = if i < emittable_fields.len() - 1 { "," } else { "" };
975        if is_named_enum(&field.ty) {
976            // Enum fields: use Option<String> — parsed back via serde_json in the body.
977            writeln!(out, "    {}: Option<String>{}", field.name, comma).ok();
978        } else if ty_is_optional(&field.ty) {
979            // Already Optional type: type_mapper emits "Option<T>", so don't double-wrap.
980            // The param is the same type as the field (no extra Option wrapper needed for kwargs).
981            let param_type = type_mapper(&field.ty);
982            writeln!(out, "    {}: {}{}", field.name, param_type, comma).ok();
983        } else {
984            let param_type = type_mapper(&field.ty);
985            writeln!(out, "    {}: Option<{}>{}", field.name, param_type, comma).ok();
986        }
987    }
988
989    writeln!(out, ") -> {} {{", typ.name).ok();
990    // Use the type's Default impl as a base so unspecified fields keep their natural
991    // defaults rather than `Default::default()` for each individual field type (which
992    // would, for example, give an empty String for fields whose true default is
993    // `"utf-8"`).  Then overlay any caller-provided values.
994    writeln!(out, "    let mut __out = <{}>::default();", typ.name).ok();
995    for field in &typ.fields {
996        // Skip struct-typed fields — they were omitted from the parameter list and
997        // will keep their Default value from __out.
998        if is_named_struct(&field.ty) || is_optional_named_struct(&field.ty) {
999            continue;
1000        }
1001        if is_named_enum(&field.ty) {
1002            // Enum field via String: parse with serde_json, fall back to Default on error.
1003            if field.optional {
1004                writeln!(
1005                    out,
1006                    "    if let Some(v) = {name} {{ __out.{name} = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")).ok(); }}",
1007                    name = field.name
1008                )
1009                .ok();
1010            } else {
1011                writeln!(
1012                    out,
1013                    "    if let Some(v) = {name} {{ if let Ok(parsed) = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")) {{ __out.{name} = parsed; }} }}",
1014                    name = field.name
1015                )
1016                .ok();
1017            }
1018        } else if ty_is_optional(&field.ty) {
1019            // Already Optional: param IS Option<inner>, assign through Some on match.
1020            writeln!(
1021                out,
1022                "    if let Some(v) = {name} {{ __out.{name} = Some(v); }}",
1023                name = field.name
1024            )
1025            .ok();
1026        } else if field.optional {
1027            // Optional flag set but type is plain T: wrap with Some.
1028            writeln!(
1029                out,
1030                "    if let Some(v) = {name} {{ __out.{name} = Some(v); }}",
1031                name = field.name
1032            )
1033            .ok();
1034        } else {
1035            writeln!(
1036                out,
1037                "    if let Some(v) = {name} {{ __out.{name} = v; }}",
1038                name = field.name
1039            )
1040            .ok();
1041        }
1042    }
1043    writeln!(out, "    __out").ok();
1044    writeln!(out, "}}").ok();
1045
1046    out
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use super::*;
1052    use alef_core::ir::{CoreWrapper, FieldDef, PrimitiveType, TypeRef};
1053
1054    fn make_test_type() -> TypeDef {
1055        TypeDef {
1056            name: "Config".to_string(),
1057            rust_path: "my_crate::Config".to_string(),
1058            original_rust_path: String::new(),
1059            fields: vec![
1060                FieldDef {
1061                    name: "timeout".to_string(),
1062                    ty: TypeRef::Primitive(PrimitiveType::U64),
1063                    optional: false,
1064                    default: Some("30".to_string()),
1065                    doc: "Timeout in seconds".to_string(),
1066                    sanitized: false,
1067                    is_boxed: false,
1068                    type_rust_path: None,
1069                    cfg: None,
1070                    typed_default: Some(DefaultValue::IntLiteral(30)),
1071                    core_wrapper: CoreWrapper::None,
1072                    vec_inner_core_wrapper: CoreWrapper::None,
1073                    newtype_wrapper: None,
1074                },
1075                FieldDef {
1076                    name: "enabled".to_string(),
1077                    ty: TypeRef::Primitive(PrimitiveType::Bool),
1078                    optional: false,
1079                    default: None,
1080                    doc: "Enable feature".to_string(),
1081                    sanitized: false,
1082                    is_boxed: false,
1083                    type_rust_path: None,
1084                    cfg: None,
1085                    typed_default: Some(DefaultValue::BoolLiteral(true)),
1086                    core_wrapper: CoreWrapper::None,
1087                    vec_inner_core_wrapper: CoreWrapper::None,
1088                    newtype_wrapper: None,
1089                },
1090                FieldDef {
1091                    name: "name".to_string(),
1092                    ty: TypeRef::String,
1093                    optional: false,
1094                    default: None,
1095                    doc: "Config name".to_string(),
1096                    sanitized: false,
1097                    is_boxed: false,
1098                    type_rust_path: None,
1099                    cfg: None,
1100                    typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
1101                    core_wrapper: CoreWrapper::None,
1102                    vec_inner_core_wrapper: CoreWrapper::None,
1103                    newtype_wrapper: None,
1104                },
1105            ],
1106            methods: vec![],
1107            is_opaque: false,
1108            is_clone: true,
1109            is_copy: false,
1110            doc: "Configuration type".to_string(),
1111            cfg: None,
1112            is_trait: false,
1113            has_default: true,
1114            has_stripped_cfg_fields: false,
1115            is_return_type: false,
1116            serde_rename_all: None,
1117            has_serde: false,
1118            super_traits: vec![],
1119        }
1120    }
1121
1122    #[test]
1123    fn test_default_value_bool_true_python() {
1124        let field = FieldDef {
1125            name: "enabled".to_string(),
1126            ty: TypeRef::Primitive(PrimitiveType::Bool),
1127            optional: false,
1128            default: None,
1129            doc: String::new(),
1130            sanitized: false,
1131            is_boxed: false,
1132            type_rust_path: None,
1133            cfg: None,
1134            typed_default: Some(DefaultValue::BoolLiteral(true)),
1135            core_wrapper: CoreWrapper::None,
1136            vec_inner_core_wrapper: CoreWrapper::None,
1137            newtype_wrapper: None,
1138        };
1139        assert_eq!(default_value_for_field(&field, "python"), "True");
1140    }
1141
1142    #[test]
1143    fn test_default_value_bool_false_go() {
1144        let field = FieldDef {
1145            name: "enabled".to_string(),
1146            ty: TypeRef::Primitive(PrimitiveType::Bool),
1147            optional: false,
1148            default: None,
1149            doc: String::new(),
1150            sanitized: false,
1151            is_boxed: false,
1152            type_rust_path: None,
1153            cfg: None,
1154            typed_default: Some(DefaultValue::BoolLiteral(false)),
1155            core_wrapper: CoreWrapper::None,
1156            vec_inner_core_wrapper: CoreWrapper::None,
1157            newtype_wrapper: None,
1158        };
1159        assert_eq!(default_value_for_field(&field, "go"), "false");
1160    }
1161
1162    #[test]
1163    fn test_default_value_string_literal() {
1164        let field = FieldDef {
1165            name: "name".to_string(),
1166            ty: TypeRef::String,
1167            optional: false,
1168            default: None,
1169            doc: String::new(),
1170            sanitized: false,
1171            is_boxed: false,
1172            type_rust_path: None,
1173            cfg: None,
1174            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1175            core_wrapper: CoreWrapper::None,
1176            vec_inner_core_wrapper: CoreWrapper::None,
1177            newtype_wrapper: None,
1178        };
1179        assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
1180        assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
1181    }
1182
1183    #[test]
1184    fn test_default_value_int_literal() {
1185        let field = FieldDef {
1186            name: "timeout".to_string(),
1187            ty: TypeRef::Primitive(PrimitiveType::U64),
1188            optional: false,
1189            default: None,
1190            doc: String::new(),
1191            sanitized: false,
1192            is_boxed: false,
1193            type_rust_path: None,
1194            cfg: None,
1195            typed_default: Some(DefaultValue::IntLiteral(42)),
1196            core_wrapper: CoreWrapper::None,
1197            vec_inner_core_wrapper: CoreWrapper::None,
1198            newtype_wrapper: None,
1199        };
1200        let result = default_value_for_field(&field, "python");
1201        assert_eq!(result, "42");
1202    }
1203
1204    #[test]
1205    fn test_default_value_none() {
1206        let field = FieldDef {
1207            name: "maybe".to_string(),
1208            ty: TypeRef::Optional(Box::new(TypeRef::String)),
1209            optional: true,
1210            default: None,
1211            doc: String::new(),
1212            sanitized: false,
1213            is_boxed: false,
1214            type_rust_path: None,
1215            cfg: None,
1216            typed_default: Some(DefaultValue::None),
1217            core_wrapper: CoreWrapper::None,
1218            vec_inner_core_wrapper: CoreWrapper::None,
1219            newtype_wrapper: None,
1220        };
1221        assert_eq!(default_value_for_field(&field, "python"), "None");
1222        assert_eq!(default_value_for_field(&field, "go"), "nil");
1223        assert_eq!(default_value_for_field(&field, "java"), "null");
1224        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1225    }
1226
1227    #[test]
1228    fn test_default_value_fallback_string() {
1229        let field = FieldDef {
1230            name: "name".to_string(),
1231            ty: TypeRef::String,
1232            optional: false,
1233            default: Some("\"custom\"".to_string()),
1234            doc: String::new(),
1235            sanitized: false,
1236            is_boxed: false,
1237            type_rust_path: None,
1238            cfg: None,
1239            typed_default: None,
1240            core_wrapper: CoreWrapper::None,
1241            vec_inner_core_wrapper: CoreWrapper::None,
1242            newtype_wrapper: None,
1243        };
1244        assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1245    }
1246
1247    #[test]
1248    fn test_gen_pyo3_kwargs_constructor() {
1249        let typ = make_test_type();
1250        let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1251            TypeRef::Primitive(p) => format!("{:?}", p),
1252            TypeRef::String | TypeRef::Char => "str".to_string(),
1253            _ => "Any".to_string(),
1254        });
1255
1256        assert!(output.contains("#[new]"));
1257        assert!(output.contains("#[pyo3(signature = ("));
1258        assert!(output.contains("timeout=30"));
1259        assert!(output.contains("enabled=True"));
1260        assert!(output.contains("name=\"default\""));
1261        assert!(output.contains("fn new("));
1262    }
1263
1264    #[test]
1265    fn test_gen_napi_defaults_constructor() {
1266        let typ = make_test_type();
1267        let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1268            TypeRef::Primitive(p) => format!("{:?}", p),
1269            TypeRef::String | TypeRef::Char => "String".to_string(),
1270            _ => "Value".to_string(),
1271        });
1272
1273        assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1274        assert!(output.contains("timeout"));
1275        assert!(output.contains("enabled"));
1276        assert!(output.contains("name"));
1277    }
1278
1279    #[test]
1280    fn test_gen_go_functional_options() {
1281        let typ = make_test_type();
1282        let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1283            TypeRef::Primitive(p) => match p {
1284                PrimitiveType::U64 => "uint64".to_string(),
1285                PrimitiveType::Bool => "bool".to_string(),
1286                _ => "interface{}".to_string(),
1287            },
1288            TypeRef::String | TypeRef::Char => "string".to_string(),
1289            _ => "interface{}".to_string(),
1290        });
1291
1292        assert!(output.contains("type Config struct {"));
1293        assert!(output.contains("type ConfigOption func(*Config)"));
1294        assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1295        assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1296        assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1297        assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1298    }
1299
1300    #[test]
1301    fn test_gen_java_builder() {
1302        let typ = make_test_type();
1303        let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1304            TypeRef::Primitive(p) => match p {
1305                PrimitiveType::U64 => "long".to_string(),
1306                PrimitiveType::Bool => "boolean".to_string(),
1307                _ => "int".to_string(),
1308            },
1309            TypeRef::String | TypeRef::Char => "String".to_string(),
1310            _ => "Object".to_string(),
1311        });
1312
1313        assert!(output.contains("package dev.test;"));
1314        assert!(output.contains("public class ConfigBuilder"));
1315        assert!(output.contains("withTimeout"));
1316        assert!(output.contains("withEnabled"));
1317        assert!(output.contains("withName"));
1318        assert!(output.contains("public Config build()"));
1319    }
1320
1321    #[test]
1322    fn test_gen_csharp_record() {
1323        let typ = make_test_type();
1324        let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1325            TypeRef::Primitive(p) => match p {
1326                PrimitiveType::U64 => "ulong".to_string(),
1327                PrimitiveType::Bool => "bool".to_string(),
1328                _ => "int".to_string(),
1329            },
1330            TypeRef::String | TypeRef::Char => "string".to_string(),
1331            _ => "object".to_string(),
1332        });
1333
1334        assert!(output.contains("namespace MyNamespace;"));
1335        assert!(output.contains("public record Config"));
1336        assert!(output.contains("public ulong Timeout"));
1337        assert!(output.contains("public bool Enabled"));
1338        assert!(output.contains("public string Name"));
1339        assert!(output.contains("init;"));
1340    }
1341
1342    #[test]
1343    fn test_default_value_float_literal() {
1344        let field = FieldDef {
1345            name: "ratio".to_string(),
1346            ty: TypeRef::Primitive(PrimitiveType::F64),
1347            optional: false,
1348            default: None,
1349            doc: String::new(),
1350            sanitized: false,
1351            is_boxed: false,
1352            type_rust_path: None,
1353            cfg: None,
1354            typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1355            core_wrapper: CoreWrapper::None,
1356            vec_inner_core_wrapper: CoreWrapper::None,
1357            newtype_wrapper: None,
1358        };
1359        let result = default_value_for_field(&field, "python");
1360        assert!(result.contains("1.5"));
1361    }
1362
1363    #[test]
1364    fn test_default_value_no_typed_no_default() {
1365        let field = FieldDef {
1366            name: "count".to_string(),
1367            ty: TypeRef::Primitive(PrimitiveType::U32),
1368            optional: false,
1369            default: None,
1370            doc: String::new(),
1371            sanitized: false,
1372            is_boxed: false,
1373            type_rust_path: None,
1374            cfg: None,
1375            typed_default: None,
1376            core_wrapper: CoreWrapper::None,
1377            vec_inner_core_wrapper: CoreWrapper::None,
1378            newtype_wrapper: None,
1379        };
1380        // Should fall back to type-based zero value
1381        assert_eq!(default_value_for_field(&field, "python"), "0");
1382        assert_eq!(default_value_for_field(&field, "go"), "0");
1383    }
1384
1385    fn make_field(name: &str, ty: TypeRef) -> FieldDef {
1386        FieldDef {
1387            name: name.to_string(),
1388            ty,
1389            optional: false,
1390            default: None,
1391            doc: String::new(),
1392            sanitized: false,
1393            is_boxed: false,
1394            type_rust_path: None,
1395            cfg: None,
1396            typed_default: None,
1397            core_wrapper: CoreWrapper::None,
1398            vec_inner_core_wrapper: CoreWrapper::None,
1399            newtype_wrapper: None,
1400        }
1401    }
1402
1403    fn simple_type_mapper(tr: &TypeRef) -> String {
1404        match tr {
1405            TypeRef::Primitive(p) => match p {
1406                PrimitiveType::U64 => "u64".to_string(),
1407                PrimitiveType::Bool => "bool".to_string(),
1408                PrimitiveType::U32 => "u32".to_string(),
1409                _ => "i64".to_string(),
1410            },
1411            TypeRef::String | TypeRef::Char => "String".to_string(),
1412            TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
1413            TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
1414            TypeRef::Named(n) => n.clone(),
1415            _ => "Value".to_string(),
1416        }
1417    }
1418
1419    // -------------------------------------------------------------------------
1420    // default_value_for_field — untested branches
1421    // -------------------------------------------------------------------------
1422
1423    #[test]
1424    fn test_default_value_bool_literal_ruby() {
1425        let field = FieldDef {
1426            name: "flag".to_string(),
1427            ty: TypeRef::Primitive(PrimitiveType::Bool),
1428            optional: false,
1429            default: None,
1430            doc: String::new(),
1431            sanitized: false,
1432            is_boxed: false,
1433            type_rust_path: None,
1434            cfg: None,
1435            typed_default: Some(DefaultValue::BoolLiteral(true)),
1436            core_wrapper: CoreWrapper::None,
1437            vec_inner_core_wrapper: CoreWrapper::None,
1438            newtype_wrapper: None,
1439        };
1440        assert_eq!(default_value_for_field(&field, "ruby"), "true");
1441        assert_eq!(default_value_for_field(&field, "php"), "true");
1442        assert_eq!(default_value_for_field(&field, "csharp"), "true");
1443        assert_eq!(default_value_for_field(&field, "java"), "true");
1444        assert_eq!(default_value_for_field(&field, "rust"), "true");
1445    }
1446
1447    #[test]
1448    fn test_default_value_bool_literal_r() {
1449        let field = FieldDef {
1450            name: "flag".to_string(),
1451            ty: TypeRef::Primitive(PrimitiveType::Bool),
1452            optional: false,
1453            default: None,
1454            doc: String::new(),
1455            sanitized: false,
1456            is_boxed: false,
1457            type_rust_path: None,
1458            cfg: None,
1459            typed_default: Some(DefaultValue::BoolLiteral(false)),
1460            core_wrapper: CoreWrapper::None,
1461            vec_inner_core_wrapper: CoreWrapper::None,
1462            newtype_wrapper: None,
1463        };
1464        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1465    }
1466
1467    #[test]
1468    fn test_default_value_string_literal_rust() {
1469        let field = FieldDef {
1470            name: "label".to_string(),
1471            ty: TypeRef::String,
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::StringLiteral("hello".to_string())),
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, "rust"), "\"hello\".to_string()");
1485    }
1486
1487    #[test]
1488    fn test_default_value_string_literal_escapes_quotes() {
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::StringLiteral("say \"hi\"".to_string())),
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, "python"), "\"say \\\"hi\\\"\"");
1505    }
1506
1507    #[test]
1508    fn test_default_value_float_literal_whole_number() {
1509        // A whole-number float should be rendered with ".0" suffix.
1510        let field = FieldDef {
1511            name: "scale".to_string(),
1512            ty: TypeRef::Primitive(PrimitiveType::F32),
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::FloatLiteral(2.0)),
1521            core_wrapper: CoreWrapper::None,
1522            vec_inner_core_wrapper: CoreWrapper::None,
1523            newtype_wrapper: None,
1524        };
1525        let result = default_value_for_field(&field, "python");
1526        assert!(result.contains('.'), "whole-number float should contain '.': {result}");
1527    }
1528
1529    #[test]
1530    fn test_default_value_enum_variant_per_language() {
1531        let field = FieldDef {
1532            name: "format".to_string(),
1533            ty: TypeRef::Named("OutputFormat".to_string()),
1534            optional: false,
1535            default: None,
1536            doc: String::new(),
1537            sanitized: false,
1538            is_boxed: false,
1539            type_rust_path: None,
1540            cfg: None,
1541            typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
1542            core_wrapper: CoreWrapper::None,
1543            vec_inner_core_wrapper: CoreWrapper::None,
1544            newtype_wrapper: None,
1545        };
1546        assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
1547        assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
1548        assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
1549        assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
1550        assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
1551        assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
1552        assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
1553        assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
1554    }
1555
1556    #[test]
1557    fn test_default_value_empty_vec_per_language() {
1558        let field = FieldDef {
1559            name: "items".to_string(),
1560            ty: TypeRef::Vec(Box::new(TypeRef::String)),
1561            optional: false,
1562            default: None,
1563            doc: String::new(),
1564            sanitized: false,
1565            is_boxed: false,
1566            type_rust_path: None,
1567            cfg: None,
1568            typed_default: Some(DefaultValue::Empty),
1569            core_wrapper: CoreWrapper::None,
1570            vec_inner_core_wrapper: CoreWrapper::None,
1571            newtype_wrapper: None,
1572        };
1573        assert_eq!(default_value_for_field(&field, "python"), "[]");
1574        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1575        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1576        assert_eq!(default_value_for_field(&field, "go"), "nil");
1577        assert_eq!(default_value_for_field(&field, "java"), "List.of()");
1578        assert_eq!(default_value_for_field(&field, "php"), "[]");
1579        assert_eq!(default_value_for_field(&field, "r"), "c()");
1580        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1581    }
1582
1583    #[test]
1584    fn test_default_value_empty_map_per_language() {
1585        let field = FieldDef {
1586            name: "meta".to_string(),
1587            ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1588            optional: false,
1589            default: None,
1590            doc: String::new(),
1591            sanitized: false,
1592            is_boxed: false,
1593            type_rust_path: None,
1594            cfg: None,
1595            typed_default: Some(DefaultValue::Empty),
1596            core_wrapper: CoreWrapper::None,
1597            vec_inner_core_wrapper: CoreWrapper::None,
1598            newtype_wrapper: None,
1599        };
1600        assert_eq!(default_value_for_field(&field, "python"), "{}");
1601        assert_eq!(default_value_for_field(&field, "go"), "nil");
1602        assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
1603        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1604    }
1605
1606    #[test]
1607    fn test_default_value_empty_bool_primitive() {
1608        let field = FieldDef {
1609            name: "flag".to_string(),
1610            ty: TypeRef::Primitive(PrimitiveType::Bool),
1611            optional: false,
1612            default: None,
1613            doc: String::new(),
1614            sanitized: false,
1615            is_boxed: false,
1616            type_rust_path: None,
1617            cfg: None,
1618            typed_default: Some(DefaultValue::Empty),
1619            core_wrapper: CoreWrapper::None,
1620            vec_inner_core_wrapper: CoreWrapper::None,
1621            newtype_wrapper: None,
1622        };
1623        assert_eq!(default_value_for_field(&field, "python"), "False");
1624        assert_eq!(default_value_for_field(&field, "ruby"), "false");
1625        assert_eq!(default_value_for_field(&field, "go"), "false");
1626    }
1627
1628    #[test]
1629    fn test_default_value_empty_float_primitive() {
1630        let field = FieldDef {
1631            name: "ratio".to_string(),
1632            ty: TypeRef::Primitive(PrimitiveType::F64),
1633            optional: false,
1634            default: None,
1635            doc: String::new(),
1636            sanitized: false,
1637            is_boxed: false,
1638            type_rust_path: None,
1639            cfg: None,
1640            typed_default: Some(DefaultValue::Empty),
1641            core_wrapper: CoreWrapper::None,
1642            vec_inner_core_wrapper: CoreWrapper::None,
1643            newtype_wrapper: None,
1644        };
1645        assert_eq!(default_value_for_field(&field, "python"), "0.0");
1646    }
1647
1648    #[test]
1649    fn test_default_value_empty_string_type() {
1650        let field = FieldDef {
1651            name: "label".to_string(),
1652            ty: TypeRef::String,
1653            optional: false,
1654            default: None,
1655            doc: String::new(),
1656            sanitized: false,
1657            is_boxed: false,
1658            type_rust_path: None,
1659            cfg: None,
1660            typed_default: Some(DefaultValue::Empty),
1661            core_wrapper: CoreWrapper::None,
1662            vec_inner_core_wrapper: CoreWrapper::None,
1663            newtype_wrapper: None,
1664        };
1665        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1666        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1667    }
1668
1669    #[test]
1670    fn test_default_value_empty_bytes_type() {
1671        let field = FieldDef {
1672            name: "data".to_string(),
1673            ty: TypeRef::Bytes,
1674            optional: false,
1675            default: None,
1676            doc: String::new(),
1677            sanitized: false,
1678            is_boxed: false,
1679            type_rust_path: None,
1680            cfg: None,
1681            typed_default: Some(DefaultValue::Empty),
1682            core_wrapper: CoreWrapper::None,
1683            vec_inner_core_wrapper: CoreWrapper::None,
1684            newtype_wrapper: None,
1685        };
1686        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1687        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1688        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1689    }
1690
1691    #[test]
1692    fn test_default_value_empty_json_type() {
1693        let field = FieldDef {
1694            name: "payload".to_string(),
1695            ty: TypeRef::Json,
1696            optional: false,
1697            default: None,
1698            doc: String::new(),
1699            sanitized: false,
1700            is_boxed: false,
1701            type_rust_path: None,
1702            cfg: None,
1703            typed_default: Some(DefaultValue::Empty),
1704            core_wrapper: CoreWrapper::None,
1705            vec_inner_core_wrapper: CoreWrapper::None,
1706            newtype_wrapper: None,
1707        };
1708        assert_eq!(default_value_for_field(&field, "python"), "{}");
1709        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1710        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1711        assert_eq!(default_value_for_field(&field, "r"), "list()");
1712        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1713    }
1714
1715    #[test]
1716    fn test_default_value_none_ruby_php_r() {
1717        let field = FieldDef {
1718            name: "maybe".to_string(),
1719            ty: TypeRef::Optional(Box::new(TypeRef::String)),
1720            optional: true,
1721            default: None,
1722            doc: String::new(),
1723            sanitized: false,
1724            is_boxed: false,
1725            type_rust_path: None,
1726            cfg: None,
1727            typed_default: Some(DefaultValue::None),
1728            core_wrapper: CoreWrapper::None,
1729            vec_inner_core_wrapper: CoreWrapper::None,
1730            newtype_wrapper: None,
1731        };
1732        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1733        assert_eq!(default_value_for_field(&field, "php"), "null");
1734        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1735        assert_eq!(default_value_for_field(&field, "rust"), "None");
1736    }
1737
1738    // -------------------------------------------------------------------------
1739    // Fallback (no typed_default, no default) — type-based zero values
1740    // -------------------------------------------------------------------------
1741
1742    #[test]
1743    fn test_default_value_fallback_bool_all_languages() {
1744        let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
1745        assert_eq!(default_value_for_field(&field, "python"), "False");
1746        assert_eq!(default_value_for_field(&field, "ruby"), "false");
1747        assert_eq!(default_value_for_field(&field, "csharp"), "false");
1748        assert_eq!(default_value_for_field(&field, "java"), "false");
1749        assert_eq!(default_value_for_field(&field, "php"), "false");
1750        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1751        assert_eq!(default_value_for_field(&field, "rust"), "false");
1752    }
1753
1754    #[test]
1755    fn test_default_value_fallback_float() {
1756        let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
1757        assert_eq!(default_value_for_field(&field, "python"), "0.0");
1758        assert_eq!(default_value_for_field(&field, "rust"), "0.0");
1759    }
1760
1761    #[test]
1762    fn test_default_value_fallback_string_all_languages() {
1763        let field = make_field("name", TypeRef::String);
1764        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1765        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1766        assert_eq!(default_value_for_field(&field, "go"), "\"\"");
1767        assert_eq!(default_value_for_field(&field, "java"), "\"\"");
1768        assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
1769        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1770        assert_eq!(default_value_for_field(&field, "r"), "\"\"");
1771        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1772    }
1773
1774    #[test]
1775    fn test_default_value_fallback_bytes_all_languages() {
1776        let field = make_field("data", TypeRef::Bytes);
1777        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1778        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1779        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1780        assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
1781        assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
1782        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1783        assert_eq!(default_value_for_field(&field, "r"), "raw()");
1784        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1785    }
1786
1787    #[test]
1788    fn test_default_value_fallback_optional() {
1789        let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
1790        assert_eq!(default_value_for_field(&field, "python"), "None");
1791        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1792        assert_eq!(default_value_for_field(&field, "go"), "nil");
1793        assert_eq!(default_value_for_field(&field, "java"), "null");
1794        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1795        assert_eq!(default_value_for_field(&field, "php"), "null");
1796        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1797        assert_eq!(default_value_for_field(&field, "rust"), "None");
1798    }
1799
1800    #[test]
1801    fn test_default_value_fallback_vec_all_languages() {
1802        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
1803        assert_eq!(default_value_for_field(&field, "python"), "[]");
1804        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1805        assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
1806        assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
1807        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1808        assert_eq!(default_value_for_field(&field, "php"), "[]");
1809        assert_eq!(default_value_for_field(&field, "r"), "c()");
1810        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1811    }
1812
1813    #[test]
1814    fn test_default_value_fallback_map_all_languages() {
1815        let field = make_field(
1816            "meta",
1817            TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1818        );
1819        assert_eq!(default_value_for_field(&field, "python"), "{}");
1820        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1821        assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
1822        assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
1823        assert_eq!(
1824            default_value_for_field(&field, "csharp"),
1825            "new Dictionary<string, object>()"
1826        );
1827        assert_eq!(default_value_for_field(&field, "php"), "[]");
1828        assert_eq!(default_value_for_field(&field, "r"), "list()");
1829        assert_eq!(
1830            default_value_for_field(&field, "rust"),
1831            "std::collections::HashMap::new()"
1832        );
1833    }
1834
1835    #[test]
1836    fn test_default_value_fallback_json_all_languages() {
1837        let field = make_field("payload", TypeRef::Json);
1838        assert_eq!(default_value_for_field(&field, "python"), "{}");
1839        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1840        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1841        assert_eq!(default_value_for_field(&field, "r"), "list()");
1842        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1843    }
1844
1845    #[test]
1846    fn test_default_value_fallback_named_type() {
1847        let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
1848        assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
1849        assert_eq!(default_value_for_field(&field, "python"), "None");
1850        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1851        assert_eq!(default_value_for_field(&field, "go"), "nil");
1852        assert_eq!(default_value_for_field(&field, "java"), "null");
1853        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1854        assert_eq!(default_value_for_field(&field, "php"), "null");
1855        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1856    }
1857
1858    #[test]
1859    fn test_default_value_fallback_duration() {
1860        // Duration falls through to the wildcard arm
1861        let field = make_field("timeout", TypeRef::Duration);
1862        assert_eq!(default_value_for_field(&field, "python"), "None");
1863        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1864    }
1865
1866    // -------------------------------------------------------------------------
1867    // gen_magnus_kwargs_constructor — positional (≤15 fields)
1868    // -------------------------------------------------------------------------
1869
1870    #[test]
1871    fn test_gen_magnus_kwargs_constructor_positional_basic() {
1872        let typ = make_test_type();
1873        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1874
1875        assert!(output.contains("fn new("), "should have fn new");
1876        // All params are Option<T>
1877        assert!(output.contains("Option<u64>"), "timeout should be Option<u64>");
1878        assert!(output.contains("Option<bool>"), "enabled should be Option<bool>");
1879        assert!(output.contains("Option<String>"), "name should be Option<String>");
1880        assert!(output.contains("-> Self {"), "should return Self");
1881        // timeout has IntLiteral(30), use_unwrap_or_default is false for Named → uses unwrap_or
1882        assert!(
1883            output.contains("timeout: timeout.unwrap_or(30),"),
1884            "should apply int default"
1885        );
1886        // enabled has BoolLiteral(true), not unwrap_or_default
1887        assert!(
1888            output.contains("enabled: enabled.unwrap_or(true),"),
1889            "should apply bool default"
1890        );
1891        // name has StringLiteral, not unwrap_or_default
1892        assert!(
1893            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1894            "should apply string default"
1895        );
1896    }
1897
1898    #[test]
1899    fn test_gen_magnus_kwargs_constructor_positional_optional_field() {
1900        // A field with optional=true should be assigned directly (no unwrap)
1901        let mut typ = make_test_type();
1902        typ.fields.push(FieldDef {
1903            name: "extra".to_string(),
1904            ty: TypeRef::String,
1905            optional: true,
1906            default: None,
1907            doc: String::new(),
1908            sanitized: false,
1909            is_boxed: false,
1910            type_rust_path: None,
1911            cfg: None,
1912            typed_default: None,
1913            core_wrapper: CoreWrapper::None,
1914            vec_inner_core_wrapper: CoreWrapper::None,
1915            newtype_wrapper: None,
1916        });
1917        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1918        // Optional field param is Option<String> and assigned directly
1919        assert!(output.contains("extra,"), "optional field should be assigned directly");
1920        assert!(!output.contains("extra.unwrap"), "optional field should not use unwrap");
1921    }
1922
1923    #[test]
1924    fn test_gen_magnus_kwargs_constructor_unwrap_or_default() {
1925        // A primitive field with no typed_default and no default should use unwrap_or_default()
1926        let mut typ = make_test_type();
1927        typ.fields.push(FieldDef {
1928            name: "count".to_string(),
1929            ty: TypeRef::Primitive(PrimitiveType::U32),
1930            optional: false,
1931            default: None,
1932            doc: String::new(),
1933            sanitized: false,
1934            is_boxed: false,
1935            type_rust_path: None,
1936            cfg: None,
1937            typed_default: None,
1938            core_wrapper: CoreWrapper::None,
1939            vec_inner_core_wrapper: CoreWrapper::None,
1940            newtype_wrapper: None,
1941        });
1942        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1943        assert!(
1944            output.contains("count: count.unwrap_or_default(),"),
1945            "plain primitive with no default should use unwrap_or_default"
1946        );
1947    }
1948
1949    #[test]
1950    fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
1951        // Build a type with 16 fields (> MAGNUS_MAX_ARITY = 15) to force hash path
1952        let mut fields: Vec<FieldDef> = (0..16)
1953            .map(|i| FieldDef {
1954                name: format!("field_{i}"),
1955                ty: TypeRef::Primitive(PrimitiveType::U32),
1956                optional: false,
1957                default: None,
1958                doc: String::new(),
1959                sanitized: false,
1960                is_boxed: false,
1961                type_rust_path: None,
1962                cfg: None,
1963                typed_default: None,
1964                core_wrapper: CoreWrapper::None,
1965                vec_inner_core_wrapper: CoreWrapper::None,
1966                newtype_wrapper: None,
1967            })
1968            .collect();
1969        // Make one field optional to exercise that branch in the hash constructor
1970        fields[0].optional = true;
1971
1972        let typ = TypeDef {
1973            name: "BigConfig".to_string(),
1974            rust_path: "crate::BigConfig".to_string(),
1975            original_rust_path: String::new(),
1976            fields,
1977            methods: vec![],
1978            is_opaque: false,
1979            is_clone: true,
1980            is_copy: false,
1981            doc: String::new(),
1982            cfg: None,
1983            is_trait: false,
1984            has_default: true,
1985            has_stripped_cfg_fields: false,
1986            is_return_type: false,
1987            serde_rename_all: None,
1988            has_serde: false,
1989            super_traits: vec![],
1990        };
1991        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1992
1993        assert!(
1994            output.contains("Option<magnus::RHash>"),
1995            "should accept RHash via scan_args"
1996        );
1997        assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
1998        // Optional field uses and_then without unwrap_or
1999        assert!(
2000            output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
2001            "optional field should use and_then"
2002        );
2003        assert!(
2004            output.contains("field_0:").then_some(()).is_some(),
2005            "field_0 should appear in output"
2006        );
2007    }
2008
2009    // -------------------------------------------------------------------------
2010    // gen_php_kwargs_constructor
2011    // -------------------------------------------------------------------------
2012
2013    #[test]
2014    fn test_gen_php_kwargs_constructor_basic() {
2015        let typ = make_test_type();
2016        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2017
2018        assert!(
2019            output.contains("pub fn __construct("),
2020            "should use PHP constructor name"
2021        );
2022        // All params are Option<T>
2023        assert!(
2024            output.contains("timeout: Option<u64>"),
2025            "timeout param should be Option<u64>"
2026        );
2027        assert!(
2028            output.contains("enabled: Option<bool>"),
2029            "enabled param should be Option<bool>"
2030        );
2031        assert!(
2032            output.contains("name: Option<String>"),
2033            "name param should be Option<String>"
2034        );
2035        assert!(output.contains("-> Self {"), "should return Self");
2036        assert!(
2037            output.contains("timeout: timeout.unwrap_or(30),"),
2038            "should apply int default for timeout"
2039        );
2040        assert!(
2041            output.contains("enabled: enabled.unwrap_or(true),"),
2042            "should apply bool default for enabled"
2043        );
2044        assert!(
2045            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
2046            "should apply string default for name"
2047        );
2048    }
2049
2050    #[test]
2051    fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
2052        let mut typ = make_test_type();
2053        typ.fields.push(FieldDef {
2054            name: "tag".to_string(),
2055            ty: TypeRef::String,
2056            optional: true,
2057            default: None,
2058            doc: String::new(),
2059            sanitized: false,
2060            is_boxed: false,
2061            type_rust_path: None,
2062            cfg: None,
2063            typed_default: None,
2064            core_wrapper: CoreWrapper::None,
2065            vec_inner_core_wrapper: CoreWrapper::None,
2066            newtype_wrapper: None,
2067        });
2068        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2069        assert!(
2070            output.contains("tag,"),
2071            "optional field should be passed through directly"
2072        );
2073        assert!(!output.contains("tag.unwrap"), "optional field should not call unwrap");
2074    }
2075
2076    #[test]
2077    fn test_gen_php_kwargs_constructor_unwrap_or_default_for_primitive() {
2078        let mut typ = make_test_type();
2079        typ.fields.push(FieldDef {
2080            name: "retries".to_string(),
2081            ty: TypeRef::Primitive(PrimitiveType::U32),
2082            optional: false,
2083            default: None,
2084            doc: String::new(),
2085            sanitized: false,
2086            is_boxed: false,
2087            type_rust_path: None,
2088            cfg: None,
2089            typed_default: None,
2090            core_wrapper: CoreWrapper::None,
2091            vec_inner_core_wrapper: CoreWrapper::None,
2092            newtype_wrapper: None,
2093        });
2094        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2095        assert!(
2096            output.contains("retries: retries.unwrap_or_default(),"),
2097            "primitive with no default should use unwrap_or_default"
2098        );
2099    }
2100
2101    // -------------------------------------------------------------------------
2102    // gen_rustler_kwargs_constructor
2103    // -------------------------------------------------------------------------
2104
2105    #[test]
2106    fn test_gen_rustler_kwargs_constructor_basic() {
2107        let typ = make_test_type();
2108        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2109
2110        assert!(
2111            output.contains("pub fn new(opts: std::collections::HashMap<String, rustler::Term>)"),
2112            "should accept HashMap of Terms"
2113        );
2114        assert!(output.contains("Self {"), "should construct Self");
2115        // timeout has IntLiteral(30) — explicit unwrap_or
2116        assert!(
2117            output.contains("timeout: opts.get(\"timeout\").and_then(|t| t.decode().ok()).unwrap_or(30),"),
2118            "should apply int default for timeout"
2119        );
2120        // enabled has BoolLiteral(true) — explicit unwrap_or
2121        assert!(
2122            output.contains("enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),"),
2123            "should apply bool default for enabled"
2124        );
2125    }
2126
2127    #[test]
2128    fn test_gen_rustler_kwargs_constructor_optional_field() {
2129        let mut typ = make_test_type();
2130        typ.fields.push(FieldDef {
2131            name: "extra".to_string(),
2132            ty: TypeRef::String,
2133            optional: true,
2134            default: None,
2135            doc: String::new(),
2136            sanitized: false,
2137            is_boxed: false,
2138            type_rust_path: None,
2139            cfg: None,
2140            typed_default: None,
2141            core_wrapper: CoreWrapper::None,
2142            vec_inner_core_wrapper: CoreWrapper::None,
2143            newtype_wrapper: None,
2144        });
2145        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2146        assert!(
2147            output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
2148            "optional field should decode without unwrap"
2149        );
2150    }
2151
2152    #[test]
2153    fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
2154        let mut typ = make_test_type();
2155        typ.fields.push(FieldDef {
2156            name: "inner".to_string(),
2157            ty: TypeRef::Named("InnerConfig".to_string()),
2158            optional: false,
2159            default: None,
2160            doc: String::new(),
2161            sanitized: false,
2162            is_boxed: false,
2163            type_rust_path: None,
2164            cfg: None,
2165            typed_default: None,
2166            core_wrapper: CoreWrapper::None,
2167            vec_inner_core_wrapper: CoreWrapper::None,
2168            newtype_wrapper: None,
2169        });
2170        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2171        assert!(
2172            output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2173            "Named type with no default should use unwrap_or_default"
2174        );
2175    }
2176
2177    #[test]
2178    fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
2179        // A String field with a StringLiteral default contains "::", triggering the
2180        // is_enum_variant_default check — should fall back to unwrap_or_default().
2181        let mut typ = make_test_type();
2182        // 'name' field in make_test_type() has StringLiteral("default") — verify it
2183        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2184        assert!(
2185            output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2186            "String field with quoted default should use unwrap_or_default"
2187        );
2188        // Also verify a plain string field (no default) also falls through to unwrap_or_default
2189        typ.fields.push(FieldDef {
2190            name: "label".to_string(),
2191            ty: TypeRef::String,
2192            optional: false,
2193            default: None,
2194            doc: String::new(),
2195            sanitized: false,
2196            is_boxed: false,
2197            type_rust_path: None,
2198            cfg: None,
2199            typed_default: None,
2200            core_wrapper: CoreWrapper::None,
2201            vec_inner_core_wrapper: CoreWrapper::None,
2202            newtype_wrapper: None,
2203        });
2204        let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2205        assert!(
2206            output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2207            "String field with no default should use unwrap_or_default"
2208        );
2209    }
2210
2211    // -------------------------------------------------------------------------
2212    // gen_extendr_kwargs_constructor
2213    // -------------------------------------------------------------------------
2214
2215    #[test]
2216    fn test_gen_extendr_kwargs_constructor_basic() {
2217        let typ = make_test_type();
2218        let empty_enums = ahash::AHashSet::new();
2219        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2220
2221        assert!(output.contains("#[extendr]"), "should have extendr attribute");
2222        assert!(
2223            output.contains("pub fn new_config("),
2224            "function name should be lowercase type name"
2225        );
2226        // Fields appear as Option<T> parameters — Rust does not support param defaults.
2227        assert!(
2228            output.contains("timeout: Option<u64>"),
2229            "should accept timeout as Option<u64>: {output}"
2230        );
2231        assert!(
2232            output.contains("enabled: Option<bool>"),
2233            "should accept enabled as Option<bool>: {output}"
2234        );
2235        assert!(
2236            output.contains("name: Option<String>"),
2237            "should accept name as Option<String>: {output}"
2238        );
2239        assert!(output.contains("-> Config {"), "should return Config");
2240        assert!(
2241            output.contains("let mut __out = <Config>::default();"),
2242            "should base on Default impl: {output}"
2243        );
2244        assert!(
2245            output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
2246            "should overlay caller-provided timeout"
2247        );
2248        assert!(
2249            output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
2250            "should overlay caller-provided enabled"
2251        );
2252        assert!(
2253            output.contains("if let Some(v) = name { __out.name = v; }"),
2254            "should overlay caller-provided name"
2255        );
2256    }
2257
2258    #[test]
2259    fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
2260        // Rust function-parameter defaults (`x: T = expr`) are a syntax error and
2261        // extendr 0.9 only supports defaults via the `#[extendr(default = "...")]`
2262        // attribute.  Verify that no field is emitted with a Rust-syntax default.
2263        let typ = make_test_type();
2264        let empty_enums = ahash::AHashSet::new();
2265        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2266        assert!(
2267            !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
2268            "constructor must not use Rust-syntax param defaults: {output}"
2269        );
2270    }
2271
2272    // -------------------------------------------------------------------------
2273    // gen_go_functional_options — tuple-field filtering
2274    // -------------------------------------------------------------------------
2275
2276    #[test]
2277    fn test_gen_go_functional_options_skips_tuple_fields() {
2278        let mut typ = make_test_type();
2279        typ.fields.push(FieldDef {
2280            name: "_0".to_string(),
2281            ty: TypeRef::Primitive(PrimitiveType::U32),
2282            optional: false,
2283            default: None,
2284            doc: String::new(),
2285            sanitized: false,
2286            is_boxed: false,
2287            type_rust_path: None,
2288            cfg: None,
2289            typed_default: None,
2290            core_wrapper: CoreWrapper::None,
2291            vec_inner_core_wrapper: CoreWrapper::None,
2292            newtype_wrapper: None,
2293        });
2294        let output = gen_go_functional_options(&typ, &simple_type_mapper);
2295        assert!(
2296            !output.contains("_0"),
2297            "tuple field _0 should be filtered out from Go output"
2298        );
2299    }
2300
2301    // -------------------------------------------------------------------------
2302    // as_type_path_prefix — tested indirectly through hash constructor
2303    // -------------------------------------------------------------------------
2304
2305    #[test]
2306    fn test_gen_magnus_hash_constructor_generic_type_prefix() {
2307        // A field with a Vec type should use <Vec<...>>::try_convert UFCS form
2308        let fields: Vec<FieldDef> = (0..16)
2309            .map(|i| FieldDef {
2310                name: format!("field_{i}"),
2311                ty: if i == 0 {
2312                    TypeRef::Vec(Box::new(TypeRef::String))
2313                } else {
2314                    TypeRef::Primitive(PrimitiveType::U32)
2315                },
2316                optional: false,
2317                default: None,
2318                doc: String::new(),
2319                sanitized: false,
2320                is_boxed: false,
2321                type_rust_path: None,
2322                cfg: None,
2323                typed_default: None,
2324                core_wrapper: CoreWrapper::None,
2325                vec_inner_core_wrapper: CoreWrapper::None,
2326                newtype_wrapper: None,
2327            })
2328            .collect();
2329        let typ = TypeDef {
2330            name: "WideConfig".to_string(),
2331            rust_path: "crate::WideConfig".to_string(),
2332            original_rust_path: String::new(),
2333            fields,
2334            methods: vec![],
2335            is_opaque: false,
2336            is_clone: true,
2337            is_copy: false,
2338            doc: String::new(),
2339            cfg: None,
2340            is_trait: false,
2341            has_default: true,
2342            has_stripped_cfg_fields: false,
2343            is_return_type: false,
2344            serde_rename_all: None,
2345            has_serde: false,
2346            super_traits: vec![],
2347        };
2348        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2349        // Vec<String> is a generic type; must use <Vec<String>>::try_convert
2350        assert!(
2351            output.contains("<Vec<String>>::try_convert"),
2352            "generic types should use UFCS angle-bracket prefix: {output}"
2353        );
2354    }
2355
2356    // -------------------------------------------------------------------------
2357    // Bug B regression: Option<Option<T>> must not appear when field.optional==true
2358    // and field.ty==Optional(T). This happens for "Update" structs where the core
2359    // field is Option<Option<T>> — the binding flattens to Option<T>.
2360    // -------------------------------------------------------------------------
2361
2362    #[test]
2363    fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
2364        // field with optional=true AND ty=Optional(Usize) — represents a core Option<Option<usize>>
2365        // that should flatten to Option<usize> in the binding constructor.
2366        // simple_type_mapper maps Usize → "i64" (catch-all primitive arm).
2367        let field = FieldDef {
2368            name: "max_depth".to_string(),
2369            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2370            optional: true,
2371            default: None,
2372            doc: String::new(),
2373            sanitized: false,
2374            is_boxed: false,
2375            type_rust_path: None,
2376            cfg: None,
2377            typed_default: None,
2378            core_wrapper: CoreWrapper::None,
2379            vec_inner_core_wrapper: CoreWrapper::None,
2380            newtype_wrapper: None,
2381        };
2382        // Build a large type (>15 fields) so the hash constructor is used
2383        let mut fields: Vec<FieldDef> = (0..15)
2384            .map(|i| FieldDef {
2385                name: format!("field_{i}"),
2386                ty: TypeRef::Primitive(PrimitiveType::U32),
2387                optional: false,
2388                default: None,
2389                doc: String::new(),
2390                sanitized: false,
2391                is_boxed: false,
2392                type_rust_path: None,
2393                cfg: None,
2394                typed_default: None,
2395                core_wrapper: CoreWrapper::None,
2396                vec_inner_core_wrapper: CoreWrapper::None,
2397                newtype_wrapper: None,
2398            })
2399            .collect();
2400        fields.push(field);
2401        let typ = TypeDef {
2402            name: "UpdateConfig".to_string(),
2403            rust_path: "crate::UpdateConfig".to_string(),
2404            original_rust_path: String::new(),
2405            fields,
2406            methods: vec![],
2407            is_opaque: false,
2408            is_clone: true,
2409            is_copy: false,
2410            doc: String::new(),
2411            cfg: None,
2412            is_trait: false,
2413            has_default: true,
2414            has_stripped_cfg_fields: false,
2415            is_return_type: false,
2416            serde_rename_all: None,
2417            has_serde: false,
2418            super_traits: vec![],
2419        };
2420        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2421        // The try_convert call must be for the inner type (i64, as mapped by simple_type_mapper),
2422        // not Option<i64> (which would yield Option<Option<i64>>).
2423        assert!(
2424            !output.contains("Option<Option<"),
2425            "hash constructor must not emit double Option: {output}"
2426        );
2427        assert!(
2428            output.contains("i64::try_convert"),
2429            "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
2430        );
2431    }
2432
2433    #[test]
2434    fn test_magnus_positional_constructor_no_double_option_when_ty_is_optional() {
2435        // field with optional=true AND ty=Optional(Usize) — small type uses positional constructor
2436        // simple_type_mapper maps Usize → "i64"
2437        let field = FieldDef {
2438            name: "max_depth".to_string(),
2439            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2440            optional: true,
2441            default: None,
2442            doc: String::new(),
2443            sanitized: false,
2444            is_boxed: false,
2445            type_rust_path: None,
2446            cfg: None,
2447            typed_default: None,
2448            core_wrapper: CoreWrapper::None,
2449            vec_inner_core_wrapper: CoreWrapper::None,
2450            newtype_wrapper: None,
2451        };
2452        let typ = TypeDef {
2453            name: "SmallUpdate".to_string(),
2454            rust_path: "crate::SmallUpdate".to_string(),
2455            original_rust_path: String::new(),
2456            fields: vec![field],
2457            methods: vec![],
2458            is_opaque: false,
2459            is_clone: true,
2460            is_copy: false,
2461            doc: String::new(),
2462            cfg: None,
2463            is_trait: false,
2464            has_default: true,
2465            has_stripped_cfg_fields: false,
2466            is_return_type: false,
2467            serde_rename_all: None,
2468            has_serde: false,
2469            super_traits: vec![],
2470        };
2471        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2472        // simple_type_mapper maps Usize → "i64", so Optional(Usize) → "Option<i64>"
2473        // The param must be Option<i64>, never Option<Option<i64>>.
2474        assert!(
2475            !output.contains("Option<Option<"),
2476            "positional constructor must not emit double Option: {output}"
2477        );
2478        assert!(
2479            output.contains("Option<i64>"),
2480            "positional constructor should emit Option<inner> for optional Optional(T): {output}"
2481        );
2482    }
2483}