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