Skip to main content

alef_codegen/
config_gen.rs

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