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