1use alef_core::ir::{DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
2use heck::{ToPascalCase, ToShoutySnakeCase, ToSnakeCase};
3
4fn 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
11fn 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 field.default.is_none() && !matches!(&field.ty, TypeRef::Named(_))
23}
24
25pub 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 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 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 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
60pub 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 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
85pub fn gen_go_functional_options(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
88 let mut lines = Vec::new();
89
90 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 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 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 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
154pub 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 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 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 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 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
210pub 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 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
245pub fn default_value_for_field(field: &FieldDef, language: &str) -> String {
248 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) => {
326 if matches!(field.ty, TypeRef::String) {
330 let snake = v.to_snake_case();
331 return match language {
332 "rust" => format!("\"{}\".to_string()", snake),
333 _ => format!("\"{}\"", snake),
334 };
335 }
336 match language {
337 "python" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
338 "ruby" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
339 "go" => format!("{}{}", field.ty.type_name(), v.to_pascal_case()),
340 "java" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
341 "csharp" => format!("{}.{}", field.ty.type_name(), v.to_pascal_case()),
342 "php" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
343 "r" => format!("{}${}", field.ty.type_name(), v.to_pascal_case()),
344 "rust" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
345 _ => v.clone(),
346 }
347 }
348 DefaultValue::Empty => {
349 match &field.ty {
351 TypeRef::Vec(_) => match language {
352 "python" | "ruby" | "csharp" => "[]".to_string(),
353 "go" => "nil".to_string(),
354 "java" => "List.of()".to_string(),
355 "php" => "[]".to_string(),
356 "r" => "c()".to_string(),
357 "rust" => "vec![]".to_string(),
358 _ => "null".to_string(),
359 },
360 TypeRef::Map(_, _) => match language {
361 "python" => "{}".to_string(),
362 "go" => "nil".to_string(),
363 "java" => "Map.of()".to_string(),
364 "rust" => "Default::default()".to_string(),
365 _ => "null".to_string(),
366 },
367 TypeRef::Primitive(p) => match p {
368 PrimitiveType::Bool => match language {
369 "python" => "False".to_string(),
370 "ruby" => "false".to_string(),
371 _ => "false".to_string(),
372 },
373 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
374 _ => "0".to_string(),
375 },
376 TypeRef::String | TypeRef::Char | TypeRef::Path => match language {
377 "rust" => "String::new()".to_string(),
378 _ => "\"\"".to_string(),
379 },
380 TypeRef::Json => match language {
381 "python" | "ruby" => "{}".to_string(),
382 "go" => "json.RawMessage(nil)".to_string(),
383 "java" => "new com.fasterxml.jackson.databind.node.ObjectNode(null)".to_string(),
384 "csharp" => "JObject.Parse(\"{}\")".to_string(),
385 "php" => "[]".to_string(),
386 "r" => "list()".to_string(),
387 "rust" => "serde_json::json!({})".to_string(),
388 _ => "{}".to_string(),
389 },
390 TypeRef::Duration => "0".to_string(),
391 TypeRef::Bytes => match language {
392 "python" => "b\"\"".to_string(),
393 "go" => "[]byte{}".to_string(),
394 "rust" => "vec![]".to_string(),
395 _ => "\"\"".to_string(),
396 },
397 _ => match language {
398 "python" => "None".to_string(),
399 "ruby" => "nil".to_string(),
400 "go" => "nil".to_string(),
401 "rust" => "Default::default()".to_string(),
402 _ => "null".to_string(),
403 },
404 }
405 }
406 DefaultValue::None => match language {
407 "python" => "None".to_string(),
408 "ruby" => "nil".to_string(),
409 "go" => "nil".to_string(),
410 "java" => "null".to_string(),
411 "csharp" => "null".to_string(),
412 "php" => "null".to_string(),
413 "r" => "NULL".to_string(),
414 "rust" => "None".to_string(),
415 _ => "null".to_string(),
416 },
417 };
418 }
419
420 if let Some(default_str) = &field.default {
422 return default_str.clone();
423 }
424
425 match &field.ty {
427 TypeRef::Primitive(p) => match p {
428 alef_core::ir::PrimitiveType::Bool => match language {
429 "python" => "False".to_string(),
430 "ruby" => "false".to_string(),
431 "csharp" => "false".to_string(),
432 "java" => "false".to_string(),
433 "php" => "false".to_string(),
434 "r" => "FALSE".to_string(),
435 _ => "false".to_string(),
436 },
437 alef_core::ir::PrimitiveType::U8
438 | alef_core::ir::PrimitiveType::U16
439 | alef_core::ir::PrimitiveType::U32
440 | alef_core::ir::PrimitiveType::U64
441 | alef_core::ir::PrimitiveType::I8
442 | alef_core::ir::PrimitiveType::I16
443 | alef_core::ir::PrimitiveType::I32
444 | alef_core::ir::PrimitiveType::I64
445 | alef_core::ir::PrimitiveType::Usize
446 | alef_core::ir::PrimitiveType::Isize => "0".to_string(),
447 alef_core::ir::PrimitiveType::F32 | alef_core::ir::PrimitiveType::F64 => "0.0".to_string(),
448 },
449 TypeRef::String | TypeRef::Char => match language {
450 "python" => "\"\"".to_string(),
451 "ruby" => "\"\"".to_string(),
452 "go" => "\"\"".to_string(),
453 "java" => "\"\"".to_string(),
454 "csharp" => "\"\"".to_string(),
455 "php" => "\"\"".to_string(),
456 "r" => "\"\"".to_string(),
457 "rust" => "String::new()".to_string(),
458 _ => "\"\"".to_string(),
459 },
460 TypeRef::Bytes => match language {
461 "python" => "b\"\"".to_string(),
462 "ruby" => "\"\"".to_string(),
463 "go" => "[]byte{}".to_string(),
464 "java" => "new byte[]{}".to_string(),
465 "csharp" => "new byte[]{}".to_string(),
466 "php" => "\"\"".to_string(),
467 "r" => "raw()".to_string(),
468 "rust" => "vec![]".to_string(),
469 _ => "[]".to_string(),
470 },
471 TypeRef::Optional(_) => match language {
472 "python" => "None".to_string(),
473 "ruby" => "nil".to_string(),
474 "go" => "nil".to_string(),
475 "java" => "null".to_string(),
476 "csharp" => "null".to_string(),
477 "php" => "null".to_string(),
478 "r" => "NULL".to_string(),
479 "rust" => "None".to_string(),
480 _ => "null".to_string(),
481 },
482 TypeRef::Vec(_) => match language {
483 "python" => "[]".to_string(),
484 "ruby" => "[]".to_string(),
485 "go" => "[]interface{}{}".to_string(),
486 "java" => "new java.util.ArrayList<>()".to_string(),
487 "csharp" => "[]".to_string(),
488 "php" => "[]".to_string(),
489 "r" => "c()".to_string(),
490 "rust" => "vec![]".to_string(),
491 _ => "[]".to_string(),
492 },
493 TypeRef::Map(_, _) => match language {
494 "python" => "{}".to_string(),
495 "ruby" => "{}".to_string(),
496 "go" => "make(map[string]interface{})".to_string(),
497 "java" => "new java.util.HashMap<>()".to_string(),
498 "csharp" => "new Dictionary<string, object>()".to_string(),
499 "php" => "[]".to_string(),
500 "r" => "list()".to_string(),
501 "rust" => "std::collections::HashMap::new()".to_string(),
502 _ => "{}".to_string(),
503 },
504 TypeRef::Json => match language {
505 "python" => "{}".to_string(),
506 "ruby" => "{}".to_string(),
507 "go" => "json.RawMessage(nil)".to_string(),
508 "java" => "new com.fasterxml.jackson.databind.JsonNode()".to_string(),
509 "csharp" => "JObject.Parse(\"{}\")".to_string(),
510 "php" => "[]".to_string(),
511 "r" => "list()".to_string(),
512 "rust" => "serde_json::json!({})".to_string(),
513 _ => "{}".to_string(),
514 },
515 TypeRef::Named(name) => match language {
516 "rust" => format!("{name}::default()"),
517 "python" => "None".to_string(),
518 "ruby" => "nil".to_string(),
519 "go" => "nil".to_string(),
520 "java" => "null".to_string(),
521 "csharp" => "null".to_string(),
522 "php" => "null".to_string(),
523 "r" => "NULL".to_string(),
524 _ => "null".to_string(),
525 },
526 _ => match language {
527 "python" => "None".to_string(),
528 "ruby" => "nil".to_string(),
529 "go" => "nil".to_string(),
530 "java" => "null".to_string(),
531 "csharp" => "null".to_string(),
532 "php" => "null".to_string(),
533 "r" => "NULL".to_string(),
534 "rust" => "Default::default()".to_string(),
535 _ => "null".to_string(),
536 },
537 }
538}
539
540trait TypeRefExt {
542 fn type_name(&self) -> String;
543}
544
545impl TypeRefExt for TypeRef {
546 fn type_name(&self) -> String {
547 match self {
548 TypeRef::Named(n) => n.clone(),
549 TypeRef::Primitive(p) => format!("{:?}", p),
550 TypeRef::String | TypeRef::Char => "String".to_string(),
551 TypeRef::Bytes => "Bytes".to_string(),
552 TypeRef::Optional(inner) => format!("Option<{}>", inner.type_name()),
553 TypeRef::Vec(inner) => format!("Vec<{}>", inner.type_name()),
554 TypeRef::Map(k, v) => format!("Map<{}, {}>", k.type_name(), v.type_name()),
555 TypeRef::Path => "Path".to_string(),
556 TypeRef::Unit => "()".to_string(),
557 TypeRef::Json => "Json".to_string(),
558 TypeRef::Duration => "Duration".to_string(),
559 }
560 }
561}
562
563const MAGNUS_MAX_ARITY: usize = 15;
565
566pub fn gen_magnus_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
572 if typ.fields.len() > MAGNUS_MAX_ARITY {
573 gen_magnus_hash_constructor(typ, type_mapper)
574 } else {
575 gen_magnus_positional_constructor(typ, type_mapper)
576 }
577}
578
579fn as_type_path_prefix(type_str: &str) -> String {
586 if type_str.contains('<') {
587 format!("<{type_str}>")
588 } else {
589 type_str.to_string()
590 }
591}
592
593fn gen_magnus_hash_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
596 let fields: Vec<_> = typ
597 .fields
598 .iter()
599 .map(|field| {
600 let is_optional = field_is_optional_in_rust(field);
601 let effective_inner_ty = match &field.ty {
605 TypeRef::Optional(inner) if is_optional => inner.as_ref(),
606 ty => ty,
607 };
608 let inner_type = type_mapper(effective_inner_ty);
609 let type_prefix = as_type_path_prefix(&inner_type);
610
611 let assignment = if is_optional {
612 format!(
614 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()),",
615 field.name, type_prefix
616 )
617 } else if use_unwrap_or_default(field) {
618 format!(
619 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).unwrap_or_default(),",
620 field.name, type_prefix
621 )
622 } else {
623 let default_str = if inner_type == "String" {
628 if let Some(DefaultValue::EnumVariant(variant)) = &field.typed_default {
629 use heck::ToSnakeCase;
630 format!("\"{}\".to_string()", variant.to_snake_case())
631 } else {
632 default_value_for_field(field, "rust")
633 }
634 } else {
635 default_value_for_field(field, "rust")
636 };
637 format!(
638 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).unwrap_or({}),",
639 field.name, type_prefix, default_str
640 )
641 };
642
643 minijinja::context! {
644 name => field.name.clone(),
645 assignment => assignment,
646 }
647 })
648 .collect();
649
650 crate::template_env::render(
651 "config_gen/magnus_hash_constructor.jinja",
652 minijinja::context! {
653 fields => fields,
654 },
655 )
656}
657
658fn field_is_optional_in_rust(field: &FieldDef) -> bool {
663 field.optional || matches!(&field.ty, TypeRef::Optional(_))
664}
665
666fn gen_magnus_positional_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
669 let fields: Vec<_> = typ
670 .fields
671 .iter()
672 .map(|field| {
673 let is_optional = field_is_optional_in_rust(field);
677 let param_type = if is_optional {
678 let effective_inner_ty = match &field.ty {
682 TypeRef::Optional(inner) => inner.as_ref(),
683 ty => ty,
684 };
685 let inner_type = type_mapper(effective_inner_ty);
686 format!("Option<{}>", inner_type)
687 } else {
688 let field_type = type_mapper(&field.ty);
689 format!("Option<{}>", field_type)
690 };
691
692 let assignment = if is_optional {
693 field.name.clone()
695 } else if use_unwrap_or_default(field) {
696 format!("{}.unwrap_or_default()", field.name)
697 } else {
698 let default_str = default_value_for_field(field, "rust");
699 format!("{}.unwrap_or({})", field.name, default_str)
700 };
701
702 minijinja::context! {
703 name => field.name.clone(),
704 param_type => param_type,
705 assignment => assignment,
706 }
707 })
708 .collect();
709
710 crate::template_env::render(
711 "config_gen/magnus_positional_constructor.jinja",
712 minijinja::context! {
713 fields => fields,
714 },
715 )
716}
717
718pub fn gen_php_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
722 let fields: Vec<_> = typ
723 .fields
724 .iter()
725 .map(|field| {
726 let mapped = type_mapper(&field.ty);
727 let is_optional_field = field.optional || matches!(&field.ty, TypeRef::Optional(_));
728
729 let assignment = if is_optional_field {
730 field.name.clone()
732 } else if use_unwrap_or_default(field) {
733 format!("{}.unwrap_or_default()", field.name)
735 } else {
736 let default_str = default_value_for_field(field, "rust");
738 format!("{}.unwrap_or({})", field.name, default_str)
739 };
740
741 minijinja::context! {
742 name => field.name.clone(),
743 ty => mapped,
744 assignment => assignment,
745 }
746 })
747 .collect();
748
749 crate::template_env::render(
750 "config_gen/php_kwargs_constructor.jinja",
751 minijinja::context! {
752 fields => fields,
753 },
754 )
755}
756
757pub fn gen_rustler_kwargs_constructor_with_exclude(
761 typ: &TypeDef,
762 _type_mapper: &dyn Fn(&TypeRef) -> String,
763 exclude_fields: &std::collections::HashSet<String>,
764) -> String {
765 let fields: Vec<_> = typ
767 .fields
768 .iter()
769 .filter(|f| !exclude_fields.contains(&f.name))
770 .map(|field| {
771 let assignment = if field.optional {
772 format!("opts.get(\"{}\").and_then(|t| t.decode().ok()),", field.name)
773 } else if use_unwrap_or_default(field) {
774 format!(
775 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
776 field.name
777 )
778 } else {
779 let default_str = default_value_for_field(field, "rust");
780 let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
781
782 if (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
783 || matches!(&field.ty, TypeRef::Named(_))
784 {
785 format!(
786 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
787 field.name
788 )
789 } else {
790 format!(
791 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
792 field.name, default_str
793 )
794 }
795 };
796
797 minijinja::context! {
798 name => field.name.clone(),
799 assignment => assignment,
800 }
801 })
802 .collect();
803
804 crate::template_env::render(
805 "config_gen/rustler_kwargs_constructor.jinja",
806 minijinja::context! {
807 fields => fields,
808 },
809 )
810}
811
812pub fn gen_rustler_kwargs_constructor(typ: &TypeDef, _type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
815 let fields: Vec<_> = typ
817 .fields
818 .iter()
819 .map(|field| {
820 let assignment = if field.optional {
821 format!("opts.get(\"{}\").and_then(|t| t.decode().ok()),", field.name)
822 } else if use_unwrap_or_default(field) {
823 format!(
824 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
825 field.name
826 )
827 } else {
828 let default_str = default_value_for_field(field, "rust");
829 let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
830
831 let unwrap_default = (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
832 || matches!(&field.ty, TypeRef::Named(_));
833 if unwrap_default {
834 format!(
835 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
836 field.name
837 )
838 } else {
839 format!(
840 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
841 field.name, default_str
842 )
843 }
844 };
845
846 minijinja::context! {
847 name => field.name.clone(),
848 assignment => assignment,
849 }
850 })
851 .collect();
852
853 crate::template_env::render(
854 "config_gen/rustler_kwargs_constructor.jinja",
855 minijinja::context! {
856 fields => fields,
857 },
858 )
859}
860
861pub fn gen_extendr_kwargs_constructor(
876 typ: &TypeDef,
877 type_mapper: &dyn Fn(&TypeRef) -> String,
878 enum_names: &ahash::AHashSet<String>,
879) -> String {
880 let is_named_enum = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if enum_names.contains(n.as_str())) };
882 let is_named_struct =
883 |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if !enum_names.contains(n.as_str())) };
884 let is_optional_named_struct = |ty: &TypeRef| -> bool {
885 if let TypeRef::Optional(inner) = ty {
886 is_named_struct(inner)
887 } else {
888 false
889 }
890 };
891 let ty_is_optional = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Optional(_)) };
892
893 let emittable_fields: Vec<_> = typ
895 .fields
896 .iter()
897 .filter(|f| !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
898 .map(|field| {
899 let param_type = if is_named_enum(&field.ty) {
900 "Option<String>".to_string()
901 } else if ty_is_optional(&field.ty) {
902 type_mapper(&field.ty)
903 } else {
904 format!("Option<{}>", type_mapper(&field.ty))
905 };
906
907 minijinja::context! {
908 name => field.name.clone(),
909 type => param_type,
910 }
911 })
912 .collect();
913
914 let body_assignments: Vec<_> = typ
916 .fields
917 .iter()
918 .filter(|f| !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
919 .map(|field| {
920 let code = if is_named_enum(&field.ty) {
921 if field.optional {
922 format!(
923 "if let Some(v) = {} {{ __out.{} = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")).ok(); }}",
924 field.name, field.name
925 )
926 } else {
927 format!(
928 "if let Some(v) = {} {{ if let Ok(parsed) = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")) {{ __out.{} = parsed; }} }}",
929 field.name, field.name
930 )
931 }
932 } else if ty_is_optional(&field.ty) || field.optional {
933 format!(
934 "if let Some(v) = {} {{ __out.{} = Some(v); }}",
935 field.name, field.name
936 )
937 } else {
938 format!(
939 "if let Some(v) = {} {{ __out.{} = v; }}",
940 field.name, field.name
941 )
942 };
943
944 minijinja::context! {
945 code => code,
946 }
947 })
948 .collect();
949
950 crate::template_env::render(
951 "config_gen/extendr_kwargs_constructor.jinja",
952 minijinja::context! {
953 type_name => typ.name.clone(),
954 type_name_lower => typ.name.to_lowercase(),
955 params => emittable_fields,
956 body_assignments => body_assignments,
957 },
958 )
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964 use alef_core::ir::{CoreWrapper, FieldDef, PrimitiveType, TypeRef};
965
966 fn make_test_type() -> TypeDef {
967 TypeDef {
968 name: "Config".to_string(),
969 rust_path: "my_crate::Config".to_string(),
970 original_rust_path: String::new(),
971 fields: vec![
972 FieldDef {
973 name: "timeout".to_string(),
974 ty: TypeRef::Primitive(PrimitiveType::U64),
975 optional: false,
976 default: Some("30".to_string()),
977 doc: "Timeout in seconds".to_string(),
978 sanitized: false,
979 is_boxed: false,
980 type_rust_path: None,
981 cfg: None,
982 typed_default: Some(DefaultValue::IntLiteral(30)),
983 core_wrapper: CoreWrapper::None,
984 vec_inner_core_wrapper: CoreWrapper::None,
985 newtype_wrapper: None,
986 },
987 FieldDef {
988 name: "enabled".to_string(),
989 ty: TypeRef::Primitive(PrimitiveType::Bool),
990 optional: false,
991 default: None,
992 doc: "Enable feature".to_string(),
993 sanitized: false,
994 is_boxed: false,
995 type_rust_path: None,
996 cfg: None,
997 typed_default: Some(DefaultValue::BoolLiteral(true)),
998 core_wrapper: CoreWrapper::None,
999 vec_inner_core_wrapper: CoreWrapper::None,
1000 newtype_wrapper: None,
1001 },
1002 FieldDef {
1003 name: "name".to_string(),
1004 ty: TypeRef::String,
1005 optional: false,
1006 default: None,
1007 doc: "Config name".to_string(),
1008 sanitized: false,
1009 is_boxed: false,
1010 type_rust_path: None,
1011 cfg: None,
1012 typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
1013 core_wrapper: CoreWrapper::None,
1014 vec_inner_core_wrapper: CoreWrapper::None,
1015 newtype_wrapper: None,
1016 },
1017 ],
1018 methods: vec![],
1019 is_opaque: false,
1020 is_clone: true,
1021 is_copy: false,
1022 doc: "Configuration type".to_string(),
1023 cfg: None,
1024 is_trait: false,
1025 has_default: true,
1026 has_stripped_cfg_fields: false,
1027 is_return_type: false,
1028 serde_rename_all: None,
1029 has_serde: false,
1030 super_traits: vec![],
1031 }
1032 }
1033
1034 #[test]
1035 fn test_default_value_bool_true_python() {
1036 let field = FieldDef {
1037 name: "enabled".to_string(),
1038 ty: TypeRef::Primitive(PrimitiveType::Bool),
1039 optional: false,
1040 default: None,
1041 doc: String::new(),
1042 sanitized: false,
1043 is_boxed: false,
1044 type_rust_path: None,
1045 cfg: None,
1046 typed_default: Some(DefaultValue::BoolLiteral(true)),
1047 core_wrapper: CoreWrapper::None,
1048 vec_inner_core_wrapper: CoreWrapper::None,
1049 newtype_wrapper: None,
1050 };
1051 assert_eq!(default_value_for_field(&field, "python"), "True");
1052 }
1053
1054 #[test]
1055 fn test_default_value_bool_false_go() {
1056 let field = FieldDef {
1057 name: "enabled".to_string(),
1058 ty: TypeRef::Primitive(PrimitiveType::Bool),
1059 optional: false,
1060 default: None,
1061 doc: String::new(),
1062 sanitized: false,
1063 is_boxed: false,
1064 type_rust_path: None,
1065 cfg: None,
1066 typed_default: Some(DefaultValue::BoolLiteral(false)),
1067 core_wrapper: CoreWrapper::None,
1068 vec_inner_core_wrapper: CoreWrapper::None,
1069 newtype_wrapper: None,
1070 };
1071 assert_eq!(default_value_for_field(&field, "go"), "false");
1072 }
1073
1074 #[test]
1075 fn test_default_value_string_literal() {
1076 let field = FieldDef {
1077 name: "name".to_string(),
1078 ty: TypeRef::String,
1079 optional: false,
1080 default: None,
1081 doc: String::new(),
1082 sanitized: false,
1083 is_boxed: false,
1084 type_rust_path: None,
1085 cfg: None,
1086 typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1087 core_wrapper: CoreWrapper::None,
1088 vec_inner_core_wrapper: CoreWrapper::None,
1089 newtype_wrapper: None,
1090 };
1091 assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
1092 assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
1093 }
1094
1095 #[test]
1096 fn test_default_value_int_literal() {
1097 let field = FieldDef {
1098 name: "timeout".to_string(),
1099 ty: TypeRef::Primitive(PrimitiveType::U64),
1100 optional: false,
1101 default: None,
1102 doc: String::new(),
1103 sanitized: false,
1104 is_boxed: false,
1105 type_rust_path: None,
1106 cfg: None,
1107 typed_default: Some(DefaultValue::IntLiteral(42)),
1108 core_wrapper: CoreWrapper::None,
1109 vec_inner_core_wrapper: CoreWrapper::None,
1110 newtype_wrapper: None,
1111 };
1112 let result = default_value_for_field(&field, "python");
1113 assert_eq!(result, "42");
1114 }
1115
1116 #[test]
1117 fn test_default_value_none() {
1118 let field = FieldDef {
1119 name: "maybe".to_string(),
1120 ty: TypeRef::Optional(Box::new(TypeRef::String)),
1121 optional: true,
1122 default: None,
1123 doc: String::new(),
1124 sanitized: false,
1125 is_boxed: false,
1126 type_rust_path: None,
1127 cfg: None,
1128 typed_default: Some(DefaultValue::None),
1129 core_wrapper: CoreWrapper::None,
1130 vec_inner_core_wrapper: CoreWrapper::None,
1131 newtype_wrapper: None,
1132 };
1133 assert_eq!(default_value_for_field(&field, "python"), "None");
1134 assert_eq!(default_value_for_field(&field, "go"), "nil");
1135 assert_eq!(default_value_for_field(&field, "java"), "null");
1136 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1137 }
1138
1139 #[test]
1140 fn test_default_value_fallback_string() {
1141 let field = FieldDef {
1142 name: "name".to_string(),
1143 ty: TypeRef::String,
1144 optional: false,
1145 default: Some("\"custom\"".to_string()),
1146 doc: String::new(),
1147 sanitized: false,
1148 is_boxed: false,
1149 type_rust_path: None,
1150 cfg: None,
1151 typed_default: None,
1152 core_wrapper: CoreWrapper::None,
1153 vec_inner_core_wrapper: CoreWrapper::None,
1154 newtype_wrapper: None,
1155 };
1156 assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1157 }
1158
1159 #[test]
1160 fn test_gen_pyo3_kwargs_constructor() {
1161 let typ = make_test_type();
1162 let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1163 TypeRef::Primitive(p) => format!("{:?}", p),
1164 TypeRef::String | TypeRef::Char => "str".to_string(),
1165 _ => "Any".to_string(),
1166 });
1167
1168 assert!(output.contains("#[new]"));
1169 assert!(output.contains("#[pyo3(signature = ("));
1170 assert!(output.contains("timeout=30"));
1171 assert!(output.contains("enabled=True"));
1172 assert!(output.contains("name=\"default\""));
1173 assert!(output.contains("fn new("));
1174 }
1175
1176 #[test]
1177 fn test_gen_napi_defaults_constructor() {
1178 let typ = make_test_type();
1179 let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1180 TypeRef::Primitive(p) => format!("{:?}", p),
1181 TypeRef::String | TypeRef::Char => "String".to_string(),
1182 _ => "Value".to_string(),
1183 });
1184
1185 assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1186 assert!(output.contains("timeout"));
1187 assert!(output.contains("enabled"));
1188 assert!(output.contains("name"));
1189 }
1190
1191 #[test]
1192 fn test_gen_go_functional_options() {
1193 let typ = make_test_type();
1194 let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1195 TypeRef::Primitive(p) => match p {
1196 PrimitiveType::U64 => "uint64".to_string(),
1197 PrimitiveType::Bool => "bool".to_string(),
1198 _ => "interface{}".to_string(),
1199 },
1200 TypeRef::String | TypeRef::Char => "string".to_string(),
1201 _ => "interface{}".to_string(),
1202 });
1203
1204 assert!(output.contains("type Config struct {"));
1205 assert!(output.contains("type ConfigOption func(*Config)"));
1206 assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1207 assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1208 assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1209 assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1210 }
1211
1212 #[test]
1213 fn test_gen_java_builder() {
1214 let typ = make_test_type();
1215 let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1216 TypeRef::Primitive(p) => match p {
1217 PrimitiveType::U64 => "long".to_string(),
1218 PrimitiveType::Bool => "boolean".to_string(),
1219 _ => "int".to_string(),
1220 },
1221 TypeRef::String | TypeRef::Char => "String".to_string(),
1222 _ => "Object".to_string(),
1223 });
1224
1225 assert!(output.contains("package dev.test;"));
1226 assert!(output.contains("public class ConfigBuilder"));
1227 assert!(output.contains("withTimeout"));
1228 assert!(output.contains("withEnabled"));
1229 assert!(output.contains("withName"));
1230 assert!(output.contains("public Config build()"));
1231 }
1232
1233 #[test]
1234 fn test_gen_csharp_record() {
1235 let typ = make_test_type();
1236 let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1237 TypeRef::Primitive(p) => match p {
1238 PrimitiveType::U64 => "ulong".to_string(),
1239 PrimitiveType::Bool => "bool".to_string(),
1240 _ => "int".to_string(),
1241 },
1242 TypeRef::String | TypeRef::Char => "string".to_string(),
1243 _ => "object".to_string(),
1244 });
1245
1246 assert!(output.contains("namespace MyNamespace;"));
1247 assert!(output.contains("public record Config"));
1248 assert!(output.contains("public ulong Timeout"));
1249 assert!(output.contains("public bool Enabled"));
1250 assert!(output.contains("public string Name"));
1251 assert!(output.contains("init;"));
1252 }
1253
1254 #[test]
1255 fn test_default_value_float_literal() {
1256 let field = FieldDef {
1257 name: "ratio".to_string(),
1258 ty: TypeRef::Primitive(PrimitiveType::F64),
1259 optional: false,
1260 default: None,
1261 doc: String::new(),
1262 sanitized: false,
1263 is_boxed: false,
1264 type_rust_path: None,
1265 cfg: None,
1266 typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1267 core_wrapper: CoreWrapper::None,
1268 vec_inner_core_wrapper: CoreWrapper::None,
1269 newtype_wrapper: None,
1270 };
1271 let result = default_value_for_field(&field, "python");
1272 assert!(result.contains("1.5"));
1273 }
1274
1275 #[test]
1276 fn test_default_value_no_typed_no_default() {
1277 let field = FieldDef {
1278 name: "count".to_string(),
1279 ty: TypeRef::Primitive(PrimitiveType::U32),
1280 optional: false,
1281 default: None,
1282 doc: String::new(),
1283 sanitized: false,
1284 is_boxed: false,
1285 type_rust_path: None,
1286 cfg: None,
1287 typed_default: None,
1288 core_wrapper: CoreWrapper::None,
1289 vec_inner_core_wrapper: CoreWrapper::None,
1290 newtype_wrapper: None,
1291 };
1292 assert_eq!(default_value_for_field(&field, "python"), "0");
1294 assert_eq!(default_value_for_field(&field, "go"), "0");
1295 }
1296
1297 fn make_field(name: &str, ty: TypeRef) -> FieldDef {
1298 FieldDef {
1299 name: name.to_string(),
1300 ty,
1301 optional: false,
1302 default: None,
1303 doc: String::new(),
1304 sanitized: false,
1305 is_boxed: false,
1306 type_rust_path: None,
1307 cfg: None,
1308 typed_default: None,
1309 core_wrapper: CoreWrapper::None,
1310 vec_inner_core_wrapper: CoreWrapper::None,
1311 newtype_wrapper: None,
1312 }
1313 }
1314
1315 fn simple_type_mapper(tr: &TypeRef) -> String {
1316 match tr {
1317 TypeRef::Primitive(p) => match p {
1318 PrimitiveType::U64 => "u64".to_string(),
1319 PrimitiveType::Bool => "bool".to_string(),
1320 PrimitiveType::U32 => "u32".to_string(),
1321 _ => "i64".to_string(),
1322 },
1323 TypeRef::String | TypeRef::Char => "String".to_string(),
1324 TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
1325 TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
1326 TypeRef::Named(n) => n.clone(),
1327 _ => "Value".to_string(),
1328 }
1329 }
1330
1331 #[test]
1336 fn test_default_value_bool_literal_ruby() {
1337 let field = FieldDef {
1338 name: "flag".to_string(),
1339 ty: TypeRef::Primitive(PrimitiveType::Bool),
1340 optional: false,
1341 default: None,
1342 doc: String::new(),
1343 sanitized: false,
1344 is_boxed: false,
1345 type_rust_path: None,
1346 cfg: None,
1347 typed_default: Some(DefaultValue::BoolLiteral(true)),
1348 core_wrapper: CoreWrapper::None,
1349 vec_inner_core_wrapper: CoreWrapper::None,
1350 newtype_wrapper: None,
1351 };
1352 assert_eq!(default_value_for_field(&field, "ruby"), "true");
1353 assert_eq!(default_value_for_field(&field, "php"), "true");
1354 assert_eq!(default_value_for_field(&field, "csharp"), "true");
1355 assert_eq!(default_value_for_field(&field, "java"), "true");
1356 assert_eq!(default_value_for_field(&field, "rust"), "true");
1357 }
1358
1359 #[test]
1360 fn test_default_value_bool_literal_r() {
1361 let field = FieldDef {
1362 name: "flag".to_string(),
1363 ty: TypeRef::Primitive(PrimitiveType::Bool),
1364 optional: false,
1365 default: None,
1366 doc: String::new(),
1367 sanitized: false,
1368 is_boxed: false,
1369 type_rust_path: None,
1370 cfg: None,
1371 typed_default: Some(DefaultValue::BoolLiteral(false)),
1372 core_wrapper: CoreWrapper::None,
1373 vec_inner_core_wrapper: CoreWrapper::None,
1374 newtype_wrapper: None,
1375 };
1376 assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1377 }
1378
1379 #[test]
1380 fn test_default_value_string_literal_rust() {
1381 let field = FieldDef {
1382 name: "label".to_string(),
1383 ty: TypeRef::String,
1384 optional: false,
1385 default: None,
1386 doc: String::new(),
1387 sanitized: false,
1388 is_boxed: false,
1389 type_rust_path: None,
1390 cfg: None,
1391 typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1392 core_wrapper: CoreWrapper::None,
1393 vec_inner_core_wrapper: CoreWrapper::None,
1394 newtype_wrapper: None,
1395 };
1396 assert_eq!(default_value_for_field(&field, "rust"), "\"hello\".to_string()");
1397 }
1398
1399 #[test]
1400 fn test_default_value_string_literal_escapes_quotes() {
1401 let field = FieldDef {
1402 name: "label".to_string(),
1403 ty: TypeRef::String,
1404 optional: false,
1405 default: None,
1406 doc: String::new(),
1407 sanitized: false,
1408 is_boxed: false,
1409 type_rust_path: None,
1410 cfg: None,
1411 typed_default: Some(DefaultValue::StringLiteral("say \"hi\"".to_string())),
1412 core_wrapper: CoreWrapper::None,
1413 vec_inner_core_wrapper: CoreWrapper::None,
1414 newtype_wrapper: None,
1415 };
1416 assert_eq!(default_value_for_field(&field, "python"), "\"say \\\"hi\\\"\"");
1417 }
1418
1419 #[test]
1420 fn test_default_value_float_literal_whole_number() {
1421 let field = FieldDef {
1423 name: "scale".to_string(),
1424 ty: TypeRef::Primitive(PrimitiveType::F32),
1425 optional: false,
1426 default: None,
1427 doc: String::new(),
1428 sanitized: false,
1429 is_boxed: false,
1430 type_rust_path: None,
1431 cfg: None,
1432 typed_default: Some(DefaultValue::FloatLiteral(2.0)),
1433 core_wrapper: CoreWrapper::None,
1434 vec_inner_core_wrapper: CoreWrapper::None,
1435 newtype_wrapper: None,
1436 };
1437 let result = default_value_for_field(&field, "python");
1438 assert!(result.contains('.'), "whole-number float should contain '.': {result}");
1439 }
1440
1441 #[test]
1442 fn test_default_value_enum_variant_per_language() {
1443 let field = FieldDef {
1444 name: "format".to_string(),
1445 ty: TypeRef::Named("OutputFormat".to_string()),
1446 optional: false,
1447 default: None,
1448 doc: String::new(),
1449 sanitized: false,
1450 is_boxed: false,
1451 type_rust_path: None,
1452 cfg: None,
1453 typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
1454 core_wrapper: CoreWrapper::None,
1455 vec_inner_core_wrapper: CoreWrapper::None,
1456 newtype_wrapper: None,
1457 };
1458 assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
1459 assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
1460 assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
1461 assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
1462 assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
1463 assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
1464 assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
1465 assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
1466 }
1467
1468 #[test]
1469 fn test_default_value_empty_vec_per_language() {
1470 let field = FieldDef {
1471 name: "items".to_string(),
1472 ty: TypeRef::Vec(Box::new(TypeRef::String)),
1473 optional: false,
1474 default: None,
1475 doc: String::new(),
1476 sanitized: false,
1477 is_boxed: false,
1478 type_rust_path: None,
1479 cfg: None,
1480 typed_default: Some(DefaultValue::Empty),
1481 core_wrapper: CoreWrapper::None,
1482 vec_inner_core_wrapper: CoreWrapper::None,
1483 newtype_wrapper: None,
1484 };
1485 assert_eq!(default_value_for_field(&field, "python"), "[]");
1486 assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1487 assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1488 assert_eq!(default_value_for_field(&field, "go"), "nil");
1489 assert_eq!(default_value_for_field(&field, "java"), "List.of()");
1490 assert_eq!(default_value_for_field(&field, "php"), "[]");
1491 assert_eq!(default_value_for_field(&field, "r"), "c()");
1492 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1493 }
1494
1495 #[test]
1496 fn test_default_value_empty_map_per_language() {
1497 let field = FieldDef {
1498 name: "meta".to_string(),
1499 ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1500 optional: false,
1501 default: None,
1502 doc: String::new(),
1503 sanitized: false,
1504 is_boxed: false,
1505 type_rust_path: None,
1506 cfg: None,
1507 typed_default: Some(DefaultValue::Empty),
1508 core_wrapper: CoreWrapper::None,
1509 vec_inner_core_wrapper: CoreWrapper::None,
1510 newtype_wrapper: None,
1511 };
1512 assert_eq!(default_value_for_field(&field, "python"), "{}");
1513 assert_eq!(default_value_for_field(&field, "go"), "nil");
1514 assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
1515 assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1516 }
1517
1518 #[test]
1519 fn test_default_value_empty_bool_primitive() {
1520 let field = FieldDef {
1521 name: "flag".to_string(),
1522 ty: TypeRef::Primitive(PrimitiveType::Bool),
1523 optional: false,
1524 default: None,
1525 doc: String::new(),
1526 sanitized: false,
1527 is_boxed: false,
1528 type_rust_path: None,
1529 cfg: None,
1530 typed_default: Some(DefaultValue::Empty),
1531 core_wrapper: CoreWrapper::None,
1532 vec_inner_core_wrapper: CoreWrapper::None,
1533 newtype_wrapper: None,
1534 };
1535 assert_eq!(default_value_for_field(&field, "python"), "False");
1536 assert_eq!(default_value_for_field(&field, "ruby"), "false");
1537 assert_eq!(default_value_for_field(&field, "go"), "false");
1538 }
1539
1540 #[test]
1541 fn test_default_value_empty_float_primitive() {
1542 let field = FieldDef {
1543 name: "ratio".to_string(),
1544 ty: TypeRef::Primitive(PrimitiveType::F64),
1545 optional: false,
1546 default: None,
1547 doc: String::new(),
1548 sanitized: false,
1549 is_boxed: false,
1550 type_rust_path: None,
1551 cfg: None,
1552 typed_default: Some(DefaultValue::Empty),
1553 core_wrapper: CoreWrapper::None,
1554 vec_inner_core_wrapper: CoreWrapper::None,
1555 newtype_wrapper: None,
1556 };
1557 assert_eq!(default_value_for_field(&field, "python"), "0.0");
1558 }
1559
1560 #[test]
1561 fn test_default_value_empty_string_type() {
1562 let field = FieldDef {
1563 name: "label".to_string(),
1564 ty: TypeRef::String,
1565 optional: false,
1566 default: None,
1567 doc: String::new(),
1568 sanitized: false,
1569 is_boxed: false,
1570 type_rust_path: None,
1571 cfg: None,
1572 typed_default: Some(DefaultValue::Empty),
1573 core_wrapper: CoreWrapper::None,
1574 vec_inner_core_wrapper: CoreWrapper::None,
1575 newtype_wrapper: None,
1576 };
1577 assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1578 assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1579 }
1580
1581 #[test]
1582 fn test_default_value_empty_bytes_type() {
1583 let field = FieldDef {
1584 name: "data".to_string(),
1585 ty: TypeRef::Bytes,
1586 optional: false,
1587 default: None,
1588 doc: String::new(),
1589 sanitized: false,
1590 is_boxed: false,
1591 type_rust_path: None,
1592 cfg: None,
1593 typed_default: Some(DefaultValue::Empty),
1594 core_wrapper: CoreWrapper::None,
1595 vec_inner_core_wrapper: CoreWrapper::None,
1596 newtype_wrapper: None,
1597 };
1598 assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1599 assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1600 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1601 }
1602
1603 #[test]
1604 fn test_default_value_empty_json_type() {
1605 let field = FieldDef {
1606 name: "payload".to_string(),
1607 ty: TypeRef::Json,
1608 optional: false,
1609 default: None,
1610 doc: String::new(),
1611 sanitized: false,
1612 is_boxed: false,
1613 type_rust_path: None,
1614 cfg: None,
1615 typed_default: Some(DefaultValue::Empty),
1616 core_wrapper: CoreWrapper::None,
1617 vec_inner_core_wrapper: CoreWrapper::None,
1618 newtype_wrapper: None,
1619 };
1620 assert_eq!(default_value_for_field(&field, "python"), "{}");
1621 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1622 assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1623 assert_eq!(default_value_for_field(&field, "r"), "list()");
1624 assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1625 }
1626
1627 #[test]
1628 fn test_default_value_none_ruby_php_r() {
1629 let field = FieldDef {
1630 name: "maybe".to_string(),
1631 ty: TypeRef::Optional(Box::new(TypeRef::String)),
1632 optional: true,
1633 default: None,
1634 doc: String::new(),
1635 sanitized: false,
1636 is_boxed: false,
1637 type_rust_path: None,
1638 cfg: None,
1639 typed_default: Some(DefaultValue::None),
1640 core_wrapper: CoreWrapper::None,
1641 vec_inner_core_wrapper: CoreWrapper::None,
1642 newtype_wrapper: None,
1643 };
1644 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1645 assert_eq!(default_value_for_field(&field, "php"), "null");
1646 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1647 assert_eq!(default_value_for_field(&field, "rust"), "None");
1648 }
1649
1650 #[test]
1655 fn test_default_value_fallback_bool_all_languages() {
1656 let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
1657 assert_eq!(default_value_for_field(&field, "python"), "False");
1658 assert_eq!(default_value_for_field(&field, "ruby"), "false");
1659 assert_eq!(default_value_for_field(&field, "csharp"), "false");
1660 assert_eq!(default_value_for_field(&field, "java"), "false");
1661 assert_eq!(default_value_for_field(&field, "php"), "false");
1662 assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1663 assert_eq!(default_value_for_field(&field, "rust"), "false");
1664 }
1665
1666 #[test]
1667 fn test_default_value_fallback_float() {
1668 let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
1669 assert_eq!(default_value_for_field(&field, "python"), "0.0");
1670 assert_eq!(default_value_for_field(&field, "rust"), "0.0");
1671 }
1672
1673 #[test]
1674 fn test_default_value_fallback_string_all_languages() {
1675 let field = make_field("name", TypeRef::String);
1676 assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1677 assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1678 assert_eq!(default_value_for_field(&field, "go"), "\"\"");
1679 assert_eq!(default_value_for_field(&field, "java"), "\"\"");
1680 assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
1681 assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1682 assert_eq!(default_value_for_field(&field, "r"), "\"\"");
1683 assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1684 }
1685
1686 #[test]
1687 fn test_default_value_fallback_bytes_all_languages() {
1688 let field = make_field("data", TypeRef::Bytes);
1689 assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1690 assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1691 assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1692 assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
1693 assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
1694 assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1695 assert_eq!(default_value_for_field(&field, "r"), "raw()");
1696 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1697 }
1698
1699 #[test]
1700 fn test_default_value_fallback_optional() {
1701 let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
1702 assert_eq!(default_value_for_field(&field, "python"), "None");
1703 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1704 assert_eq!(default_value_for_field(&field, "go"), "nil");
1705 assert_eq!(default_value_for_field(&field, "java"), "null");
1706 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1707 assert_eq!(default_value_for_field(&field, "php"), "null");
1708 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1709 assert_eq!(default_value_for_field(&field, "rust"), "None");
1710 }
1711
1712 #[test]
1713 fn test_default_value_fallback_vec_all_languages() {
1714 let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
1715 assert_eq!(default_value_for_field(&field, "python"), "[]");
1716 assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1717 assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
1718 assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
1719 assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1720 assert_eq!(default_value_for_field(&field, "php"), "[]");
1721 assert_eq!(default_value_for_field(&field, "r"), "c()");
1722 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1723 }
1724
1725 #[test]
1726 fn test_default_value_fallback_map_all_languages() {
1727 let field = make_field(
1728 "meta",
1729 TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1730 );
1731 assert_eq!(default_value_for_field(&field, "python"), "{}");
1732 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1733 assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
1734 assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
1735 assert_eq!(
1736 default_value_for_field(&field, "csharp"),
1737 "new Dictionary<string, object>()"
1738 );
1739 assert_eq!(default_value_for_field(&field, "php"), "[]");
1740 assert_eq!(default_value_for_field(&field, "r"), "list()");
1741 assert_eq!(
1742 default_value_for_field(&field, "rust"),
1743 "std::collections::HashMap::new()"
1744 );
1745 }
1746
1747 #[test]
1748 fn test_default_value_fallback_json_all_languages() {
1749 let field = make_field("payload", TypeRef::Json);
1750 assert_eq!(default_value_for_field(&field, "python"), "{}");
1751 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1752 assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1753 assert_eq!(default_value_for_field(&field, "r"), "list()");
1754 assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1755 }
1756
1757 #[test]
1758 fn test_default_value_fallback_named_type() {
1759 let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
1760 assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
1761 assert_eq!(default_value_for_field(&field, "python"), "None");
1762 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1763 assert_eq!(default_value_for_field(&field, "go"), "nil");
1764 assert_eq!(default_value_for_field(&field, "java"), "null");
1765 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1766 assert_eq!(default_value_for_field(&field, "php"), "null");
1767 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1768 }
1769
1770 #[test]
1771 fn test_default_value_fallback_duration() {
1772 let field = make_field("timeout", TypeRef::Duration);
1774 assert_eq!(default_value_for_field(&field, "python"), "None");
1775 assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1776 }
1777
1778 #[test]
1783 fn test_gen_magnus_kwargs_constructor_positional_basic() {
1784 let typ = make_test_type();
1785 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1786
1787 assert!(output.contains("fn new("), "should have fn new");
1788 assert!(output.contains("Option<u64>"), "timeout should be Option<u64>");
1790 assert!(output.contains("Option<bool>"), "enabled should be Option<bool>");
1791 assert!(output.contains("Option<String>"), "name should be Option<String>");
1792 assert!(output.contains("-> Self {"), "should return Self");
1793 assert!(
1795 output.contains("timeout: timeout.unwrap_or(30),"),
1796 "should apply int default"
1797 );
1798 assert!(
1800 output.contains("enabled: enabled.unwrap_or(true),"),
1801 "should apply bool default"
1802 );
1803 assert!(
1805 output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1806 "should apply string default"
1807 );
1808 }
1809
1810 #[test]
1811 fn test_gen_magnus_kwargs_constructor_positional_optional_field() {
1812 let mut typ = make_test_type();
1814 typ.fields.push(FieldDef {
1815 name: "extra".to_string(),
1816 ty: TypeRef::String,
1817 optional: true,
1818 default: None,
1819 doc: String::new(),
1820 sanitized: false,
1821 is_boxed: false,
1822 type_rust_path: None,
1823 cfg: None,
1824 typed_default: None,
1825 core_wrapper: CoreWrapper::None,
1826 vec_inner_core_wrapper: CoreWrapper::None,
1827 newtype_wrapper: None,
1828 });
1829 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1830 assert!(output.contains("extra,"), "optional field should be assigned directly");
1832 assert!(!output.contains("extra.unwrap"), "optional field should not use unwrap");
1833 }
1834
1835 #[test]
1836 fn test_gen_magnus_kwargs_constructor_unwrap_or_default() {
1837 let mut typ = make_test_type();
1839 typ.fields.push(FieldDef {
1840 name: "count".to_string(),
1841 ty: TypeRef::Primitive(PrimitiveType::U32),
1842 optional: false,
1843 default: None,
1844 doc: String::new(),
1845 sanitized: false,
1846 is_boxed: false,
1847 type_rust_path: None,
1848 cfg: None,
1849 typed_default: None,
1850 core_wrapper: CoreWrapper::None,
1851 vec_inner_core_wrapper: CoreWrapper::None,
1852 newtype_wrapper: None,
1853 });
1854 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1855 assert!(
1856 output.contains("count: count.unwrap_or_default(),"),
1857 "plain primitive with no default should use unwrap_or_default"
1858 );
1859 }
1860
1861 #[test]
1862 fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
1863 let mut fields: Vec<FieldDef> = (0..16)
1865 .map(|i| FieldDef {
1866 name: format!("field_{i}"),
1867 ty: TypeRef::Primitive(PrimitiveType::U32),
1868 optional: false,
1869 default: None,
1870 doc: String::new(),
1871 sanitized: false,
1872 is_boxed: false,
1873 type_rust_path: None,
1874 cfg: None,
1875 typed_default: None,
1876 core_wrapper: CoreWrapper::None,
1877 vec_inner_core_wrapper: CoreWrapper::None,
1878 newtype_wrapper: None,
1879 })
1880 .collect();
1881 fields[0].optional = true;
1883
1884 let typ = TypeDef {
1885 name: "BigConfig".to_string(),
1886 rust_path: "crate::BigConfig".to_string(),
1887 original_rust_path: String::new(),
1888 fields,
1889 methods: vec![],
1890 is_opaque: false,
1891 is_clone: true,
1892 is_copy: false,
1893 doc: String::new(),
1894 cfg: None,
1895 is_trait: false,
1896 has_default: true,
1897 has_stripped_cfg_fields: false,
1898 is_return_type: false,
1899 serde_rename_all: None,
1900 has_serde: false,
1901 super_traits: vec![],
1902 };
1903 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1904
1905 assert!(
1906 output.contains("Option<magnus::RHash>"),
1907 "should accept RHash via scan_args"
1908 );
1909 assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
1910 assert!(
1912 output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
1913 "optional field should use and_then"
1914 );
1915 assert!(
1916 output.contains("field_0:").then_some(()).is_some(),
1917 "field_0 should appear in output"
1918 );
1919 }
1920
1921 #[test]
1926 fn test_gen_php_kwargs_constructor_basic() {
1927 let typ = make_test_type();
1928 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1929
1930 assert!(
1931 output.contains("pub fn __construct("),
1932 "should use PHP constructor name"
1933 );
1934 assert!(
1936 output.contains("timeout: Option<u64>"),
1937 "timeout param should be Option<u64>"
1938 );
1939 assert!(
1940 output.contains("enabled: Option<bool>"),
1941 "enabled param should be Option<bool>"
1942 );
1943 assert!(
1944 output.contains("name: Option<String>"),
1945 "name param should be Option<String>"
1946 );
1947 assert!(output.contains("-> Self {"), "should return Self");
1948 assert!(
1949 output.contains("timeout: timeout.unwrap_or(30),"),
1950 "should apply int default for timeout"
1951 );
1952 assert!(
1953 output.contains("enabled: enabled.unwrap_or(true),"),
1954 "should apply bool default for enabled"
1955 );
1956 assert!(
1957 output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1958 "should apply string default for name"
1959 );
1960 }
1961
1962 #[test]
1963 fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
1964 let mut typ = make_test_type();
1965 typ.fields.push(FieldDef {
1966 name: "tag".to_string(),
1967 ty: TypeRef::String,
1968 optional: true,
1969 default: None,
1970 doc: String::new(),
1971 sanitized: false,
1972 is_boxed: false,
1973 type_rust_path: None,
1974 cfg: None,
1975 typed_default: None,
1976 core_wrapper: CoreWrapper::None,
1977 vec_inner_core_wrapper: CoreWrapper::None,
1978 newtype_wrapper: None,
1979 });
1980 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1981 assert!(
1982 output.contains("tag,"),
1983 "optional field should be passed through directly"
1984 );
1985 assert!(!output.contains("tag.unwrap"), "optional field should not call unwrap");
1986 }
1987
1988 #[test]
1989 fn test_gen_php_kwargs_constructor_unwrap_or_default_for_primitive() {
1990 let mut typ = make_test_type();
1991 typ.fields.push(FieldDef {
1992 name: "retries".to_string(),
1993 ty: TypeRef::Primitive(PrimitiveType::U32),
1994 optional: false,
1995 default: None,
1996 doc: String::new(),
1997 sanitized: false,
1998 is_boxed: false,
1999 type_rust_path: None,
2000 cfg: None,
2001 typed_default: None,
2002 core_wrapper: CoreWrapper::None,
2003 vec_inner_core_wrapper: CoreWrapper::None,
2004 newtype_wrapper: None,
2005 });
2006 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2007 assert!(
2008 output.contains("retries: retries.unwrap_or_default(),"),
2009 "primitive with no default should use unwrap_or_default"
2010 );
2011 }
2012
2013 #[test]
2018 fn test_gen_rustler_kwargs_constructor_basic() {
2019 let typ = make_test_type();
2020 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2021
2022 assert!(
2023 output.contains("pub fn new(opts: std::collections::HashMap<String, rustler::Term>)"),
2024 "should accept HashMap of Terms"
2025 );
2026 assert!(output.contains("Self {"), "should construct Self");
2027 assert!(
2029 output.contains("timeout: opts.get(\"timeout\").and_then(|t| t.decode().ok()).unwrap_or(30),"),
2030 "should apply int default for timeout"
2031 );
2032 assert!(
2034 output.contains("enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),"),
2035 "should apply bool default for enabled"
2036 );
2037 }
2038
2039 #[test]
2040 fn test_gen_rustler_kwargs_constructor_optional_field() {
2041 let mut typ = make_test_type();
2042 typ.fields.push(FieldDef {
2043 name: "extra".to_string(),
2044 ty: TypeRef::String,
2045 optional: true,
2046 default: None,
2047 doc: String::new(),
2048 sanitized: false,
2049 is_boxed: false,
2050 type_rust_path: None,
2051 cfg: None,
2052 typed_default: None,
2053 core_wrapper: CoreWrapper::None,
2054 vec_inner_core_wrapper: CoreWrapper::None,
2055 newtype_wrapper: None,
2056 });
2057 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2058 assert!(
2059 output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
2060 "optional field should decode without unwrap"
2061 );
2062 }
2063
2064 #[test]
2065 fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
2066 let mut typ = make_test_type();
2067 typ.fields.push(FieldDef {
2068 name: "inner".to_string(),
2069 ty: TypeRef::Named("InnerConfig".to_string()),
2070 optional: false,
2071 default: None,
2072 doc: String::new(),
2073 sanitized: false,
2074 is_boxed: false,
2075 type_rust_path: None,
2076 cfg: None,
2077 typed_default: None,
2078 core_wrapper: CoreWrapper::None,
2079 vec_inner_core_wrapper: CoreWrapper::None,
2080 newtype_wrapper: None,
2081 });
2082 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2083 assert!(
2084 output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2085 "Named type with no default should use unwrap_or_default"
2086 );
2087 }
2088
2089 #[test]
2090 fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
2091 let mut typ = make_test_type();
2094 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2096 assert!(
2097 output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2098 "String field with quoted default should use unwrap_or_default"
2099 );
2100 typ.fields.push(FieldDef {
2102 name: "label".to_string(),
2103 ty: TypeRef::String,
2104 optional: false,
2105 default: None,
2106 doc: String::new(),
2107 sanitized: false,
2108 is_boxed: false,
2109 type_rust_path: None,
2110 cfg: None,
2111 typed_default: None,
2112 core_wrapper: CoreWrapper::None,
2113 vec_inner_core_wrapper: CoreWrapper::None,
2114 newtype_wrapper: None,
2115 });
2116 let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2117 assert!(
2118 output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2119 "String field with no default should use unwrap_or_default"
2120 );
2121 }
2122
2123 #[test]
2128 fn test_gen_extendr_kwargs_constructor_basic() {
2129 let typ = make_test_type();
2130 let empty_enums = ahash::AHashSet::new();
2131 let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2132
2133 assert!(output.contains("#[extendr]"), "should have extendr attribute");
2134 assert!(
2135 output.contains("pub fn new_config("),
2136 "function name should be lowercase type name"
2137 );
2138 assert!(
2140 output.contains("timeout: Option<u64>"),
2141 "should accept timeout as Option<u64>: {output}"
2142 );
2143 assert!(
2144 output.contains("enabled: Option<bool>"),
2145 "should accept enabled as Option<bool>: {output}"
2146 );
2147 assert!(
2148 output.contains("name: Option<String>"),
2149 "should accept name as Option<String>: {output}"
2150 );
2151 assert!(output.contains("-> Config {"), "should return Config");
2152 assert!(
2153 output.contains("let mut __out = <Config>::default();"),
2154 "should base on Default impl: {output}"
2155 );
2156 assert!(
2157 output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
2158 "should overlay caller-provided timeout"
2159 );
2160 assert!(
2161 output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
2162 "should overlay caller-provided enabled"
2163 );
2164 assert!(
2165 output.contains("if let Some(v) = name { __out.name = v; }"),
2166 "should overlay caller-provided name"
2167 );
2168 }
2169
2170 #[test]
2171 fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
2172 let typ = make_test_type();
2176 let empty_enums = ahash::AHashSet::new();
2177 let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2178 assert!(
2179 !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
2180 "constructor must not use Rust-syntax param defaults: {output}"
2181 );
2182 }
2183
2184 #[test]
2189 fn test_gen_go_functional_options_skips_tuple_fields() {
2190 let mut typ = make_test_type();
2191 typ.fields.push(FieldDef {
2192 name: "_0".to_string(),
2193 ty: TypeRef::Primitive(PrimitiveType::U32),
2194 optional: false,
2195 default: None,
2196 doc: String::new(),
2197 sanitized: false,
2198 is_boxed: false,
2199 type_rust_path: None,
2200 cfg: None,
2201 typed_default: None,
2202 core_wrapper: CoreWrapper::None,
2203 vec_inner_core_wrapper: CoreWrapper::None,
2204 newtype_wrapper: None,
2205 });
2206 let output = gen_go_functional_options(&typ, &simple_type_mapper);
2207 assert!(
2208 !output.contains("_0"),
2209 "tuple field _0 should be filtered out from Go output"
2210 );
2211 }
2212
2213 #[test]
2218 fn test_gen_magnus_hash_constructor_generic_type_prefix() {
2219 let fields: Vec<FieldDef> = (0..16)
2221 .map(|i| FieldDef {
2222 name: format!("field_{i}"),
2223 ty: if i == 0 {
2224 TypeRef::Vec(Box::new(TypeRef::String))
2225 } else {
2226 TypeRef::Primitive(PrimitiveType::U32)
2227 },
2228 optional: false,
2229 default: None,
2230 doc: String::new(),
2231 sanitized: false,
2232 is_boxed: false,
2233 type_rust_path: None,
2234 cfg: None,
2235 typed_default: None,
2236 core_wrapper: CoreWrapper::None,
2237 vec_inner_core_wrapper: CoreWrapper::None,
2238 newtype_wrapper: None,
2239 })
2240 .collect();
2241 let typ = TypeDef {
2242 name: "WideConfig".to_string(),
2243 rust_path: "crate::WideConfig".to_string(),
2244 original_rust_path: String::new(),
2245 fields,
2246 methods: vec![],
2247 is_opaque: false,
2248 is_clone: true,
2249 is_copy: false,
2250 doc: String::new(),
2251 cfg: None,
2252 is_trait: false,
2253 has_default: true,
2254 has_stripped_cfg_fields: false,
2255 is_return_type: false,
2256 serde_rename_all: None,
2257 has_serde: false,
2258 super_traits: vec![],
2259 };
2260 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2261 assert!(
2263 output.contains("<Vec<String>>::try_convert"),
2264 "generic types should use UFCS angle-bracket prefix: {output}"
2265 );
2266 }
2267
2268 #[test]
2275 fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
2276 let field = FieldDef {
2280 name: "max_depth".to_string(),
2281 ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2282 optional: true,
2283 default: None,
2284 doc: String::new(),
2285 sanitized: false,
2286 is_boxed: false,
2287 type_rust_path: None,
2288 cfg: None,
2289 typed_default: None,
2290 core_wrapper: CoreWrapper::None,
2291 vec_inner_core_wrapper: CoreWrapper::None,
2292 newtype_wrapper: None,
2293 };
2294 let mut fields: Vec<FieldDef> = (0..15)
2296 .map(|i| FieldDef {
2297 name: format!("field_{i}"),
2298 ty: TypeRef::Primitive(PrimitiveType::U32),
2299 optional: false,
2300 default: None,
2301 doc: String::new(),
2302 sanitized: false,
2303 is_boxed: false,
2304 type_rust_path: None,
2305 cfg: None,
2306 typed_default: None,
2307 core_wrapper: CoreWrapper::None,
2308 vec_inner_core_wrapper: CoreWrapper::None,
2309 newtype_wrapper: None,
2310 })
2311 .collect();
2312 fields.push(field);
2313 let typ = TypeDef {
2314 name: "UpdateConfig".to_string(),
2315 rust_path: "crate::UpdateConfig".to_string(),
2316 original_rust_path: String::new(),
2317 fields,
2318 methods: vec![],
2319 is_opaque: false,
2320 is_clone: true,
2321 is_copy: false,
2322 doc: String::new(),
2323 cfg: None,
2324 is_trait: false,
2325 has_default: true,
2326 has_stripped_cfg_fields: false,
2327 is_return_type: false,
2328 serde_rename_all: None,
2329 has_serde: false,
2330 super_traits: vec![],
2331 };
2332 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2333 assert!(
2336 !output.contains("Option<Option<"),
2337 "hash constructor must not emit double Option: {output}"
2338 );
2339 assert!(
2340 output.contains("i64::try_convert"),
2341 "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
2342 );
2343 }
2344
2345 #[test]
2346 fn test_magnus_positional_constructor_no_double_option_when_ty_is_optional() {
2347 let field = FieldDef {
2350 name: "max_depth".to_string(),
2351 ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2352 optional: true,
2353 default: None,
2354 doc: String::new(),
2355 sanitized: false,
2356 is_boxed: false,
2357 type_rust_path: None,
2358 cfg: None,
2359 typed_default: None,
2360 core_wrapper: CoreWrapper::None,
2361 vec_inner_core_wrapper: CoreWrapper::None,
2362 newtype_wrapper: None,
2363 };
2364 let typ = TypeDef {
2365 name: "SmallUpdate".to_string(),
2366 rust_path: "crate::SmallUpdate".to_string(),
2367 original_rust_path: String::new(),
2368 fields: vec![field],
2369 methods: vec![],
2370 is_opaque: false,
2371 is_clone: true,
2372 is_copy: false,
2373 doc: String::new(),
2374 cfg: None,
2375 is_trait: false,
2376 has_default: true,
2377 has_stripped_cfg_fields: false,
2378 is_return_type: false,
2379 serde_rename_all: None,
2380 has_serde: false,
2381 super_traits: vec![],
2382 };
2383 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2384 assert!(
2387 !output.contains("Option<Option<"),
2388 "positional constructor must not emit double Option: {output}"
2389 );
2390 assert!(
2391 output.contains("Option<i64>"),
2392 "positional constructor should emit Option<inner> for optional Optional(T): {output}"
2393 );
2394 }
2395}