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