Skip to main content

alef_codegen/
config_gen.rs

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