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