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