Skip to main content

alef_codegen/
config_gen.rs

1use alef_core::ir::{DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
2use heck::{ToPascalCase, ToShoutySnakeCase};
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 mut lines = Vec::new();
29    lines.push("#[new]".to_string());
30
31    // Build the signature line with defaults
32    let mut sig_parts = Vec::new();
33    for field in &typ.fields {
34        let default_str = default_value_for_field(field, "python");
35        sig_parts.push(format!("{}={}", field.name, default_str));
36    }
37    let signature = format!("#[pyo3(signature = ({}))]", sig_parts.join(", "));
38    lines.push(signature);
39
40    // Function signature
41    lines.push("fn new(".to_string());
42    for (i, field) in typ.fields.iter().enumerate() {
43        let type_str = type_mapper(&field.ty);
44        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
45        lines.push(format!("    {}: {}{}", field.name, type_str, comma));
46    }
47    lines.push(") -> Self {".to_string());
48
49    // Body
50    lines.push("    Self {".to_string());
51    for field in &typ.fields {
52        lines.push(format!("        {},", field.name));
53    }
54    lines.push("    }".to_string());
55    lines.push("}".to_string());
56
57    lines.join("\n")
58}
59
60/// Generate NAPI constructor that applies defaults for missing optional fields.
61pub fn gen_napi_defaults_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
62    let mut lines = Vec::new();
63    lines.push("pub fn new(mut env: napi::Env, obj: napi::Object) -> napi::Result<Self> {".to_string());
64
65    // Field assignments with defaults
66    for field in &typ.fields {
67        let type_str = type_mapper(&field.ty);
68        let default_str = default_value_for_field(field, "rust");
69        lines.push(format!(
70            "    let {}: {} = obj.get(\"{}\").unwrap_or({})?;",
71            field.name, type_str, field.name, default_str
72        ));
73    }
74
75    lines.push("    Ok(Self {".to_string());
76    for field in &typ.fields {
77        lines.push(format!("        {},", field.name));
78    }
79    lines.push("    })".to_string());
80    lines.push("}".to_string());
81
82    lines.join("\n")
83}
84
85/// Generate Go functional options pattern for a type with `has_default`.
86/// Returns: type definition + Option type + WithField functions + NewConfig constructor
87pub fn gen_go_functional_options(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
88    let mut lines = Vec::new();
89
90    // Type definition
91    lines.push(format!("// {} is a configuration type.", typ.name));
92    lines.push(format!("type {} struct {{", typ.name));
93    for field in &typ.fields {
94        if is_tuple_field(field) {
95            continue;
96        }
97        let go_type = type_mapper(&field.ty);
98        lines.push(format!("    {} {}", field.name.to_pascal_case(), go_type));
99    }
100    lines.push("}".to_string());
101    lines.push("".to_string());
102
103    // Option function type
104    lines.push(format!(
105        "// {}Option is a functional option for {}.",
106        typ.name, typ.name
107    ));
108    lines.push(format!("type {}Option func(*{})", typ.name, typ.name));
109    lines.push("".to_string());
110
111    // WithField functions
112    for field in &typ.fields {
113        if is_tuple_field(field) {
114            continue;
115        }
116        let option_name = format!("With{}{}", typ.name, field.name.to_pascal_case());
117        let go_type = type_mapper(&field.ty);
118        lines.push(format!("// {} sets the {}.", option_name, field.name));
119        lines.push(format!("func {}(val {}) {}Option {{", option_name, go_type, typ.name));
120        lines.push(format!("    return func(c *{}) {{", typ.name));
121        lines.push(format!("        c.{} = val", field.name.to_pascal_case()));
122        lines.push("    }".to_string());
123        lines.push("}".to_string());
124        lines.push("".to_string());
125    }
126
127    // New constructor
128    lines.push(format!(
129        "// New{} creates a new {} with default values and applies options.",
130        typ.name, typ.name
131    ));
132    lines.push(format!(
133        "func New{}(opts ...{}Option) *{} {{",
134        typ.name, typ.name, typ.name
135    ));
136    lines.push(format!("    c := &{} {{", typ.name));
137    for field in &typ.fields {
138        if is_tuple_field(field) {
139            continue;
140        }
141        let default_str = default_value_for_field(field, "go");
142        lines.push(format!("        {}: {},", field.name.to_pascal_case(), default_str));
143    }
144    lines.push("    }".to_string());
145    lines.push("    for _, opt := range opts {".to_string());
146    lines.push("        opt(c)".to_string());
147    lines.push("    }".to_string());
148    lines.push("    return c".to_string());
149    lines.push("}".to_string());
150
151    lines.join("\n")
152}
153
154/// Generate Java builder pattern for a type with `has_default`.
155/// Returns: Builder inner class with withField methods + build() method
156pub fn gen_java_builder(typ: &TypeDef, package: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
157    let mut lines = Vec::new();
158
159    lines.push(format!(
160        "// DO NOT EDIT - auto-generated by alef\npackage {};\n",
161        package
162    ));
163    lines.push("/// Builder for creating instances of {} with sensible defaults".to_string());
164    lines.push(format!("public class {}Builder {{", typ.name));
165
166    // Fields
167    for field in &typ.fields {
168        let java_type = type_mapper(&field.ty);
169        lines.push(format!("    private {} {};", java_type, field.name.to_lowercase()));
170    }
171    lines.push("".to_string());
172
173    // Constructor
174    lines.push(format!("    public {}Builder() {{", typ.name));
175    for field in &typ.fields {
176        let default_str = default_value_for_field(field, "java");
177        lines.push(format!("        this.{} = {};", field.name.to_lowercase(), default_str));
178    }
179    lines.push("    }".to_string());
180    lines.push("".to_string());
181
182    // withField methods
183    for field in &typ.fields {
184        let java_type = type_mapper(&field.ty);
185        let method_name = format!("with{}", field.name.to_pascal_case());
186        lines.push(format!(
187            "    public {}Builder {}({} value) {{",
188            typ.name, method_name, java_type
189        ));
190        lines.push(format!("        this.{} = value;", field.name.to_lowercase()));
191        lines.push("        return this;".to_string());
192        lines.push("    }".to_string());
193        lines.push("".to_string());
194    }
195
196    // build() method
197    lines.push(format!("    public {} build() {{", typ.name));
198    lines.push(format!("        return new {}(", typ.name));
199    for (i, field) in typ.fields.iter().enumerate() {
200        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
201        lines.push(format!("            this.{}{}", field.name.to_lowercase(), comma));
202    }
203    lines.push("        );".to_string());
204    lines.push("    }".to_string());
205    lines.push("}".to_string());
206
207    lines.join("\n")
208}
209
210/// Generate C# record with init properties for a type with `has_default`.
211pub fn gen_csharp_record(typ: &TypeDef, namespace: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
212    let mut lines = Vec::new();
213
214    lines.push("// This file is auto-generated by alef. DO NOT EDIT.".to_string());
215    lines.push("using System;".to_string());
216    lines.push("".to_string());
217    lines.push(format!("namespace {};\n", namespace));
218
219    lines.push(format!("/// Configuration record: {}", typ.name));
220    lines.push(format!("public record {} {{", typ.name));
221
222    for field in &typ.fields {
223        // Skip tuple struct internals (e.g., _0, _1, etc.)
224        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
225            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
226        {
227            continue;
228        }
229
230        let cs_type = type_mapper(&field.ty);
231        let default_str = default_value_for_field(field, "csharp");
232        lines.push(format!(
233            "    public {} {} {{ get; init; }} = {};",
234            cs_type,
235            field.name.to_pascal_case(),
236            default_str
237        ));
238    }
239
240    lines.push("}".to_string());
241
242    lines.join("\n")
243}
244
245/// Get a language-appropriate default value string for a field.
246/// Uses `typed_default` if available, falls back to `default` string, or type-based zero value.
247pub fn default_value_for_field(field: &FieldDef, language: &str) -> String {
248    // First try typed_default if it exists
249    if let Some(typed_default) = &field.typed_default {
250        return match typed_default {
251            DefaultValue::BoolLiteral(b) => match language {
252                "python" => {
253                    if *b {
254                        "True".to_string()
255                    } else {
256                        "False".to_string()
257                    }
258                }
259                "ruby" => {
260                    if *b {
261                        "true".to_string()
262                    } else {
263                        "false".to_string()
264                    }
265                }
266                "go" => {
267                    if *b {
268                        "true".to_string()
269                    } else {
270                        "false".to_string()
271                    }
272                }
273                "java" => {
274                    if *b {
275                        "true".to_string()
276                    } else {
277                        "false".to_string()
278                    }
279                }
280                "csharp" => {
281                    if *b {
282                        "true".to_string()
283                    } else {
284                        "false".to_string()
285                    }
286                }
287                "php" => {
288                    if *b {
289                        "true".to_string()
290                    } else {
291                        "false".to_string()
292                    }
293                }
294                "r" => {
295                    if *b {
296                        "TRUE".to_string()
297                    } else {
298                        "FALSE".to_string()
299                    }
300                }
301                "rust" => {
302                    if *b {
303                        "true".to_string()
304                    } else {
305                        "false".to_string()
306                    }
307                }
308                _ => {
309                    if *b {
310                        "true".to_string()
311                    } else {
312                        "false".to_string()
313                    }
314                }
315            },
316            DefaultValue::StringLiteral(s) => match language {
317                "rust" => format!("\"{}\".to_string()", s.replace('"', "\\\"")),
318                _ => format!("\"{}\"", s.replace('"', "\\\"")),
319            },
320            DefaultValue::IntLiteral(n) => n.to_string(),
321            DefaultValue::FloatLiteral(f) => {
322                let s = f.to_string();
323                if !s.contains('.') { format!("{}.0", s) } else { s }
324            }
325            DefaultValue::EnumVariant(v) => match language {
326                "python" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
327                "ruby" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
328                "go" => format!("{}{}", field.ty.type_name(), v.to_pascal_case()),
329                "java" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
330                "csharp" => format!("{}.{}", field.ty.type_name(), v.to_pascal_case()),
331                "php" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
332                "r" => format!("{}${}", field.ty.type_name(), v.to_pascal_case()),
333                "rust" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
334                _ => v.clone(),
335            },
336            DefaultValue::Empty => {
337                // Empty means "type's default" — check field type to pick the right zero value
338                match &field.ty {
339                    TypeRef::Vec(_) => match language {
340                        "python" | "ruby" | "csharp" => "[]".to_string(),
341                        "go" => "nil".to_string(),
342                        "java" => "List.of()".to_string(),
343                        "php" => "[]".to_string(),
344                        "r" => "c()".to_string(),
345                        "rust" => "vec![]".to_string(),
346                        _ => "null".to_string(),
347                    },
348                    TypeRef::Map(_, _) => match language {
349                        "python" => "{}".to_string(),
350                        "go" => "nil".to_string(),
351                        "java" => "Map.of()".to_string(),
352                        "rust" => "Default::default()".to_string(),
353                        _ => "null".to_string(),
354                    },
355                    TypeRef::Primitive(p) => match p {
356                        PrimitiveType::Bool => match language {
357                            "python" => "False".to_string(),
358                            "ruby" => "false".to_string(),
359                            _ => "false".to_string(),
360                        },
361                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
362                        _ => "0".to_string(),
363                    },
364                    TypeRef::String | TypeRef::Char | TypeRef::Path => match language {
365                        "rust" => "String::new()".to_string(),
366                        _ => "\"\"".to_string(),
367                    },
368                    TypeRef::Json => match language {
369                        "python" | "ruby" => "{}".to_string(),
370                        "go" => "map[string]interface{}{}".to_string(),
371                        "java" => "new com.fasterxml.jackson.databind.node.ObjectNode(null)".to_string(),
372                        "csharp" => "JObject.Parse(\"{}\")".to_string(),
373                        "php" => "[]".to_string(),
374                        "r" => "list()".to_string(),
375                        "rust" => "serde_json::json!({})".to_string(),
376                        _ => "{}".to_string(),
377                    },
378                    TypeRef::Duration => "0".to_string(),
379                    TypeRef::Bytes => match language {
380                        "python" => "b\"\"".to_string(),
381                        "go" => "[]byte{}".to_string(),
382                        "rust" => "vec![]".to_string(),
383                        _ => "\"\"".to_string(),
384                    },
385                    _ => match language {
386                        "python" => "None".to_string(),
387                        "ruby" => "nil".to_string(),
388                        "go" => "nil".to_string(),
389                        "rust" => "Default::default()".to_string(),
390                        _ => "null".to_string(),
391                    },
392                }
393            }
394            DefaultValue::None => match language {
395                "python" => "None".to_string(),
396                "ruby" => "nil".to_string(),
397                "go" => "nil".to_string(),
398                "java" => "null".to_string(),
399                "csharp" => "null".to_string(),
400                "php" => "null".to_string(),
401                "r" => "NULL".to_string(),
402                "rust" => "None".to_string(),
403                _ => "null".to_string(),
404            },
405        };
406    }
407
408    // Fall back to string default if it exists
409    if let Some(default_str) = &field.default {
410        return default_str.clone();
411    }
412
413    // Final fallback: type-based zero value
414    match &field.ty {
415        TypeRef::Primitive(p) => match p {
416            alef_core::ir::PrimitiveType::Bool => match language {
417                "python" => "False".to_string(),
418                "ruby" => "false".to_string(),
419                "csharp" => "false".to_string(),
420                "java" => "false".to_string(),
421                "php" => "false".to_string(),
422                "r" => "FALSE".to_string(),
423                _ => "false".to_string(),
424            },
425            alef_core::ir::PrimitiveType::U8
426            | alef_core::ir::PrimitiveType::U16
427            | alef_core::ir::PrimitiveType::U32
428            | alef_core::ir::PrimitiveType::U64
429            | alef_core::ir::PrimitiveType::I8
430            | alef_core::ir::PrimitiveType::I16
431            | alef_core::ir::PrimitiveType::I32
432            | alef_core::ir::PrimitiveType::I64
433            | alef_core::ir::PrimitiveType::Usize
434            | alef_core::ir::PrimitiveType::Isize => "0".to_string(),
435            alef_core::ir::PrimitiveType::F32 | alef_core::ir::PrimitiveType::F64 => "0.0".to_string(),
436        },
437        TypeRef::String | TypeRef::Char => match language {
438            "python" => "\"\"".to_string(),
439            "ruby" => "\"\"".to_string(),
440            "go" => "\"\"".to_string(),
441            "java" => "\"\"".to_string(),
442            "csharp" => "\"\"".to_string(),
443            "php" => "\"\"".to_string(),
444            "r" => "\"\"".to_string(),
445            "rust" => "String::new()".to_string(),
446            _ => "\"\"".to_string(),
447        },
448        TypeRef::Bytes => match language {
449            "python" => "b\"\"".to_string(),
450            "ruby" => "\"\"".to_string(),
451            "go" => "[]byte{}".to_string(),
452            "java" => "new byte[]{}".to_string(),
453            "csharp" => "new byte[]{}".to_string(),
454            "php" => "\"\"".to_string(),
455            "r" => "raw()".to_string(),
456            "rust" => "vec![]".to_string(),
457            _ => "[]".to_string(),
458        },
459        TypeRef::Optional(_) => match language {
460            "python" => "None".to_string(),
461            "ruby" => "nil".to_string(),
462            "go" => "nil".to_string(),
463            "java" => "null".to_string(),
464            "csharp" => "null".to_string(),
465            "php" => "null".to_string(),
466            "r" => "NULL".to_string(),
467            "rust" => "None".to_string(),
468            _ => "null".to_string(),
469        },
470        TypeRef::Vec(_) => match language {
471            "python" => "[]".to_string(),
472            "ruby" => "[]".to_string(),
473            "go" => "[]interface{}{}".to_string(),
474            "java" => "new java.util.ArrayList<>()".to_string(),
475            "csharp" => "[]".to_string(),
476            "php" => "[]".to_string(),
477            "r" => "c()".to_string(),
478            "rust" => "vec![]".to_string(),
479            _ => "[]".to_string(),
480        },
481        TypeRef::Map(_, _) => match language {
482            "python" => "{}".to_string(),
483            "ruby" => "{}".to_string(),
484            "go" => "make(map[string]interface{})".to_string(),
485            "java" => "new java.util.HashMap<>()".to_string(),
486            "csharp" => "new Dictionary<string, object>()".to_string(),
487            "php" => "[]".to_string(),
488            "r" => "list()".to_string(),
489            "rust" => "std::collections::HashMap::new()".to_string(),
490            _ => "{}".to_string(),
491        },
492        TypeRef::Json => match language {
493            "python" => "{}".to_string(),
494            "ruby" => "{}".to_string(),
495            "go" => "make(map[string]interface{})".to_string(),
496            "java" => "new com.fasterxml.jackson.databind.JsonNode()".to_string(),
497            "csharp" => "JObject.Parse(\"{}\")".to_string(),
498            "php" => "[]".to_string(),
499            "r" => "list()".to_string(),
500            "rust" => "serde_json::json!({})".to_string(),
501            _ => "{}".to_string(),
502        },
503        TypeRef::Named(name) => match language {
504            "rust" => format!("{name}::default()"),
505            "python" => "None".to_string(),
506            "ruby" => "nil".to_string(),
507            "go" => "nil".to_string(),
508            "java" => "null".to_string(),
509            "csharp" => "null".to_string(),
510            "php" => "null".to_string(),
511            "r" => "NULL".to_string(),
512            _ => "null".to_string(),
513        },
514        _ => match language {
515            "python" => "None".to_string(),
516            "ruby" => "nil".to_string(),
517            "go" => "nil".to_string(),
518            "java" => "null".to_string(),
519            "csharp" => "null".to_string(),
520            "php" => "null".to_string(),
521            "r" => "NULL".to_string(),
522            "rust" => "Default::default()".to_string(),
523            _ => "null".to_string(),
524        },
525    }
526}
527
528// Helper trait extension for TypeRef to get type name
529trait TypeRefExt {
530    fn type_name(&self) -> String;
531}
532
533impl TypeRefExt for TypeRef {
534    fn type_name(&self) -> String {
535        match self {
536            TypeRef::Named(n) => n.clone(),
537            TypeRef::Primitive(p) => format!("{:?}", p),
538            TypeRef::String | TypeRef::Char => "String".to_string(),
539            TypeRef::Bytes => "Bytes".to_string(),
540            TypeRef::Optional(inner) => format!("Option<{}>", inner.type_name()),
541            TypeRef::Vec(inner) => format!("Vec<{}>", inner.type_name()),
542            TypeRef::Map(k, v) => format!("Map<{}, {}>", k.type_name(), v.type_name()),
543            TypeRef::Path => "Path".to_string(),
544            TypeRef::Unit => "()".to_string(),
545            TypeRef::Json => "Json".to_string(),
546            TypeRef::Duration => "Duration".to_string(),
547        }
548    }
549}
550
551/// The maximum arity supported by Magnus `function!` macro.
552const MAGNUS_MAX_ARITY: usize = 15;
553
554/// Generate a Magnus (Ruby) kwargs constructor for a type with `has_default`.
555///
556/// For types with <=15 fields, generates a positional `Option<T>` parameter constructor.
557/// For types with >15 fields (exceeding Magnus arity limit), generates a hash-based constructor
558/// using `RHash` that extracts fields by name, applying defaults for missing keys.
559pub fn gen_magnus_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
560    if typ.fields.len() > MAGNUS_MAX_ARITY {
561        gen_magnus_hash_constructor(typ, type_mapper)
562    } else {
563        gen_magnus_positional_constructor(typ, type_mapper)
564    }
565}
566
567/// Wrap a type string for use as a type-path prefix in Rust.
568///
569/// Types containing `<` (generics like `Vec<String>`, `Option<T>`) cannot be used as
570/// `Vec<String>::try_convert(v)` — that's a parse error. They must use the UFCS form
571/// `<Vec<String>>::try_convert(v)` instead. Simple names like `String`, `bool` can use
572/// `String::try_convert(v)` directly.
573fn as_type_path_prefix(type_str: &str) -> String {
574    if type_str.contains('<') {
575        format!("<{type_str}>")
576    } else {
577        type_str.to_string()
578    }
579}
580
581/// Generate a hash-based Magnus constructor for types with many fields.
582/// Accepts `(kwargs: RHash)` and extracts each field by symbol name, applying defaults.
583fn gen_magnus_hash_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
584    use std::fmt::Write;
585    let mut out = String::with_capacity(1024);
586
587    writeln!(out, "fn new(kwargs: magnus::RHash) -> Result<Self, magnus::Error> {{").ok();
588    writeln!(out, "    let ruby = unsafe {{ magnus::Ruby::get_unchecked() }};").ok();
589    writeln!(out, "    Ok(Self {{").ok();
590
591    for field in &typ.fields {
592        let is_optional = field_is_optional_in_rust(field);
593        // Use inner type for try_convert, since the hash value is the inner T, not Option<T>.
594        let inner_type = type_mapper(&field.ty);
595        let type_prefix = as_type_path_prefix(&inner_type);
596        if is_optional {
597            // Field is Option<T>: extract from hash, wrap in Some, default to None
598            writeln!(
599                out,
600                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()),",
601                name = field.name,
602                type_prefix = type_prefix,
603            ).ok();
604        } else if use_unwrap_or_default(field) {
605            writeln!(
606                out,
607                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()).unwrap_or_default(),",
608                name = field.name,
609                type_prefix = type_prefix,
610            ).ok();
611        } else {
612            let default_str = default_value_for_field(field, "rust");
613            writeln!(
614                out,
615                "        {name}: kwargs.get(ruby.to_symbol(\"{name}\")).and_then(|v| {type_prefix}::try_convert(v).ok()).unwrap_or({default}),",
616                name = field.name,
617                type_prefix = type_prefix,
618                default = default_str,
619            ).ok();
620        }
621    }
622
623    writeln!(out, "    }})").ok();
624    writeln!(out, "}}").ok();
625
626    out
627}
628
629/// Returns true if the generated Rust field type is already `Option<T>`.
630/// This covers both:
631/// - Fields with `optional: true` (the Rust field type becomes `Option<inner_type>`)
632/// - Fields whose `TypeRef` is explicitly `Optional(_)` (rare, for nested Option types)
633fn field_is_optional_in_rust(field: &FieldDef) -> bool {
634    field.optional || matches!(&field.ty, TypeRef::Optional(_))
635}
636
637/// Generate a positional Magnus constructor for types with <=15 fields.
638/// Uses `Option<T>` parameters and applies defaults in the body.
639fn gen_magnus_positional_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
640    use std::fmt::Write;
641    let mut out = String::with_capacity(512);
642
643    writeln!(out, "fn new(").ok();
644
645    // All params are Option<T> so Ruby users can pass nil for any field.
646    // If the Rust field type is already Option<T> (via optional:true or TypeRef::Optional),
647    // use that type directly (avoids Option<Option<T>>).
648    for (i, field) in typ.fields.iter().enumerate() {
649        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
650        let is_optional = field_is_optional_in_rust(field);
651        if is_optional {
652            // field.ty is the inner type; the mapper maps inner type, we wrap in Option<>
653            // BUT the type_mapper call site already wraps when field.optional==true.
654            // Here we call type_mapper on the field's inner type directly to get the param type.
655            let inner_type = type_mapper(&field.ty);
656            writeln!(out, "    {}: Option<{}>{}", field.name, inner_type, comma).ok();
657        } else {
658            let field_type = type_mapper(&field.ty);
659            writeln!(out, "    {}: Option<{}>{}", field.name, field_type, comma).ok();
660        }
661    }
662
663    writeln!(out, ") -> Self {{").ok();
664    writeln!(out, "    Self {{").ok();
665
666    for field in &typ.fields {
667        let is_optional = field_is_optional_in_rust(field);
668        if is_optional {
669            // The Rust field is Option<T>; param is Option<T>; assign directly.
670            writeln!(out, "        {},", field.name).ok();
671        } else if use_unwrap_or_default(field) {
672            writeln!(out, "        {}: {}.unwrap_or_default(),", field.name, field.name).ok();
673        } else {
674            let default_str = default_value_for_field(field, "rust");
675            writeln!(
676                out,
677                "        {}: {}.unwrap_or({}),",
678                field.name, field.name, default_str
679            )
680            .ok();
681        }
682    }
683
684    writeln!(out, "    }}").ok();
685    writeln!(out, "}}").ok();
686
687    out
688}
689
690/// Generate a PHP kwargs constructor for a type with `has_default`.
691/// All fields become `Option<T>` parameters so PHP users can omit any field.
692/// Assignments wrap non-Optional fields in `Some()` and apply defaults.
693pub fn gen_php_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
694    use std::fmt::Write;
695    let mut out = String::with_capacity(512);
696
697    writeln!(out, "pub fn __construct(").ok();
698
699    // All params are Option<MappedType> — PHP users can omit any field
700    for (i, field) in typ.fields.iter().enumerate() {
701        let mapped = type_mapper(&field.ty);
702        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
703        writeln!(out, "    {}: Option<{}>{}", field.name, mapped, comma).ok();
704    }
705
706    writeln!(out, ") -> Self {{").ok();
707    writeln!(out, "    Self {{").ok();
708
709    for field in &typ.fields {
710        let is_optional_field = field.optional || matches!(&field.ty, TypeRef::Optional(_));
711        if is_optional_field {
712            // Struct field is Option<T>, param is Option<T> — pass through directly
713            writeln!(out, "        {},", field.name).ok();
714        } else if use_unwrap_or_default(field) {
715            // Struct field is T, param is Option<T> — unwrap with type's default
716            writeln!(out, "        {}: {}.unwrap_or_default(),", field.name, field.name).ok();
717        } else {
718            // Struct field is T, param is Option<T> — unwrap with explicit default
719            let default_str = default_value_for_field(field, "rust");
720            writeln!(
721                out,
722                "        {}: {}.unwrap_or({}),",
723                field.name, field.name, default_str
724            )
725            .ok();
726        }
727    }
728
729    writeln!(out, "    }}").ok();
730    writeln!(out, "}}").ok();
731
732    out
733}
734
735/// Generate a Rustler (Elixir) kwargs constructor for a type with `has_default`.
736/// Accepts keyword list or map, applies defaults for missing fields.
737pub fn gen_rustler_kwargs_constructor(typ: &TypeDef, _type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
738    use std::fmt::Write;
739    let mut out = String::with_capacity(512);
740
741    // NifStruct already handles keyword list conversion, but we generate
742    // an explicit constructor wrapper that applies defaults.
743    writeln!(
744        out,
745        "pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {{"
746    )
747    .ok();
748    writeln!(out, "    Self {{").ok();
749
750    // Field assignments with defaults from opts.
751    // Optional fields (Option<T>) need special handling: decode the inner type
752    // directly so we get Option<T> from and_then, with no unwrap_or needed.
753    for field in &typ.fields {
754        if field.optional {
755            // Field type is Option<T>. Decode inner T from the Term, yielding Option<T>.
756            writeln!(
757                out,
758                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()),",
759                field.name, field.name
760            )
761            .ok();
762        } else if use_unwrap_or_default(field) {
763            writeln!(
764                out,
765                "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
766                field.name, field.name
767            )
768            .ok();
769        } else {
770            let default_str = default_value_for_field(field, "rust");
771            // Check if the default value looks like an enum variant (e.g., "OutputFormat::Plain")
772            // which wouldn't work for String types. If so, use unwrap_or_default() instead.
773            let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
774
775            if is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char) {
776                // Use default for String types with enum-like defaults
777                writeln!(
778                    out,
779                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
780                    field.name, field.name
781                )
782                .ok();
783            } else if matches!(&field.ty, TypeRef::Named(_)) {
784                // For other Named types, use unwrap_or_default() since the binding type may differ
785                // from the core type (e.g., excluded enums become String).
786                writeln!(
787                    out,
788                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
789                    field.name, field.name
790                )
791                .ok();
792            } else {
793                writeln!(
794                    out,
795                    "        {}: opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
796                    field.name, field.name, default_str
797                )
798                .ok();
799            }
800        }
801    }
802
803    writeln!(out, "    }}").ok();
804    writeln!(out, "}}").ok();
805
806    out
807}
808
809/// Generate an extendr (R) kwargs constructor for a type with `has_default`.
810/// Generates an R-callable function accepting named parameters with defaults.
811pub fn gen_extendr_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
812    use std::fmt::Write;
813    let mut out = String::with_capacity(512);
814
815    writeln!(out, "#[extendr]").ok();
816    writeln!(out, "pub fn new_{}(", typ.name.to_lowercase()).ok();
817
818    // Add all fields as named parameters with defaults
819    for (i, field) in typ.fields.iter().enumerate() {
820        let field_type = type_mapper(&field.ty);
821        let default_str = default_value_for_field(field, "r");
822        let comma = if i < typ.fields.len() - 1 { "," } else { "" };
823        writeln!(out, "    {}: {} = {}{}", field.name, field_type, default_str, comma).ok();
824    }
825
826    writeln!(out, ") -> {} {{", typ.name).ok();
827    writeln!(out, "    {} {{", typ.name).ok();
828
829    // Field assignments
830    for field in &typ.fields {
831        writeln!(out, "        {},", field.name).ok();
832    }
833
834    writeln!(out, "    }}").ok();
835    writeln!(out, "}}").ok();
836
837    out
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use alef_core::ir::{CoreWrapper, FieldDef, PrimitiveType, TypeRef};
844
845    fn make_test_type() -> TypeDef {
846        TypeDef {
847            name: "Config".to_string(),
848            rust_path: "my_crate::Config".to_string(),
849            fields: vec![
850                FieldDef {
851                    name: "timeout".to_string(),
852                    ty: TypeRef::Primitive(PrimitiveType::U64),
853                    optional: false,
854                    default: Some("30".to_string()),
855                    doc: "Timeout in seconds".to_string(),
856                    sanitized: false,
857                    is_boxed: false,
858                    type_rust_path: None,
859                    cfg: None,
860                    typed_default: Some(DefaultValue::IntLiteral(30)),
861                    core_wrapper: CoreWrapper::None,
862                    vec_inner_core_wrapper: CoreWrapper::None,
863                    newtype_wrapper: None,
864                },
865                FieldDef {
866                    name: "enabled".to_string(),
867                    ty: TypeRef::Primitive(PrimitiveType::Bool),
868                    optional: false,
869                    default: None,
870                    doc: "Enable feature".to_string(),
871                    sanitized: false,
872                    is_boxed: false,
873                    type_rust_path: None,
874                    cfg: None,
875                    typed_default: Some(DefaultValue::BoolLiteral(true)),
876                    core_wrapper: CoreWrapper::None,
877                    vec_inner_core_wrapper: CoreWrapper::None,
878                    newtype_wrapper: None,
879                },
880                FieldDef {
881                    name: "name".to_string(),
882                    ty: TypeRef::String,
883                    optional: false,
884                    default: None,
885                    doc: "Config name".to_string(),
886                    sanitized: false,
887                    is_boxed: false,
888                    type_rust_path: None,
889                    cfg: None,
890                    typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
891                    core_wrapper: CoreWrapper::None,
892                    vec_inner_core_wrapper: CoreWrapper::None,
893                    newtype_wrapper: None,
894                },
895            ],
896            methods: vec![],
897            is_opaque: false,
898            is_clone: true,
899            doc: "Configuration type".to_string(),
900            cfg: None,
901            is_trait: false,
902            has_default: true,
903            has_stripped_cfg_fields: false,
904            is_return_type: false,
905            serde_rename_all: None,
906            has_serde: false,
907        }
908    }
909
910    #[test]
911    fn test_default_value_bool_true_python() {
912        let field = FieldDef {
913            name: "enabled".to_string(),
914            ty: TypeRef::Primitive(PrimitiveType::Bool),
915            optional: false,
916            default: None,
917            doc: String::new(),
918            sanitized: false,
919            is_boxed: false,
920            type_rust_path: None,
921            cfg: None,
922            typed_default: Some(DefaultValue::BoolLiteral(true)),
923            core_wrapper: CoreWrapper::None,
924            vec_inner_core_wrapper: CoreWrapper::None,
925            newtype_wrapper: None,
926        };
927        assert_eq!(default_value_for_field(&field, "python"), "True");
928    }
929
930    #[test]
931    fn test_default_value_bool_false_go() {
932        let field = FieldDef {
933            name: "enabled".to_string(),
934            ty: TypeRef::Primitive(PrimitiveType::Bool),
935            optional: false,
936            default: None,
937            doc: String::new(),
938            sanitized: false,
939            is_boxed: false,
940            type_rust_path: None,
941            cfg: None,
942            typed_default: Some(DefaultValue::BoolLiteral(false)),
943            core_wrapper: CoreWrapper::None,
944            vec_inner_core_wrapper: CoreWrapper::None,
945            newtype_wrapper: None,
946        };
947        assert_eq!(default_value_for_field(&field, "go"), "false");
948    }
949
950    #[test]
951    fn test_default_value_string_literal() {
952        let field = FieldDef {
953            name: "name".to_string(),
954            ty: TypeRef::String,
955            optional: false,
956            default: None,
957            doc: String::new(),
958            sanitized: false,
959            is_boxed: false,
960            type_rust_path: None,
961            cfg: None,
962            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
963            core_wrapper: CoreWrapper::None,
964            vec_inner_core_wrapper: CoreWrapper::None,
965            newtype_wrapper: None,
966        };
967        assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
968        assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
969    }
970
971    #[test]
972    fn test_default_value_int_literal() {
973        let field = FieldDef {
974            name: "timeout".to_string(),
975            ty: TypeRef::Primitive(PrimitiveType::U64),
976            optional: false,
977            default: None,
978            doc: String::new(),
979            sanitized: false,
980            is_boxed: false,
981            type_rust_path: None,
982            cfg: None,
983            typed_default: Some(DefaultValue::IntLiteral(42)),
984            core_wrapper: CoreWrapper::None,
985            vec_inner_core_wrapper: CoreWrapper::None,
986            newtype_wrapper: None,
987        };
988        let result = default_value_for_field(&field, "python");
989        assert_eq!(result, "42");
990    }
991
992    #[test]
993    fn test_default_value_none() {
994        let field = FieldDef {
995            name: "maybe".to_string(),
996            ty: TypeRef::Optional(Box::new(TypeRef::String)),
997            optional: true,
998            default: None,
999            doc: String::new(),
1000            sanitized: false,
1001            is_boxed: false,
1002            type_rust_path: None,
1003            cfg: None,
1004            typed_default: Some(DefaultValue::None),
1005            core_wrapper: CoreWrapper::None,
1006            vec_inner_core_wrapper: CoreWrapper::None,
1007            newtype_wrapper: None,
1008        };
1009        assert_eq!(default_value_for_field(&field, "python"), "None");
1010        assert_eq!(default_value_for_field(&field, "go"), "nil");
1011        assert_eq!(default_value_for_field(&field, "java"), "null");
1012        assert_eq!(default_value_for_field(&field, "csharp"), "null");
1013    }
1014
1015    #[test]
1016    fn test_default_value_fallback_string() {
1017        let field = FieldDef {
1018            name: "name".to_string(),
1019            ty: TypeRef::String,
1020            optional: false,
1021            default: Some("\"custom\"".to_string()),
1022            doc: String::new(),
1023            sanitized: false,
1024            is_boxed: false,
1025            type_rust_path: None,
1026            cfg: None,
1027            typed_default: None,
1028            core_wrapper: CoreWrapper::None,
1029            vec_inner_core_wrapper: CoreWrapper::None,
1030            newtype_wrapper: None,
1031        };
1032        assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1033    }
1034
1035    #[test]
1036    fn test_gen_pyo3_kwargs_constructor() {
1037        let typ = make_test_type();
1038        let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1039            TypeRef::Primitive(p) => format!("{:?}", p),
1040            TypeRef::String | TypeRef::Char => "str".to_string(),
1041            _ => "Any".to_string(),
1042        });
1043
1044        assert!(output.contains("#[new]"));
1045        assert!(output.contains("#[pyo3(signature = ("));
1046        assert!(output.contains("timeout=30"));
1047        assert!(output.contains("enabled=True"));
1048        assert!(output.contains("name=\"default\""));
1049        assert!(output.contains("fn new("));
1050    }
1051
1052    #[test]
1053    fn test_gen_napi_defaults_constructor() {
1054        let typ = make_test_type();
1055        let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1056            TypeRef::Primitive(p) => format!("{:?}", p),
1057            TypeRef::String | TypeRef::Char => "String".to_string(),
1058            _ => "Value".to_string(),
1059        });
1060
1061        assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1062        assert!(output.contains("timeout"));
1063        assert!(output.contains("enabled"));
1064        assert!(output.contains("name"));
1065    }
1066
1067    #[test]
1068    fn test_gen_go_functional_options() {
1069        let typ = make_test_type();
1070        let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1071            TypeRef::Primitive(p) => match p {
1072                PrimitiveType::U64 => "uint64".to_string(),
1073                PrimitiveType::Bool => "bool".to_string(),
1074                _ => "interface{}".to_string(),
1075            },
1076            TypeRef::String | TypeRef::Char => "string".to_string(),
1077            _ => "interface{}".to_string(),
1078        });
1079
1080        assert!(output.contains("type Config struct {"));
1081        assert!(output.contains("type ConfigOption func(*Config)"));
1082        assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1083        assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1084        assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1085        assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1086    }
1087
1088    #[test]
1089    fn test_gen_java_builder() {
1090        let typ = make_test_type();
1091        let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1092            TypeRef::Primitive(p) => match p {
1093                PrimitiveType::U64 => "long".to_string(),
1094                PrimitiveType::Bool => "boolean".to_string(),
1095                _ => "int".to_string(),
1096            },
1097            TypeRef::String | TypeRef::Char => "String".to_string(),
1098            _ => "Object".to_string(),
1099        });
1100
1101        assert!(output.contains("package dev.test;"));
1102        assert!(output.contains("public class ConfigBuilder"));
1103        assert!(output.contains("withTimeout"));
1104        assert!(output.contains("withEnabled"));
1105        assert!(output.contains("withName"));
1106        assert!(output.contains("public Config build()"));
1107    }
1108
1109    #[test]
1110    fn test_gen_csharp_record() {
1111        let typ = make_test_type();
1112        let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1113            TypeRef::Primitive(p) => match p {
1114                PrimitiveType::U64 => "ulong".to_string(),
1115                PrimitiveType::Bool => "bool".to_string(),
1116                _ => "int".to_string(),
1117            },
1118            TypeRef::String | TypeRef::Char => "string".to_string(),
1119            _ => "object".to_string(),
1120        });
1121
1122        assert!(output.contains("namespace MyNamespace;"));
1123        assert!(output.contains("public record Config"));
1124        assert!(output.contains("public ulong Timeout"));
1125        assert!(output.contains("public bool Enabled"));
1126        assert!(output.contains("public string Name"));
1127        assert!(output.contains("init;"));
1128    }
1129
1130    #[test]
1131    fn test_default_value_float_literal() {
1132        let field = FieldDef {
1133            name: "ratio".to_string(),
1134            ty: TypeRef::Primitive(PrimitiveType::F64),
1135            optional: false,
1136            default: None,
1137            doc: String::new(),
1138            sanitized: false,
1139            is_boxed: false,
1140            type_rust_path: None,
1141            cfg: None,
1142            typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1143            core_wrapper: CoreWrapper::None,
1144            vec_inner_core_wrapper: CoreWrapper::None,
1145            newtype_wrapper: None,
1146        };
1147        let result = default_value_for_field(&field, "python");
1148        assert!(result.contains("1.5"));
1149    }
1150
1151    #[test]
1152    fn test_default_value_no_typed_no_default() {
1153        let field = FieldDef {
1154            name: "count".to_string(),
1155            ty: TypeRef::Primitive(PrimitiveType::U32),
1156            optional: false,
1157            default: None,
1158            doc: String::new(),
1159            sanitized: false,
1160            is_boxed: false,
1161            type_rust_path: None,
1162            cfg: None,
1163            typed_default: None,
1164            core_wrapper: CoreWrapper::None,
1165            vec_inner_core_wrapper: CoreWrapper::None,
1166            newtype_wrapper: None,
1167        };
1168        // Should fall back to type-based zero value
1169        assert_eq!(default_value_for_field(&field, "python"), "0");
1170        assert_eq!(default_value_for_field(&field, "go"), "0");
1171    }
1172}