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