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