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