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                    serde_rename: None,
993                    serde_flatten: false,
994                },
995                FieldDef {
996                    name: "enabled".to_string(),
997                    ty: TypeRef::Primitive(PrimitiveType::Bool),
998                    optional: false,
999                    default: None,
1000                    doc: "Enable feature".to_string(),
1001                    sanitized: false,
1002                    is_boxed: false,
1003                    type_rust_path: None,
1004                    cfg: None,
1005                    typed_default: Some(DefaultValue::BoolLiteral(true)),
1006                    core_wrapper: CoreWrapper::None,
1007                    vec_inner_core_wrapper: CoreWrapper::None,
1008                    newtype_wrapper: None,
1009                    serde_rename: None,
1010                    serde_flatten: false,
1011                },
1012                FieldDef {
1013                    name: "name".to_string(),
1014                    ty: TypeRef::String,
1015                    optional: false,
1016                    default: None,
1017                    doc: "Config name".to_string(),
1018                    sanitized: false,
1019                    is_boxed: false,
1020                    type_rust_path: None,
1021                    cfg: None,
1022                    typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
1023                    core_wrapper: CoreWrapper::None,
1024                    vec_inner_core_wrapper: CoreWrapper::None,
1025                    newtype_wrapper: None,
1026                    serde_rename: None,
1027                    serde_flatten: false,
1028                },
1029            ],
1030            methods: vec![],
1031            is_opaque: false,
1032            is_clone: true,
1033            is_copy: false,
1034            doc: "Configuration type".to_string(),
1035            cfg: None,
1036            is_trait: false,
1037            has_default: true,
1038            has_stripped_cfg_fields: false,
1039            is_return_type: false,
1040            serde_rename_all: None,
1041            has_serde: false,
1042            super_traits: vec![],
1043        }
1044    }
1045
1046    #[test]
1047    fn test_default_value_bool_true_python() {
1048        let field = FieldDef {
1049            name: "enabled".to_string(),
1050            ty: TypeRef::Primitive(PrimitiveType::Bool),
1051            optional: false,
1052            default: None,
1053            doc: String::new(),
1054            sanitized: false,
1055            is_boxed: false,
1056            type_rust_path: None,
1057            cfg: None,
1058            typed_default: Some(DefaultValue::BoolLiteral(true)),
1059            core_wrapper: CoreWrapper::None,
1060            vec_inner_core_wrapper: CoreWrapper::None,
1061            newtype_wrapper: None,
1062            serde_rename: None,
1063            serde_flatten: false,
1064        };
1065        assert_eq!(default_value_for_field(&field, "python"), "True");
1066    }
1067
1068    #[test]
1069    fn test_default_value_bool_false_go() {
1070        let field = FieldDef {
1071            name: "enabled".to_string(),
1072            ty: TypeRef::Primitive(PrimitiveType::Bool),
1073            optional: false,
1074            default: None,
1075            doc: String::new(),
1076            sanitized: false,
1077            is_boxed: false,
1078            type_rust_path: None,
1079            cfg: None,
1080            typed_default: Some(DefaultValue::BoolLiteral(false)),
1081            core_wrapper: CoreWrapper::None,
1082            vec_inner_core_wrapper: CoreWrapper::None,
1083            newtype_wrapper: None,
1084            serde_rename: None,
1085            serde_flatten: false,
1086        };
1087        assert_eq!(default_value_for_field(&field, "go"), "false");
1088    }
1089
1090    #[test]
1091    fn test_default_value_string_literal() {
1092        let field = FieldDef {
1093            name: "name".to_string(),
1094            ty: TypeRef::String,
1095            optional: false,
1096            default: None,
1097            doc: String::new(),
1098            sanitized: false,
1099            is_boxed: false,
1100            type_rust_path: None,
1101            cfg: None,
1102            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1103            core_wrapper: CoreWrapper::None,
1104            vec_inner_core_wrapper: CoreWrapper::None,
1105            newtype_wrapper: None,
1106            serde_rename: None,
1107            serde_flatten: false,
1108        };
1109        assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
1110        assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
1111    }
1112
1113    #[test]
1114    fn test_default_value_int_literal() {
1115        let field = FieldDef {
1116            name: "timeout".to_string(),
1117            ty: TypeRef::Primitive(PrimitiveType::U64),
1118            optional: false,
1119            default: None,
1120            doc: String::new(),
1121            sanitized: false,
1122            is_boxed: false,
1123            type_rust_path: None,
1124            cfg: None,
1125            typed_default: Some(DefaultValue::IntLiteral(42)),
1126            core_wrapper: CoreWrapper::None,
1127            vec_inner_core_wrapper: CoreWrapper::None,
1128            newtype_wrapper: None,
1129            serde_rename: None,
1130            serde_flatten: false,
1131        };
1132        let result = default_value_for_field(&field, "python");
1133        assert_eq!(result, "42");
1134    }
1135
1136    #[test]
1137    fn test_default_value_none() {
1138        let field = FieldDef {
1139            name: "maybe".to_string(),
1140            ty: TypeRef::Optional(Box::new(TypeRef::String)),
1141            optional: true,
1142            default: None,
1143            doc: String::new(),
1144            sanitized: false,
1145            is_boxed: false,
1146            type_rust_path: None,
1147            cfg: None,
1148            typed_default: Some(DefaultValue::None),
1149            core_wrapper: CoreWrapper::None,
1150            vec_inner_core_wrapper: CoreWrapper::None,
1151            newtype_wrapper: None,
1152            serde_rename: None,
1153            serde_flatten: false,
1154        };
1155        assert_eq!(default_value_for_field(&field, "python"), "None");
1156        assert_eq!(default_value_for_field(&field, "go"), "nil");
1157        assert_eq!(default_value_for_field(&field, "java"), "null");
1158        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1159    }
1160
1161    #[test]
1162    fn test_default_value_fallback_string() {
1163        let field = FieldDef {
1164            name: "name".to_string(),
1165            ty: TypeRef::String,
1166            optional: false,
1167            default: Some("\"custom\"".to_string()),
1168            doc: String::new(),
1169            sanitized: false,
1170            is_boxed: false,
1171            type_rust_path: None,
1172            cfg: None,
1173            typed_default: None,
1174            core_wrapper: CoreWrapper::None,
1175            vec_inner_core_wrapper: CoreWrapper::None,
1176            newtype_wrapper: None,
1177            serde_rename: None,
1178            serde_flatten: false,
1179        };
1180        assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1181    }
1182
1183    #[test]
1184    fn test_gen_pyo3_kwargs_constructor() {
1185        let typ = make_test_type();
1186        let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1187            TypeRef::Primitive(p) => format!("{:?}", p),
1188            TypeRef::String | TypeRef::Char => "str".to_string(),
1189            _ => "Any".to_string(),
1190        });
1191
1192        assert!(output.contains("#[new]"));
1193        assert!(output.contains("#[pyo3(signature = ("));
1194        assert!(output.contains("timeout=30"));
1195        assert!(output.contains("enabled=True"));
1196        assert!(output.contains("name=\"default\""));
1197        assert!(output.contains("fn new("));
1198    }
1199
1200    #[test]
1201    fn test_gen_napi_defaults_constructor() {
1202        let typ = make_test_type();
1203        let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1204            TypeRef::Primitive(p) => format!("{:?}", p),
1205            TypeRef::String | TypeRef::Char => "String".to_string(),
1206            _ => "Value".to_string(),
1207        });
1208
1209        assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1210        assert!(output.contains("timeout"));
1211        assert!(output.contains("enabled"));
1212        assert!(output.contains("name"));
1213    }
1214
1215    #[test]
1216    fn test_gen_go_functional_options() {
1217        let typ = make_test_type();
1218        let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1219            TypeRef::Primitive(p) => match p {
1220                PrimitiveType::U64 => "uint64".to_string(),
1221                PrimitiveType::Bool => "bool".to_string(),
1222                _ => "interface{}".to_string(),
1223            },
1224            TypeRef::String | TypeRef::Char => "string".to_string(),
1225            _ => "interface{}".to_string(),
1226        });
1227
1228        assert!(output.contains("type Config struct {"));
1229        assert!(output.contains("type ConfigOption func(*Config)"));
1230        assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1231        assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1232        assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1233        assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1234    }
1235
1236    #[test]
1237    fn test_gen_java_builder() {
1238        let typ = make_test_type();
1239        let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1240            TypeRef::Primitive(p) => match p {
1241                PrimitiveType::U64 => "long".to_string(),
1242                PrimitiveType::Bool => "boolean".to_string(),
1243                _ => "int".to_string(),
1244            },
1245            TypeRef::String | TypeRef::Char => "String".to_string(),
1246            _ => "Object".to_string(),
1247        });
1248
1249        assert!(output.contains("package dev.test;"));
1250        assert!(output.contains("public class ConfigBuilder"));
1251        assert!(output.contains("withTimeout"));
1252        assert!(output.contains("withEnabled"));
1253        assert!(output.contains("withName"));
1254        assert!(output.contains("public Config build()"));
1255    }
1256
1257    #[test]
1258    fn test_gen_csharp_record() {
1259        let typ = make_test_type();
1260        let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1261            TypeRef::Primitive(p) => match p {
1262                PrimitiveType::U64 => "ulong".to_string(),
1263                PrimitiveType::Bool => "bool".to_string(),
1264                _ => "int".to_string(),
1265            },
1266            TypeRef::String | TypeRef::Char => "string".to_string(),
1267            _ => "object".to_string(),
1268        });
1269
1270        assert!(output.contains("namespace MyNamespace;"));
1271        assert!(output.contains("public record Config"));
1272        assert!(output.contains("public ulong Timeout"));
1273        assert!(output.contains("public bool Enabled"));
1274        assert!(output.contains("public string Name"));
1275        assert!(output.contains("init;"));
1276    }
1277
1278    #[test]
1279    fn test_default_value_float_literal() {
1280        let field = FieldDef {
1281            name: "ratio".to_string(),
1282            ty: TypeRef::Primitive(PrimitiveType::F64),
1283            optional: false,
1284            default: None,
1285            doc: String::new(),
1286            sanitized: false,
1287            is_boxed: false,
1288            type_rust_path: None,
1289            cfg: None,
1290            typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1291            core_wrapper: CoreWrapper::None,
1292            vec_inner_core_wrapper: CoreWrapper::None,
1293            newtype_wrapper: None,
1294            serde_rename: None,
1295            serde_flatten: false,
1296        };
1297        let result = default_value_for_field(&field, "python");
1298        assert!(result.contains("1.5"));
1299    }
1300
1301    #[test]
1302    fn test_default_value_no_typed_no_default() {
1303        let field = FieldDef {
1304            name: "count".to_string(),
1305            ty: TypeRef::Primitive(PrimitiveType::U32),
1306            optional: false,
1307            default: None,
1308            doc: String::new(),
1309            sanitized: false,
1310            is_boxed: false,
1311            type_rust_path: None,
1312            cfg: None,
1313            typed_default: None,
1314            core_wrapper: CoreWrapper::None,
1315            vec_inner_core_wrapper: CoreWrapper::None,
1316            newtype_wrapper: None,
1317            serde_rename: None,
1318            serde_flatten: false,
1319        };
1320        // Should fall back to type-based zero value
1321        assert_eq!(default_value_for_field(&field, "python"), "0");
1322        assert_eq!(default_value_for_field(&field, "go"), "0");
1323    }
1324
1325    fn make_field(name: &str, ty: TypeRef) -> FieldDef {
1326        FieldDef {
1327            name: name.to_string(),
1328            ty,
1329            optional: false,
1330            default: None,
1331            doc: String::new(),
1332            sanitized: false,
1333            is_boxed: false,
1334            type_rust_path: None,
1335            cfg: None,
1336            typed_default: None,
1337            core_wrapper: CoreWrapper::None,
1338            vec_inner_core_wrapper: CoreWrapper::None,
1339            newtype_wrapper: None,
1340            serde_rename: None,
1341            serde_flatten: false,
1342        }
1343    }
1344
1345    fn simple_type_mapper(tr: &TypeRef) -> String {
1346        match tr {
1347            TypeRef::Primitive(p) => match p {
1348                PrimitiveType::U64 => "u64".to_string(),
1349                PrimitiveType::Bool => "bool".to_string(),
1350                PrimitiveType::U32 => "u32".to_string(),
1351                _ => "i64".to_string(),
1352            },
1353            TypeRef::String | TypeRef::Char => "String".to_string(),
1354            TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
1355            TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
1356            TypeRef::Named(n) => n.clone(),
1357            _ => "Value".to_string(),
1358        }
1359    }
1360
1361    // -------------------------------------------------------------------------
1362    // default_value_for_field — untested branches
1363    // -------------------------------------------------------------------------
1364
1365    #[test]
1366    fn test_default_value_bool_literal_ruby() {
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(true)),
1378            core_wrapper: CoreWrapper::None,
1379            vec_inner_core_wrapper: CoreWrapper::None,
1380            newtype_wrapper: None,
1381            serde_rename: None,
1382            serde_flatten: false,
1383        };
1384        assert_eq!(default_value_for_field(&field, "ruby"), "true");
1385        assert_eq!(default_value_for_field(&field, "php"), "true");
1386        assert_eq!(default_value_for_field(&field, "csharp"), "true");
1387        assert_eq!(default_value_for_field(&field, "java"), "true");
1388        assert_eq!(default_value_for_field(&field, "rust"), "true");
1389    }
1390
1391    #[test]
1392    fn test_default_value_bool_literal_r() {
1393        let field = FieldDef {
1394            name: "flag".to_string(),
1395            ty: TypeRef::Primitive(PrimitiveType::Bool),
1396            optional: false,
1397            default: None,
1398            doc: String::new(),
1399            sanitized: false,
1400            is_boxed: false,
1401            type_rust_path: None,
1402            cfg: None,
1403            typed_default: Some(DefaultValue::BoolLiteral(false)),
1404            core_wrapper: CoreWrapper::None,
1405            vec_inner_core_wrapper: CoreWrapper::None,
1406            newtype_wrapper: None,
1407            serde_rename: None,
1408            serde_flatten: false,
1409        };
1410        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1411    }
1412
1413    #[test]
1414    fn test_default_value_string_literal_rust() {
1415        let field = FieldDef {
1416            name: "label".to_string(),
1417            ty: TypeRef::String,
1418            optional: false,
1419            default: None,
1420            doc: String::new(),
1421            sanitized: false,
1422            is_boxed: false,
1423            type_rust_path: None,
1424            cfg: None,
1425            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1426            core_wrapper: CoreWrapper::None,
1427            vec_inner_core_wrapper: CoreWrapper::None,
1428            newtype_wrapper: None,
1429            serde_rename: None,
1430            serde_flatten: false,
1431        };
1432        assert_eq!(default_value_for_field(&field, "rust"), "\"hello\".to_string()");
1433    }
1434
1435    #[test]
1436    fn test_default_value_string_literal_escapes_quotes() {
1437        let field = FieldDef {
1438            name: "label".to_string(),
1439            ty: TypeRef::String,
1440            optional: false,
1441            default: None,
1442            doc: String::new(),
1443            sanitized: false,
1444            is_boxed: false,
1445            type_rust_path: None,
1446            cfg: None,
1447            typed_default: Some(DefaultValue::StringLiteral("say \"hi\"".to_string())),
1448            core_wrapper: CoreWrapper::None,
1449            vec_inner_core_wrapper: CoreWrapper::None,
1450            newtype_wrapper: None,
1451            serde_rename: None,
1452            serde_flatten: false,
1453        };
1454        assert_eq!(default_value_for_field(&field, "python"), "\"say \\\"hi\\\"\"");
1455    }
1456
1457    #[test]
1458    fn test_default_value_float_literal_whole_number() {
1459        // A whole-number float should be rendered with ".0" suffix.
1460        let field = FieldDef {
1461            name: "scale".to_string(),
1462            ty: TypeRef::Primitive(PrimitiveType::F32),
1463            optional: false,
1464            default: None,
1465            doc: String::new(),
1466            sanitized: false,
1467            is_boxed: false,
1468            type_rust_path: None,
1469            cfg: None,
1470            typed_default: Some(DefaultValue::FloatLiteral(2.0)),
1471            core_wrapper: CoreWrapper::None,
1472            vec_inner_core_wrapper: CoreWrapper::None,
1473            newtype_wrapper: None,
1474            serde_rename: None,
1475            serde_flatten: false,
1476        };
1477        let result = default_value_for_field(&field, "python");
1478        assert!(result.contains('.'), "whole-number float should contain '.': {result}");
1479    }
1480
1481    #[test]
1482    fn test_default_value_enum_variant_per_language() {
1483        let field = FieldDef {
1484            name: "format".to_string(),
1485            ty: TypeRef::Named("OutputFormat".to_string()),
1486            optional: false,
1487            default: None,
1488            doc: String::new(),
1489            sanitized: false,
1490            is_boxed: false,
1491            type_rust_path: None,
1492            cfg: None,
1493            typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
1494            core_wrapper: CoreWrapper::None,
1495            vec_inner_core_wrapper: CoreWrapper::None,
1496            newtype_wrapper: None,
1497            serde_rename: None,
1498            serde_flatten: false,
1499        };
1500        assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
1501        assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
1502        assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
1503        assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
1504        assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
1505        assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
1506        assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
1507        assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
1508    }
1509
1510    #[test]
1511    fn test_default_value_empty_vec_per_language() {
1512        let field = FieldDef {
1513            name: "items".to_string(),
1514            ty: TypeRef::Vec(Box::new(TypeRef::String)),
1515            optional: false,
1516            default: None,
1517            doc: String::new(),
1518            sanitized: false,
1519            is_boxed: false,
1520            type_rust_path: None,
1521            cfg: None,
1522            typed_default: Some(DefaultValue::Empty),
1523            core_wrapper: CoreWrapper::None,
1524            vec_inner_core_wrapper: CoreWrapper::None,
1525            newtype_wrapper: None,
1526            serde_rename: None,
1527            serde_flatten: false,
1528        };
1529        assert_eq!(default_value_for_field(&field, "python"), "[]");
1530        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1531        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1532        assert_eq!(default_value_for_field(&field, "go"), "nil");
1533        assert_eq!(default_value_for_field(&field, "java"), "List.of()");
1534        assert_eq!(default_value_for_field(&field, "php"), "[]");
1535        assert_eq!(default_value_for_field(&field, "r"), "c()");
1536        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1537    }
1538
1539    #[test]
1540    fn test_default_value_empty_map_per_language() {
1541        let field = FieldDef {
1542            name: "meta".to_string(),
1543            ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1544            optional: false,
1545            default: None,
1546            doc: String::new(),
1547            sanitized: false,
1548            is_boxed: false,
1549            type_rust_path: None,
1550            cfg: None,
1551            typed_default: Some(DefaultValue::Empty),
1552            core_wrapper: CoreWrapper::None,
1553            vec_inner_core_wrapper: CoreWrapper::None,
1554            newtype_wrapper: None,
1555            serde_rename: None,
1556            serde_flatten: false,
1557        };
1558        assert_eq!(default_value_for_field(&field, "python"), "{}");
1559        assert_eq!(default_value_for_field(&field, "go"), "nil");
1560        assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
1561        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1562    }
1563
1564    #[test]
1565    fn test_default_value_empty_bool_primitive() {
1566        let field = FieldDef {
1567            name: "flag".to_string(),
1568            ty: TypeRef::Primitive(PrimitiveType::Bool),
1569            optional: false,
1570            default: None,
1571            doc: String::new(),
1572            sanitized: false,
1573            is_boxed: false,
1574            type_rust_path: None,
1575            cfg: None,
1576            typed_default: Some(DefaultValue::Empty),
1577            core_wrapper: CoreWrapper::None,
1578            vec_inner_core_wrapper: CoreWrapper::None,
1579            newtype_wrapper: None,
1580            serde_rename: None,
1581            serde_flatten: false,
1582        };
1583        assert_eq!(default_value_for_field(&field, "python"), "False");
1584        assert_eq!(default_value_for_field(&field, "ruby"), "false");
1585        assert_eq!(default_value_for_field(&field, "go"), "false");
1586    }
1587
1588    #[test]
1589    fn test_default_value_empty_float_primitive() {
1590        let field = FieldDef {
1591            name: "ratio".to_string(),
1592            ty: TypeRef::Primitive(PrimitiveType::F64),
1593            optional: false,
1594            default: None,
1595            doc: String::new(),
1596            sanitized: false,
1597            is_boxed: false,
1598            type_rust_path: None,
1599            cfg: None,
1600            typed_default: Some(DefaultValue::Empty),
1601            core_wrapper: CoreWrapper::None,
1602            vec_inner_core_wrapper: CoreWrapper::None,
1603            newtype_wrapper: None,
1604            serde_rename: None,
1605            serde_flatten: false,
1606        };
1607        assert_eq!(default_value_for_field(&field, "python"), "0.0");
1608    }
1609
1610    #[test]
1611    fn test_default_value_empty_string_type() {
1612        let field = FieldDef {
1613            name: "label".to_string(),
1614            ty: TypeRef::String,
1615            optional: false,
1616            default: None,
1617            doc: String::new(),
1618            sanitized: false,
1619            is_boxed: false,
1620            type_rust_path: None,
1621            cfg: None,
1622            typed_default: Some(DefaultValue::Empty),
1623            core_wrapper: CoreWrapper::None,
1624            vec_inner_core_wrapper: CoreWrapper::None,
1625            newtype_wrapper: None,
1626            serde_rename: None,
1627            serde_flatten: false,
1628        };
1629        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1630        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1631    }
1632
1633    #[test]
1634    fn test_default_value_empty_bytes_type() {
1635        let field = FieldDef {
1636            name: "data".to_string(),
1637            ty: TypeRef::Bytes,
1638            optional: false,
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::Empty),
1646            core_wrapper: CoreWrapper::None,
1647            vec_inner_core_wrapper: CoreWrapper::None,
1648            newtype_wrapper: None,
1649            serde_rename: None,
1650            serde_flatten: false,
1651        };
1652        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1653        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1654        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1655    }
1656
1657    #[test]
1658    fn test_default_value_empty_json_type() {
1659        let field = FieldDef {
1660            name: "payload".to_string(),
1661            ty: TypeRef::Json,
1662            optional: false,
1663            default: None,
1664            doc: String::new(),
1665            sanitized: false,
1666            is_boxed: false,
1667            type_rust_path: None,
1668            cfg: None,
1669            typed_default: Some(DefaultValue::Empty),
1670            core_wrapper: CoreWrapper::None,
1671            vec_inner_core_wrapper: CoreWrapper::None,
1672            newtype_wrapper: None,
1673            serde_rename: None,
1674            serde_flatten: false,
1675        };
1676        assert_eq!(default_value_for_field(&field, "python"), "{}");
1677        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1678        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1679        assert_eq!(default_value_for_field(&field, "r"), "list()");
1680        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1681    }
1682
1683    #[test]
1684    fn test_default_value_none_ruby_php_r() {
1685        let field = FieldDef {
1686            name: "maybe".to_string(),
1687            ty: TypeRef::Optional(Box::new(TypeRef::String)),
1688            optional: true,
1689            default: None,
1690            doc: String::new(),
1691            sanitized: false,
1692            is_boxed: false,
1693            type_rust_path: None,
1694            cfg: None,
1695            typed_default: Some(DefaultValue::None),
1696            core_wrapper: CoreWrapper::None,
1697            vec_inner_core_wrapper: CoreWrapper::None,
1698            newtype_wrapper: None,
1699            serde_rename: None,
1700            serde_flatten: false,
1701        };
1702        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1703        assert_eq!(default_value_for_field(&field, "php"), "null");
1704        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1705        assert_eq!(default_value_for_field(&field, "rust"), "None");
1706    }
1707
1708    // -------------------------------------------------------------------------
1709    // Fallback (no typed_default, no default) — type-based zero values
1710    // -------------------------------------------------------------------------
1711
1712    #[test]
1713    fn test_default_value_fallback_bool_all_languages() {
1714        let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
1715        assert_eq!(default_value_for_field(&field, "python"), "False");
1716        assert_eq!(default_value_for_field(&field, "ruby"), "false");
1717        assert_eq!(default_value_for_field(&field, "csharp"), "false");
1718        assert_eq!(default_value_for_field(&field, "java"), "false");
1719        assert_eq!(default_value_for_field(&field, "php"), "false");
1720        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1721        assert_eq!(default_value_for_field(&field, "rust"), "false");
1722    }
1723
1724    #[test]
1725    fn test_default_value_fallback_float() {
1726        let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
1727        assert_eq!(default_value_for_field(&field, "python"), "0.0");
1728        assert_eq!(default_value_for_field(&field, "rust"), "0.0");
1729    }
1730
1731    #[test]
1732    fn test_default_value_fallback_string_all_languages() {
1733        let field = make_field("name", TypeRef::String);
1734        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1735        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1736        assert_eq!(default_value_for_field(&field, "go"), "\"\"");
1737        assert_eq!(default_value_for_field(&field, "java"), "\"\"");
1738        assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
1739        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1740        assert_eq!(default_value_for_field(&field, "r"), "\"\"");
1741        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1742    }
1743
1744    #[test]
1745    fn test_default_value_fallback_bytes_all_languages() {
1746        let field = make_field("data", TypeRef::Bytes);
1747        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1748        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1749        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1750        assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
1751        assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
1752        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1753        assert_eq!(default_value_for_field(&field, "r"), "raw()");
1754        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1755    }
1756
1757    #[test]
1758    fn test_default_value_fallback_optional() {
1759        let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
1760        assert_eq!(default_value_for_field(&field, "python"), "None");
1761        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1762        assert_eq!(default_value_for_field(&field, "go"), "nil");
1763        assert_eq!(default_value_for_field(&field, "java"), "null");
1764        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1765        assert_eq!(default_value_for_field(&field, "php"), "null");
1766        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1767        assert_eq!(default_value_for_field(&field, "rust"), "None");
1768    }
1769
1770    #[test]
1771    fn test_default_value_fallback_vec_all_languages() {
1772        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
1773        assert_eq!(default_value_for_field(&field, "python"), "[]");
1774        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1775        assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
1776        assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
1777        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1778        assert_eq!(default_value_for_field(&field, "php"), "[]");
1779        assert_eq!(default_value_for_field(&field, "r"), "c()");
1780        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1781    }
1782
1783    #[test]
1784    fn test_default_value_fallback_map_all_languages() {
1785        let field = make_field(
1786            "meta",
1787            TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1788        );
1789        assert_eq!(default_value_for_field(&field, "python"), "{}");
1790        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1791        assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
1792        assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
1793        assert_eq!(
1794            default_value_for_field(&field, "csharp"),
1795            "new Dictionary<string, object>()"
1796        );
1797        assert_eq!(default_value_for_field(&field, "php"), "[]");
1798        assert_eq!(default_value_for_field(&field, "r"), "list()");
1799        assert_eq!(
1800            default_value_for_field(&field, "rust"),
1801            "std::collections::HashMap::new()"
1802        );
1803    }
1804
1805    #[test]
1806    fn test_default_value_fallback_json_all_languages() {
1807        let field = make_field("payload", TypeRef::Json);
1808        assert_eq!(default_value_for_field(&field, "python"), "{}");
1809        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1810        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1811        assert_eq!(default_value_for_field(&field, "r"), "list()");
1812        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1813    }
1814
1815    #[test]
1816    fn test_default_value_fallback_named_type() {
1817        let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
1818        assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
1819        assert_eq!(default_value_for_field(&field, "python"), "None");
1820        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1821        assert_eq!(default_value_for_field(&field, "go"), "nil");
1822        assert_eq!(default_value_for_field(&field, "java"), "null");
1823        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1824        assert_eq!(default_value_for_field(&field, "php"), "null");
1825        assert_eq!(default_value_for_field(&field, "r"), "NULL");
1826    }
1827
1828    #[test]
1829    fn test_default_value_fallback_duration() {
1830        // Duration falls through to the wildcard arm
1831        let field = make_field("timeout", TypeRef::Duration);
1832        assert_eq!(default_value_for_field(&field, "python"), "None");
1833        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1834    }
1835
1836    // -------------------------------------------------------------------------
1837    // gen_magnus_kwargs_constructor — positional (≤15 fields)
1838    // -------------------------------------------------------------------------
1839
1840    #[test]
1841    fn test_gen_magnus_kwargs_constructor_positional_basic() {
1842        let typ = make_test_type();
1843        let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1844
1845        assert!(output.contains("fn new("), "should have fn new");
1846        // All params are Option<T>
1847        assert!(output.contains("Option<u64>"), "timeout should be Option<u64>");
1848        assert!(output.contains("Option<bool>"), "enabled should be Option<bool>");
1849        assert!(output.contains("Option<String>"), "name should be Option<String>");
1850        assert!(output.contains("-> Self {"), "should return Self");
1851        // timeout has IntLiteral(30), use_unwrap_or_default is false for Named → uses unwrap_or
1852        assert!(
1853            output.contains("timeout: timeout.unwrap_or(30),"),
1854            "should apply int default"
1855        );
1856        // enabled has BoolLiteral(true), not unwrap_or_default
1857        assert!(
1858            output.contains("enabled: enabled.unwrap_or(true),"),
1859            "should apply bool default"
1860        );
1861        // name has StringLiteral, not unwrap_or_default
1862        assert!(
1863            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1864            "should apply string default"
1865        );
1866    }
1867
1868    #[test]
1869    fn test_gen_magnus_kwargs_constructor_positional_optional_field() {
1870        // A field with optional=true should be assigned directly (no unwrap)
1871        let mut typ = make_test_type();
1872        typ.fields.push(FieldDef {
1873            name: "extra".to_string(),
1874            ty: TypeRef::String,
1875            optional: true,
1876            default: None,
1877            doc: String::new(),
1878            sanitized: false,
1879            is_boxed: false,
1880            type_rust_path: None,
1881            cfg: None,
1882            typed_default: None,
1883            core_wrapper: CoreWrapper::None,
1884            vec_inner_core_wrapper: CoreWrapper::None,
1885            newtype_wrapper: None,
1886            serde_rename: None,
1887            serde_flatten: false,
1888        });
1889        let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1890        // Optional field param is Option<String> and assigned directly
1891        assert!(output.contains("extra,"), "optional field should be assigned directly");
1892        assert!(!output.contains("extra.unwrap"), "optional field should not use unwrap");
1893    }
1894
1895    #[test]
1896    fn test_gen_magnus_kwargs_constructor_unwrap_or_default() {
1897        // A primitive field with no typed_default and no default should use unwrap_or_default()
1898        let mut typ = make_test_type();
1899        typ.fields.push(FieldDef {
1900            name: "count".to_string(),
1901            ty: TypeRef::Primitive(PrimitiveType::U32),
1902            optional: false,
1903            default: None,
1904            doc: String::new(),
1905            sanitized: false,
1906            is_boxed: false,
1907            type_rust_path: None,
1908            cfg: None,
1909            typed_default: None,
1910            core_wrapper: CoreWrapper::None,
1911            vec_inner_core_wrapper: CoreWrapper::None,
1912            newtype_wrapper: None,
1913            serde_rename: None,
1914            serde_flatten: false,
1915        });
1916        let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1917        assert!(
1918            output.contains("count: count.unwrap_or_default(),"),
1919            "plain primitive with no default should use unwrap_or_default"
1920        );
1921    }
1922
1923    #[test]
1924    fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
1925        // Build a type with 16 fields (> MAGNUS_MAX_ARITY = 15) to force hash path
1926        let mut fields: Vec<FieldDef> = (0..16)
1927            .map(|i| FieldDef {
1928                name: format!("field_{i}"),
1929                ty: TypeRef::Primitive(PrimitiveType::U32),
1930                optional: false,
1931                default: None,
1932                doc: String::new(),
1933                sanitized: false,
1934                is_boxed: false,
1935                type_rust_path: None,
1936                cfg: None,
1937                typed_default: None,
1938                core_wrapper: CoreWrapper::None,
1939                vec_inner_core_wrapper: CoreWrapper::None,
1940                newtype_wrapper: None,
1941                serde_rename: None,
1942                serde_flatten: false,
1943            })
1944            .collect();
1945        // Make one field optional to exercise that branch in the hash constructor
1946        fields[0].optional = true;
1947
1948        let typ = TypeDef {
1949            name: "BigConfig".to_string(),
1950            rust_path: "crate::BigConfig".to_string(),
1951            original_rust_path: String::new(),
1952            fields,
1953            methods: vec![],
1954            is_opaque: false,
1955            is_clone: true,
1956            is_copy: false,
1957            doc: String::new(),
1958            cfg: None,
1959            is_trait: false,
1960            has_default: true,
1961            has_stripped_cfg_fields: false,
1962            is_return_type: false,
1963            serde_rename_all: None,
1964            has_serde: false,
1965            super_traits: vec![],
1966        };
1967        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1968
1969        assert!(
1970            output.contains("Option<magnus::RHash>"),
1971            "should accept RHash via scan_args"
1972        );
1973        assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
1974        // Optional field uses and_then without unwrap_or
1975        assert!(
1976            output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
1977            "optional field should use and_then"
1978        );
1979        assert!(
1980            output.contains("field_0:").then_some(()).is_some(),
1981            "field_0 should appear in output"
1982        );
1983    }
1984
1985    // -------------------------------------------------------------------------
1986    // gen_php_kwargs_constructor
1987    // -------------------------------------------------------------------------
1988
1989    #[test]
1990    fn test_gen_php_kwargs_constructor_basic() {
1991        let typ = make_test_type();
1992        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1993
1994        assert!(
1995            output.contains("pub fn __construct("),
1996            "should use PHP constructor name"
1997        );
1998        // All params are Option<T>
1999        assert!(
2000            output.contains("timeout: Option<u64>"),
2001            "timeout param should be Option<u64>"
2002        );
2003        assert!(
2004            output.contains("enabled: Option<bool>"),
2005            "enabled param should be Option<bool>"
2006        );
2007        assert!(
2008            output.contains("name: Option<String>"),
2009            "name param should be Option<String>"
2010        );
2011        assert!(output.contains("-> Self {"), "should return Self");
2012        assert!(
2013            output.contains("timeout: timeout.unwrap_or(30),"),
2014            "should apply int default for timeout"
2015        );
2016        assert!(
2017            output.contains("enabled: enabled.unwrap_or(true),"),
2018            "should apply bool default for enabled"
2019        );
2020        assert!(
2021            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
2022            "should apply string default for name"
2023        );
2024    }
2025
2026    #[test]
2027    fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
2028        let mut typ = make_test_type();
2029        typ.fields.push(FieldDef {
2030            name: "tag".to_string(),
2031            ty: TypeRef::String,
2032            optional: true,
2033            default: None,
2034            doc: String::new(),
2035            sanitized: false,
2036            is_boxed: false,
2037            type_rust_path: None,
2038            cfg: None,
2039            typed_default: None,
2040            core_wrapper: CoreWrapper::None,
2041            vec_inner_core_wrapper: CoreWrapper::None,
2042            newtype_wrapper: None,
2043            serde_rename: None,
2044            serde_flatten: false,
2045        });
2046        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2047        assert!(
2048            output.contains("tag,"),
2049            "optional field should be passed through directly"
2050        );
2051        assert!(!output.contains("tag.unwrap"), "optional field should not call unwrap");
2052    }
2053
2054    #[test]
2055    fn test_gen_php_kwargs_constructor_unwrap_or_default_for_primitive() {
2056        let mut typ = make_test_type();
2057        typ.fields.push(FieldDef {
2058            name: "retries".to_string(),
2059            ty: TypeRef::Primitive(PrimitiveType::U32),
2060            optional: false,
2061            default: None,
2062            doc: String::new(),
2063            sanitized: false,
2064            is_boxed: false,
2065            type_rust_path: None,
2066            cfg: None,
2067            typed_default: None,
2068            core_wrapper: CoreWrapper::None,
2069            vec_inner_core_wrapper: CoreWrapper::None,
2070            newtype_wrapper: None,
2071            serde_rename: None,
2072            serde_flatten: false,
2073        });
2074        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2075        assert!(
2076            output.contains("retries: retries.unwrap_or_default(),"),
2077            "primitive with no default should use unwrap_or_default"
2078        );
2079    }
2080
2081    // -------------------------------------------------------------------------
2082    // gen_rustler_kwargs_constructor
2083    // -------------------------------------------------------------------------
2084
2085    #[test]
2086    fn test_gen_rustler_kwargs_constructor_basic() {
2087        let typ = make_test_type();
2088        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2089
2090        assert!(
2091            output.contains("pub fn new(opts: std::collections::HashMap<String, rustler::Term>)"),
2092            "should accept HashMap of Terms"
2093        );
2094        assert!(output.contains("Self {"), "should construct Self");
2095        // timeout has IntLiteral(30) — explicit unwrap_or
2096        assert!(
2097            output.contains("timeout: opts.get(\"timeout\").and_then(|t| t.decode().ok()).unwrap_or(30),"),
2098            "should apply int default for timeout"
2099        );
2100        // enabled has BoolLiteral(true) — explicit unwrap_or
2101        assert!(
2102            output.contains("enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),"),
2103            "should apply bool default for enabled"
2104        );
2105    }
2106
2107    #[test]
2108    fn test_gen_rustler_kwargs_constructor_optional_field() {
2109        let mut typ = make_test_type();
2110        typ.fields.push(FieldDef {
2111            name: "extra".to_string(),
2112            ty: TypeRef::String,
2113            optional: true,
2114            default: None,
2115            doc: String::new(),
2116            sanitized: false,
2117            is_boxed: false,
2118            type_rust_path: None,
2119            cfg: None,
2120            typed_default: None,
2121            core_wrapper: CoreWrapper::None,
2122            vec_inner_core_wrapper: CoreWrapper::None,
2123            newtype_wrapper: None,
2124            serde_rename: None,
2125            serde_flatten: false,
2126        });
2127        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2128        assert!(
2129            output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
2130            "optional field should decode without unwrap"
2131        );
2132    }
2133
2134    #[test]
2135    fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
2136        let mut typ = make_test_type();
2137        typ.fields.push(FieldDef {
2138            name: "inner".to_string(),
2139            ty: TypeRef::Named("InnerConfig".to_string()),
2140            optional: false,
2141            default: None,
2142            doc: String::new(),
2143            sanitized: false,
2144            is_boxed: false,
2145            type_rust_path: None,
2146            cfg: None,
2147            typed_default: None,
2148            core_wrapper: CoreWrapper::None,
2149            vec_inner_core_wrapper: CoreWrapper::None,
2150            newtype_wrapper: None,
2151            serde_rename: None,
2152            serde_flatten: false,
2153        });
2154        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2155        assert!(
2156            output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2157            "Named type with no default should use unwrap_or_default"
2158        );
2159    }
2160
2161    #[test]
2162    fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
2163        // A String field with a StringLiteral default contains "::", triggering the
2164        // is_enum_variant_default check — should fall back to unwrap_or_default().
2165        let mut typ = make_test_type();
2166        // 'name' field in make_test_type() has StringLiteral("default") — verify it
2167        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2168        assert!(
2169            output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2170            "String field with quoted default should use unwrap_or_default"
2171        );
2172        // Also verify a plain string field (no default) also falls through to unwrap_or_default
2173        typ.fields.push(FieldDef {
2174            name: "label".to_string(),
2175            ty: TypeRef::String,
2176            optional: false,
2177            default: None,
2178            doc: String::new(),
2179            sanitized: false,
2180            is_boxed: false,
2181            type_rust_path: None,
2182            cfg: None,
2183            typed_default: None,
2184            core_wrapper: CoreWrapper::None,
2185            vec_inner_core_wrapper: CoreWrapper::None,
2186            newtype_wrapper: None,
2187            serde_rename: None,
2188            serde_flatten: false,
2189        });
2190        let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2191        assert!(
2192            output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2193            "String field with no default should use unwrap_or_default"
2194        );
2195    }
2196
2197    // -------------------------------------------------------------------------
2198    // gen_extendr_kwargs_constructor
2199    // -------------------------------------------------------------------------
2200
2201    #[test]
2202    fn test_gen_extendr_kwargs_constructor_basic() {
2203        let typ = make_test_type();
2204        let empty_enums = ahash::AHashSet::new();
2205        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2206
2207        assert!(output.contains("#[extendr]"), "should have extendr attribute");
2208        assert!(
2209            output.contains("pub fn new_config("),
2210            "function name should be lowercase type name"
2211        );
2212        // Fields appear as Option<T> parameters — Rust does not support param defaults.
2213        assert!(
2214            output.contains("timeout: Option<u64>"),
2215            "should accept timeout as Option<u64>: {output}"
2216        );
2217        assert!(
2218            output.contains("enabled: Option<bool>"),
2219            "should accept enabled as Option<bool>: {output}"
2220        );
2221        assert!(
2222            output.contains("name: Option<String>"),
2223            "should accept name as Option<String>: {output}"
2224        );
2225        assert!(output.contains("-> Config {"), "should return Config");
2226        assert!(
2227            output.contains("let mut __out = <Config>::default();"),
2228            "should base on Default impl: {output}"
2229        );
2230        assert!(
2231            output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
2232            "should overlay caller-provided timeout"
2233        );
2234        assert!(
2235            output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
2236            "should overlay caller-provided enabled"
2237        );
2238        assert!(
2239            output.contains("if let Some(v) = name { __out.name = v; }"),
2240            "should overlay caller-provided name"
2241        );
2242    }
2243
2244    #[test]
2245    fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
2246        // Rust function-parameter defaults (`x: T = expr`) are a syntax error and
2247        // extendr 0.9 only supports defaults via the `#[extendr(default = "...")]`
2248        // attribute.  Verify that no field is emitted with a Rust-syntax default.
2249        let typ = make_test_type();
2250        let empty_enums = ahash::AHashSet::new();
2251        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2252        assert!(
2253            !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
2254            "constructor must not use Rust-syntax param defaults: {output}"
2255        );
2256    }
2257
2258    // -------------------------------------------------------------------------
2259    // gen_go_functional_options — tuple-field filtering
2260    // -------------------------------------------------------------------------
2261
2262    #[test]
2263    fn test_gen_go_functional_options_skips_tuple_fields() {
2264        let mut typ = make_test_type();
2265        typ.fields.push(FieldDef {
2266            name: "_0".to_string(),
2267            ty: TypeRef::Primitive(PrimitiveType::U32),
2268            optional: false,
2269            default: None,
2270            doc: String::new(),
2271            sanitized: false,
2272            is_boxed: false,
2273            type_rust_path: None,
2274            cfg: None,
2275            typed_default: None,
2276            core_wrapper: CoreWrapper::None,
2277            vec_inner_core_wrapper: CoreWrapper::None,
2278            newtype_wrapper: None,
2279            serde_rename: None,
2280            serde_flatten: false,
2281        });
2282        let output = gen_go_functional_options(&typ, &simple_type_mapper);
2283        assert!(
2284            !output.contains("_0"),
2285            "tuple field _0 should be filtered out from Go output"
2286        );
2287    }
2288
2289    // -------------------------------------------------------------------------
2290    // as_type_path_prefix — tested indirectly through hash constructor
2291    // -------------------------------------------------------------------------
2292
2293    #[test]
2294    fn test_gen_magnus_hash_constructor_generic_type_prefix() {
2295        // A field with a Vec type should use <Vec<...>>::try_convert UFCS form
2296        let fields: Vec<FieldDef> = (0..16)
2297            .map(|i| FieldDef {
2298                name: format!("field_{i}"),
2299                ty: if i == 0 {
2300                    TypeRef::Vec(Box::new(TypeRef::String))
2301                } else {
2302                    TypeRef::Primitive(PrimitiveType::U32)
2303                },
2304                optional: false,
2305                default: None,
2306                doc: String::new(),
2307                sanitized: false,
2308                is_boxed: false,
2309                type_rust_path: None,
2310                cfg: None,
2311                typed_default: None,
2312                core_wrapper: CoreWrapper::None,
2313                vec_inner_core_wrapper: CoreWrapper::None,
2314                newtype_wrapper: None,
2315                serde_rename: None,
2316                serde_flatten: false,
2317            })
2318            .collect();
2319        let typ = TypeDef {
2320            name: "WideConfig".to_string(),
2321            rust_path: "crate::WideConfig".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        // Vec<String> is a generic type; must use <Vec<String>>::try_convert
2340        assert!(
2341            output.contains("<Vec<String>>::try_convert"),
2342            "generic types should use UFCS angle-bracket prefix: {output}"
2343        );
2344    }
2345
2346    // -------------------------------------------------------------------------
2347    // Bug B regression: Option<Option<T>> must not appear when field.optional==true
2348    // and field.ty==Optional(T). This happens for "Update" structs where the core
2349    // field is Option<Option<T>> — the binding flattens to Option<T>.
2350    // -------------------------------------------------------------------------
2351
2352    #[test]
2353    fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
2354        // field with optional=true AND ty=Optional(Usize) — represents a core Option<Option<usize>>
2355        // that should flatten to Option<usize> in the binding constructor.
2356        // simple_type_mapper maps Usize → "i64" (catch-all primitive arm).
2357        let field = FieldDef {
2358            name: "max_depth".to_string(),
2359            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2360            optional: true,
2361            default: None,
2362            doc: String::new(),
2363            sanitized: false,
2364            is_boxed: false,
2365            type_rust_path: None,
2366            cfg: None,
2367            typed_default: None,
2368            core_wrapper: CoreWrapper::None,
2369            vec_inner_core_wrapper: CoreWrapper::None,
2370            newtype_wrapper: None,
2371            serde_rename: None,
2372            serde_flatten: false,
2373        };
2374        // Build a large type (>15 fields) so the hash constructor is used
2375        let mut fields: Vec<FieldDef> = (0..15)
2376            .map(|i| FieldDef {
2377                name: format!("field_{i}"),
2378                ty: TypeRef::Primitive(PrimitiveType::U32),
2379                optional: false,
2380                default: None,
2381                doc: String::new(),
2382                sanitized: false,
2383                is_boxed: false,
2384                type_rust_path: None,
2385                cfg: None,
2386                typed_default: None,
2387                core_wrapper: CoreWrapper::None,
2388                vec_inner_core_wrapper: CoreWrapper::None,
2389                newtype_wrapper: None,
2390                serde_rename: None,
2391                serde_flatten: false,
2392            })
2393            .collect();
2394        fields.push(field);
2395        let typ = TypeDef {
2396            name: "UpdateConfig".to_string(),
2397            rust_path: "crate::UpdateConfig".to_string(),
2398            original_rust_path: String::new(),
2399            fields,
2400            methods: vec![],
2401            is_opaque: false,
2402            is_clone: true,
2403            is_copy: false,
2404            doc: String::new(),
2405            cfg: None,
2406            is_trait: false,
2407            has_default: true,
2408            has_stripped_cfg_fields: false,
2409            is_return_type: false,
2410            serde_rename_all: None,
2411            has_serde: false,
2412            super_traits: vec![],
2413        };
2414        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2415        // The try_convert call must be for the inner type (i64, as mapped by simple_type_mapper),
2416        // not Option<i64> (which would yield Option<Option<i64>>).
2417        assert!(
2418            !output.contains("Option<Option<"),
2419            "hash constructor must not emit double Option: {output}"
2420        );
2421        assert!(
2422            output.contains("i64::try_convert"),
2423            "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
2424        );
2425    }
2426
2427    #[test]
2428    fn test_magnus_positional_constructor_no_double_option_when_ty_is_optional() {
2429        // field with optional=true AND ty=Optional(Usize) — small type uses positional constructor
2430        // simple_type_mapper maps Usize → "i64"
2431        let field = FieldDef {
2432            name: "max_depth".to_string(),
2433            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2434            optional: true,
2435            default: None,
2436            doc: String::new(),
2437            sanitized: false,
2438            is_boxed: false,
2439            type_rust_path: None,
2440            cfg: None,
2441            typed_default: None,
2442            core_wrapper: CoreWrapper::None,
2443            vec_inner_core_wrapper: CoreWrapper::None,
2444            newtype_wrapper: None,
2445            serde_rename: None,
2446            serde_flatten: false,
2447        };
2448        let typ = TypeDef {
2449            name: "SmallUpdate".to_string(),
2450            rust_path: "crate::SmallUpdate".to_string(),
2451            original_rust_path: String::new(),
2452            fields: vec![field],
2453            methods: vec![],
2454            is_opaque: false,
2455            is_clone: true,
2456            is_copy: false,
2457            doc: String::new(),
2458            cfg: None,
2459            is_trait: false,
2460            has_default: true,
2461            has_stripped_cfg_fields: false,
2462            is_return_type: false,
2463            serde_rename_all: None,
2464            has_serde: false,
2465            super_traits: vec![],
2466        };
2467        let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
2468        // simple_type_mapper maps Usize → "i64", so Optional(Usize) → "Option<i64>"
2469        // The param must be Option<i64>, never Option<Option<i64>>.
2470        assert!(
2471            !output.contains("Option<Option<"),
2472            "positional constructor must not emit double Option: {output}"
2473        );
2474        assert!(
2475            output.contains("Option<i64>"),
2476            "positional constructor should emit Option<inner> for optional Optional(T): {output}"
2477        );
2478    }
2479}