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