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 signature_defaults = typ
29 .fields
30 .iter()
31 .map(|field| format!("{}={}", field.name, default_value_for_field(field, "python")))
32 .collect::<Vec<_>>()
33 .join(", ");
34 let fields: Vec<_> = typ
35 .fields
36 .iter()
37 .map(|field| {
38 minijinja::context! {
39 name => field.name.clone(),
40 type => type_mapper(&field.ty),
41 }
42 })
43 .collect();
44
45 crate::template_env::render(
46 "config_gen/pyo3_kwargs_constructor.jinja",
47 minijinja::context! {
48 signature_defaults => signature_defaults,
49 fields => fields,
50 },
51 )
52 .trim_end_matches('\n')
53 .to_string()
54}
55
56pub fn gen_napi_defaults_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
58 let fields: Vec<_> = typ
59 .fields
60 .iter()
61 .map(|field| {
62 minijinja::context! {
63 name => field.name.clone(),
64 type => type_mapper(&field.ty),
65 default => default_value_for_field(field, "rust"),
66 }
67 })
68 .collect();
69
70 crate::template_env::render(
71 "config_gen/napi_defaults_constructor.jinja",
72 minijinja::context! {
73 fields => fields,
74 },
75 )
76 .trim_end_matches('\n')
77 .to_string()
78}
79
80pub fn gen_go_functional_options(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
83 let fields: Vec<_> = typ
84 .fields
85 .iter()
86 .filter(|field| !is_tuple_field(field))
87 .map(|field| {
88 minijinja::context! {
89 name => field.name.clone(),
90 pascal_name => field.name.to_pascal_case(),
91 field_name => field.name.to_pascal_case(),
92 go_type => type_mapper(&field.ty),
93 default => default_value_for_field(field, "go"),
94 }
95 })
96 .collect();
97
98 crate::template_env::render(
99 "config_gen/go_functional_options.jinja",
100 minijinja::context! {
101 type_name => typ.name.clone(),
102 fields => fields,
103 },
104 )
105 .trim_end_matches('\n')
106 .to_string()
107}
108
109pub fn gen_java_builder(typ: &TypeDef, package: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
112 let fields: Vec<_> = typ
113 .fields
114 .iter()
115 .map(|field| {
116 minijinja::context! {
117 name_lower => field.name.to_lowercase(),
118 type => type_mapper(&field.ty),
119 default => default_value_for_field(field, "java"),
120 method_name => format!("with{}", field.name.to_pascal_case()),
121 }
122 })
123 .collect();
124
125 crate::template_env::render(
126 "config_gen/java_builder.jinja",
127 minijinja::context! {
128 package => package,
129 type_name => typ.name.clone(),
130 fields => fields,
131 },
132 )
133 .trim_end_matches('\n')
134 .to_string()
135}
136
137pub fn gen_csharp_record(typ: &TypeDef, namespace: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
139 let fields: Vec<_> = typ
140 .fields
141 .iter()
142 .filter(|field| !is_tuple_field(field))
143 .map(|field| {
144 minijinja::context! {
145 type => type_mapper(&field.ty),
146 name_pascal => field.name.to_pascal_case(),
147 default => default_value_for_field(field, "csharp"),
148 }
149 })
150 .collect();
151
152 crate::template_env::render(
153 "config_gen/csharp_record.jinja",
154 minijinja::context! {
155 namespace => namespace,
156 type_name => typ.name.clone(),
157 fields => fields,
158 },
159 )
160 .trim_end_matches('\n')
161 .to_string()
162}
163
164pub fn default_value_for_field(field: &FieldDef, language: &str) -> String {
167 if let Some(typed_default) = &field.typed_default {
169 return match typed_default {
170 DefaultValue::BoolLiteral(b) => match language {
171 "python" => {
172 if *b {
173 "True".to_string()
174 } else {
175 "False".to_string()
176 }
177 }
178 "ruby" => {
179 if *b {
180 "true".to_string()
181 } else {
182 "false".to_string()
183 }
184 }
185 "go" => {
186 if *b {
187 "true".to_string()
188 } else {
189 "false".to_string()
190 }
191 }
192 "java" => {
193 if *b {
194 "true".to_string()
195 } else {
196 "false".to_string()
197 }
198 }
199 "csharp" => {
200 if *b {
201 "true".to_string()
202 } else {
203 "false".to_string()
204 }
205 }
206 "php" => {
207 if *b {
208 "true".to_string()
209 } else {
210 "false".to_string()
211 }
212 }
213 "r" => {
214 if *b {
215 "TRUE".to_string()
216 } else {
217 "FALSE".to_string()
218 }
219 }
220 "rust" => {
221 if *b {
222 "true".to_string()
223 } else {
224 "false".to_string()
225 }
226 }
227 _ => {
228 if *b {
229 "true".to_string()
230 } else {
231 "false".to_string()
232 }
233 }
234 },
235 DefaultValue::StringLiteral(s) => match language {
236 "rust" => format!("\"{}\".to_string()", s.replace('"', "\\\"")),
237 _ => format!("\"{}\"", s.replace('"', "\\\"")),
238 },
239 DefaultValue::IntLiteral(n) => n.to_string(),
240 DefaultValue::FloatLiteral(f) => {
241 let s = f.to_string();
242 if !s.contains('.') { format!("{}.0", s) } else { s }
243 }
244 DefaultValue::EnumVariant(v) => {
245 if matches!(field.ty, TypeRef::String) {
249 let snake = v.to_snake_case();
250 return match language {
251 "rust" => format!("\"{}\".to_string()", snake),
252 _ => format!("\"{}\"", snake),
253 };
254 }
255 match language {
256 "python" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
257 "ruby" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
258 "go" => format!("{}{}", field.ty.type_name(), v.to_pascal_case()),
259 "java" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
260 "csharp" => format!("{}.{}", field.ty.type_name(), v.to_pascal_case()),
261 "php" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
262 "r" => format!("{}${}", field.ty.type_name(), v.to_pascal_case()),
263 "rust" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
264 _ => v.clone(),
265 }
266 }
267 DefaultValue::Empty => {
268 match &field.ty {
270 TypeRef::Vec(_) => match language {
271 "python" | "ruby" | "csharp" => "[]".to_string(),
272 "go" => "nil".to_string(),
273 "java" => "List.of()".to_string(),
274 "php" => "[]".to_string(),
275 "r" => "c()".to_string(),
276 "rust" => "vec![]".to_string(),
277 _ => "null".to_string(),
278 },
279 TypeRef::Map(_, _) => match language {
280 "python" => "{}".to_string(),
281 "go" => "nil".to_string(),
282 "java" => "Map.of()".to_string(),
283 "rust" => "Default::default()".to_string(),
284 _ => "null".to_string(),
285 },
286 TypeRef::Primitive(p) => match p {
287 PrimitiveType::Bool => match language {
288 "python" => "False".to_string(),
289 "ruby" => "false".to_string(),
290 _ => "false".to_string(),
291 },
292 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
293 _ => "0".to_string(),
294 },
295 TypeRef::String | TypeRef::Char | TypeRef::Path => match language {
296 "rust" => "String::new()".to_string(),
297 _ => "\"\"".to_string(),
298 },
299 TypeRef::Json => match language {
300 "python" | "ruby" => "{}".to_string(),
301 "go" => "json.RawMessage(nil)".to_string(),
302 "java" => "new com.fasterxml.jackson.databind.node.ObjectNode(null)".to_string(),
303 "csharp" => "JObject.Parse(\"{}\")".to_string(),
304 "php" => "[]".to_string(),
305 "r" => "list()".to_string(),
306 "rust" => "serde_json::json!({})".to_string(),
307 _ => "{}".to_string(),
308 },
309 TypeRef::Duration => "0".to_string(),
310 TypeRef::Bytes => match language {
311 "python" => "b\"\"".to_string(),
312 "go" => "[]byte{}".to_string(),
313 "rust" => "vec![]".to_string(),
314 _ => "\"\"".to_string(),
315 },
316 _ => match language {
317 "python" => "None".to_string(),
318 "ruby" => "nil".to_string(),
319 "go" => "nil".to_string(),
320 "rust" => "Default::default()".to_string(),
321 _ => "null".to_string(),
322 },
323 }
324 }
325 DefaultValue::None => match language {
326 "python" => "None".to_string(),
327 "ruby" => "nil".to_string(),
328 "go" => "nil".to_string(),
329 "java" => "null".to_string(),
330 "csharp" => "null".to_string(),
331 "php" => "null".to_string(),
332 "r" => "NULL".to_string(),
333 "rust" => "None".to_string(),
334 _ => "null".to_string(),
335 },
336 };
337 }
338
339 if let Some(default_str) = &field.default {
341 return default_str.clone();
342 }
343
344 match &field.ty {
346 TypeRef::Primitive(p) => match p {
347 alef_core::ir::PrimitiveType::Bool => match language {
348 "python" => "False".to_string(),
349 "ruby" => "false".to_string(),
350 "csharp" => "false".to_string(),
351 "java" => "false".to_string(),
352 "php" => "false".to_string(),
353 "r" => "FALSE".to_string(),
354 _ => "false".to_string(),
355 },
356 alef_core::ir::PrimitiveType::U8
357 | alef_core::ir::PrimitiveType::U16
358 | alef_core::ir::PrimitiveType::U32
359 | alef_core::ir::PrimitiveType::U64
360 | alef_core::ir::PrimitiveType::I8
361 | alef_core::ir::PrimitiveType::I16
362 | alef_core::ir::PrimitiveType::I32
363 | alef_core::ir::PrimitiveType::I64
364 | alef_core::ir::PrimitiveType::Usize
365 | alef_core::ir::PrimitiveType::Isize => "0".to_string(),
366 alef_core::ir::PrimitiveType::F32 | alef_core::ir::PrimitiveType::F64 => "0.0".to_string(),
367 },
368 TypeRef::String | TypeRef::Char => match language {
369 "python" => "\"\"".to_string(),
370 "ruby" => "\"\"".to_string(),
371 "go" => "\"\"".to_string(),
372 "java" => "\"\"".to_string(),
373 "csharp" => "\"\"".to_string(),
374 "php" => "\"\"".to_string(),
375 "r" => "\"\"".to_string(),
376 "rust" => "String::new()".to_string(),
377 _ => "\"\"".to_string(),
378 },
379 TypeRef::Bytes => match language {
380 "python" => "b\"\"".to_string(),
381 "ruby" => "\"\"".to_string(),
382 "go" => "[]byte{}".to_string(),
383 "java" => "new byte[]{}".to_string(),
384 "csharp" => "new byte[]{}".to_string(),
385 "php" => "\"\"".to_string(),
386 "r" => "raw()".to_string(),
387 "rust" => "vec![]".to_string(),
388 _ => "[]".to_string(),
389 },
390 TypeRef::Optional(_) => match language {
391 "python" => "None".to_string(),
392 "ruby" => "nil".to_string(),
393 "go" => "nil".to_string(),
394 "java" => "null".to_string(),
395 "csharp" => "null".to_string(),
396 "php" => "null".to_string(),
397 "r" => "NULL".to_string(),
398 "rust" => "None".to_string(),
399 _ => "null".to_string(),
400 },
401 TypeRef::Vec(_) => match language {
402 "python" => "[]".to_string(),
403 "ruby" => "[]".to_string(),
404 "go" => "[]interface{}{}".to_string(),
405 "java" => "new java.util.ArrayList<>()".to_string(),
406 "csharp" => "[]".to_string(),
407 "php" => "[]".to_string(),
408 "r" => "c()".to_string(),
409 "rust" => "vec![]".to_string(),
410 _ => "[]".to_string(),
411 },
412 TypeRef::Map(_, _) => match language {
413 "python" => "{}".to_string(),
414 "ruby" => "{}".to_string(),
415 "go" => "make(map[string]interface{})".to_string(),
416 "java" => "new java.util.HashMap<>()".to_string(),
417 "csharp" => "new Dictionary<string, object>()".to_string(),
418 "php" => "[]".to_string(),
419 "r" => "list()".to_string(),
420 "rust" => "std::collections::HashMap::new()".to_string(),
421 _ => "{}".to_string(),
422 },
423 TypeRef::Json => match language {
424 "python" => "{}".to_string(),
425 "ruby" => "{}".to_string(),
426 "go" => "json.RawMessage(nil)".to_string(),
427 "java" => "new com.fasterxml.jackson.databind.JsonNode()".to_string(),
428 "csharp" => "JObject.Parse(\"{}\")".to_string(),
429 "php" => "[]".to_string(),
430 "r" => "list()".to_string(),
431 "rust" => "serde_json::json!({})".to_string(),
432 _ => "{}".to_string(),
433 },
434 TypeRef::Named(name) => match language {
435 "rust" => format!("{name}::default()"),
436 "python" => "None".to_string(),
437 "ruby" => "nil".to_string(),
438 "go" => "nil".to_string(),
439 "java" => "null".to_string(),
440 "csharp" => "null".to_string(),
441 "php" => "null".to_string(),
442 "r" => "NULL".to_string(),
443 _ => "null".to_string(),
444 },
445 _ => match language {
446 "python" => "None".to_string(),
447 "ruby" => "nil".to_string(),
448 "go" => "nil".to_string(),
449 "java" => "null".to_string(),
450 "csharp" => "null".to_string(),
451 "php" => "null".to_string(),
452 "r" => "NULL".to_string(),
453 "rust" => "Default::default()".to_string(),
454 _ => "null".to_string(),
455 },
456 }
457}
458
459trait TypeRefExt {
461 fn type_name(&self) -> String;
462}
463
464impl TypeRefExt for TypeRef {
465 fn type_name(&self) -> String {
466 match self {
467 TypeRef::Named(n) => n.clone(),
468 TypeRef::Primitive(p) => format!("{:?}", p),
469 TypeRef::String | TypeRef::Char => "String".to_string(),
470 TypeRef::Bytes => "Bytes".to_string(),
471 TypeRef::Optional(inner) => format!("Option<{}>", inner.type_name()),
472 TypeRef::Vec(inner) => format!("Vec<{}>", inner.type_name()),
473 TypeRef::Map(k, v) => format!("Map<{}, {}>", k.type_name(), v.type_name()),
474 TypeRef::Path => "Path".to_string(),
475 TypeRef::Unit => "()".to_string(),
476 TypeRef::Json => "Json".to_string(),
477 TypeRef::Duration => "Duration".to_string(),
478 }
479 }
480}
481
482const MAGNUS_MAX_ARITY: usize = 15;
484
485pub fn gen_magnus_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
491 let _ = MAGNUS_MAX_ARITY;
496 gen_magnus_hash_constructor(typ, type_mapper)
497}
498
499fn as_type_path_prefix(type_str: &str) -> String {
506 if type_str.contains('<') {
507 format!("<{type_str}>")
508 } else {
509 type_str.to_string()
510 }
511}
512
513fn gen_magnus_hash_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
516 let fields: Vec<_> = typ
517 .fields
518 .iter()
519 .map(|field| {
520 let is_optional = field_is_optional_in_rust(field);
521 let effective_inner_ty = match &field.ty {
525 TypeRef::Optional(inner) if is_optional => inner.as_ref(),
526 ty => ty,
527 };
528 let inner_type = type_mapper(effective_inner_ty);
529 let type_prefix = as_type_path_prefix(&inner_type);
530
531 let assignment = if is_optional {
532 format!(
534 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()),",
535 field.name, type_prefix
536 )
537 } else if use_unwrap_or_default(field) {
538 format!(
539 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).unwrap_or_default(),",
540 field.name, type_prefix
541 )
542 } else if matches!(effective_inner_ty, TypeRef::Named(_))
543 && !matches!(&field.typed_default, Some(DefaultValue::EnumVariant(_)))
544 {
545 format!(
554 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).ok_or_else(|| magnus::Error::new(unsafe {{ magnus::Ruby::get_unchecked() }}.exception_arg_error(), \"missing required field: {}\"))?,",
555 field.name, type_prefix, field.name
556 )
557 } else {
558 let default_str = if inner_type == "String" {
563 if let Some(DefaultValue::EnumVariant(variant)) = &field.typed_default {
564 use heck::ToSnakeCase;
565 format!("\"{}\".to_string()", variant.to_snake_case())
566 } else {
567 default_value_for_field(field, "rust")
568 }
569 } else {
570 default_value_for_field(field, "rust")
571 };
572 format!(
573 "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).unwrap_or({}),",
574 field.name, type_prefix, default_str
575 )
576 };
577
578 minijinja::context! {
579 name => field.name.clone(),
580 assignment => assignment,
581 }
582 })
583 .collect();
584
585 crate::template_env::render(
586 "config_gen/magnus_hash_constructor.jinja",
587 minijinja::context! {
588 fields => fields,
589 },
590 )
591}
592
593fn field_is_optional_in_rust(field: &FieldDef) -> bool {
598 field.optional || matches!(&field.ty, TypeRef::Optional(_))
599}
600
601#[allow(dead_code)]
608fn gen_magnus_positional_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
609 let fields: Vec<_> = typ
610 .fields
611 .iter()
612 .map(|field| {
613 let is_optional = field_is_optional_in_rust(field);
617 let param_type = if is_optional {
618 let effective_inner_ty = match &field.ty {
622 TypeRef::Optional(inner) => inner.as_ref(),
623 ty => ty,
624 };
625 let inner_type = type_mapper(effective_inner_ty);
626 format!("Option<{}>", inner_type)
627 } else {
628 let field_type = type_mapper(&field.ty);
629 format!("Option<{}>", field_type)
630 };
631
632 let assignment = if is_optional {
633 field.name.clone()
635 } else if use_unwrap_or_default(field) {
636 format!("{}.unwrap_or_default()", field.name)
637 } else {
638 let default_str = default_value_for_field(field, "rust");
639 format!("{}.unwrap_or({})", field.name, default_str)
640 };
641
642 minijinja::context! {
643 name => field.name.clone(),
644 param_type => param_type,
645 assignment => assignment,
646 }
647 })
648 .collect();
649
650 crate::template_env::render(
651 "config_gen/magnus_positional_constructor.jinja",
652 minijinja::context! {
653 fields => fields,
654 },
655 )
656}
657
658pub fn gen_php_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
662 let fields: Vec<_> = typ
663 .fields
664 .iter()
665 .map(|field| {
666 let mapped = type_mapper(&field.ty);
667 let is_optional_field = field.optional || matches!(&field.ty, TypeRef::Optional(_));
668
669 let assignment = if is_optional_field {
670 field.name.clone()
672 } else if use_unwrap_or_default(field) {
673 format!("{}.unwrap_or_default()", field.name)
675 } else {
676 let default_str = default_value_for_field(field, "rust");
678 format!("{}.unwrap_or({})", field.name, default_str)
679 };
680
681 minijinja::context! {
682 name => field.name.clone(),
683 ty => mapped,
684 assignment => assignment,
685 }
686 })
687 .collect();
688
689 crate::template_env::render(
690 "config_gen/php_kwargs_constructor.jinja",
691 minijinja::context! {
692 fields => fields,
693 },
694 )
695}
696
697pub fn gen_rustler_kwargs_constructor_with_exclude(
701 typ: &TypeDef,
702 _type_mapper: &dyn Fn(&TypeRef) -> String,
703 exclude_fields: &std::collections::HashSet<String>,
704) -> String {
705 let fields: Vec<_> = typ
707 .fields
708 .iter()
709 .filter(|f| !exclude_fields.contains(&f.name))
710 .map(|field| {
711 let assignment = if field.optional {
712 format!("opts.get(\"{}\").and_then(|t| t.decode().ok()),", field.name)
713 } else if use_unwrap_or_default(field) {
714 format!(
715 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
716 field.name
717 )
718 } else {
719 let default_str = default_value_for_field(field, "rust");
720 let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
721
722 if (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
723 || matches!(&field.ty, TypeRef::Named(_))
724 {
725 format!(
726 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
727 field.name
728 )
729 } else {
730 format!(
731 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
732 field.name, default_str
733 )
734 }
735 };
736
737 minijinja::context! {
738 name => field.name.clone(),
739 assignment => assignment,
740 }
741 })
742 .collect();
743
744 crate::template_env::render(
745 "config_gen/rustler_kwargs_constructor.jinja",
746 minijinja::context! {
747 fields => fields,
748 },
749 )
750}
751
752pub fn gen_rustler_kwargs_constructor(typ: &TypeDef, _type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
755 let fields: Vec<_> = typ
757 .fields
758 .iter()
759 .map(|field| {
760 let assignment = if field.optional {
761 format!("opts.get(\"{}\").and_then(|t| t.decode().ok()),", field.name)
762 } else if use_unwrap_or_default(field) {
763 format!(
764 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
765 field.name
766 )
767 } else {
768 let default_str = default_value_for_field(field, "rust");
769 let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");
770
771 let unwrap_default = (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
772 || matches!(&field.ty, TypeRef::Named(_));
773 if unwrap_default {
774 format!(
775 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
776 field.name
777 )
778 } else {
779 format!(
780 "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
781 field.name, default_str
782 )
783 }
784 };
785
786 minijinja::context! {
787 name => field.name.clone(),
788 assignment => assignment,
789 }
790 })
791 .collect();
792
793 crate::template_env::render(
794 "config_gen/rustler_kwargs_constructor.jinja",
795 minijinja::context! {
796 fields => fields,
797 },
798 )
799}
800
801pub fn gen_extendr_kwargs_constructor(
816 typ: &TypeDef,
817 type_mapper: &dyn Fn(&TypeRef) -> String,
818 enum_names: &ahash::AHashSet<String>,
819) -> String {
820 let is_named_enum = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if enum_names.contains(n.as_str())) };
822 let is_named_struct =
823 |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if !enum_names.contains(n.as_str())) };
824 let is_optional_named_struct = |ty: &TypeRef| -> bool {
825 if let TypeRef::Optional(inner) = ty {
826 is_named_struct(inner)
827 } else {
828 false
829 }
830 };
831 let ty_is_optional = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Optional(_)) };
832
833 let emittable_fields: Vec<_> = typ
835 .fields
836 .iter()
837 .filter(|f| f.cfg.is_none() && !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
838 .map(|field| {
839 let param_type = if is_named_enum(&field.ty) {
840 "Option<String>".to_string()
841 } else if ty_is_optional(&field.ty) {
842 type_mapper(&field.ty)
843 } else {
844 format!("Option<{}>", type_mapper(&field.ty))
845 };
846
847 minijinja::context! {
848 name => field.name.clone(),
849 type => param_type,
850 }
851 })
852 .collect();
853
854 let body_assignments: Vec<_> = typ
856 .fields
857 .iter()
858 .filter(|f| f.cfg.is_none() && !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
859 .map(|field| {
860 let code = if is_named_enum(&field.ty) {
861 if field.optional {
862 format!(
863 "if let Some(v) = {} {{ __out.{} = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")).ok(); }}",
864 field.name, field.name
865 )
866 } else {
867 format!(
868 "if let Some(v) = {} {{ if let Ok(parsed) = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")) {{ __out.{} = parsed; }} }}",
869 field.name, field.name
870 )
871 }
872 } else if ty_is_optional(&field.ty) || field.optional {
873 format!(
874 "if let Some(v) = {} {{ __out.{} = Some(v); }}",
875 field.name, field.name
876 )
877 } else {
878 format!(
879 "if let Some(v) = {} {{ __out.{} = v; }}",
880 field.name, field.name
881 )
882 };
883
884 minijinja::context! {
885 code => code,
886 }
887 })
888 .collect();
889
890 crate::template_env::render(
891 "config_gen/extendr_kwargs_constructor.jinja",
892 minijinja::context! {
893 type_name => typ.name.clone(),
894 type_name_lower => typ.name.to_lowercase(),
895 params => emittable_fields,
896 body_assignments => body_assignments,
897 },
898 )
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904 use alef_core::ir::{CoreWrapper, FieldDef, PrimitiveType, TypeRef};
905
906 fn make_test_type() -> TypeDef {
907 TypeDef {
908 name: "Config".to_string(),
909 rust_path: "my_crate::Config".to_string(),
910 original_rust_path: String::new(),
911 fields: vec![
912 FieldDef {
913 name: "timeout".to_string(),
914 ty: TypeRef::Primitive(PrimitiveType::U64),
915 optional: false,
916 default: Some("30".to_string()),
917 doc: "Timeout in seconds".to_string(),
918 sanitized: false,
919 is_boxed: false,
920 type_rust_path: None,
921 cfg: None,
922 typed_default: Some(DefaultValue::IntLiteral(30)),
923 core_wrapper: CoreWrapper::None,
924 vec_inner_core_wrapper: CoreWrapper::None,
925 newtype_wrapper: None,
926 serde_rename: None,
927 serde_flatten: false,
928 },
929 FieldDef {
930 name: "enabled".to_string(),
931 ty: TypeRef::Primitive(PrimitiveType::Bool),
932 optional: false,
933 default: None,
934 doc: "Enable feature".to_string(),
935 sanitized: false,
936 is_boxed: false,
937 type_rust_path: None,
938 cfg: None,
939 typed_default: Some(DefaultValue::BoolLiteral(true)),
940 core_wrapper: CoreWrapper::None,
941 vec_inner_core_wrapper: CoreWrapper::None,
942 newtype_wrapper: None,
943 serde_rename: None,
944 serde_flatten: false,
945 },
946 FieldDef {
947 name: "name".to_string(),
948 ty: TypeRef::String,
949 optional: false,
950 default: None,
951 doc: "Config name".to_string(),
952 sanitized: false,
953 is_boxed: false,
954 type_rust_path: None,
955 cfg: None,
956 typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
957 core_wrapper: CoreWrapper::None,
958 vec_inner_core_wrapper: CoreWrapper::None,
959 newtype_wrapper: None,
960 serde_rename: None,
961 serde_flatten: false,
962 },
963 ],
964 methods: vec![],
965 is_opaque: false,
966 is_clone: true,
967 is_copy: false,
968 doc: "Configuration type".to_string(),
969 cfg: None,
970 is_trait: false,
971 has_default: true,
972 has_stripped_cfg_fields: false,
973 is_return_type: false,
974 serde_rename_all: None,
975 has_serde: false,
976 super_traits: vec![],
977 }
978 }
979
980 #[test]
981 fn test_default_value_bool_true_python() {
982 let field = FieldDef {
983 name: "enabled".to_string(),
984 ty: TypeRef::Primitive(PrimitiveType::Bool),
985 optional: false,
986 default: None,
987 doc: String::new(),
988 sanitized: false,
989 is_boxed: false,
990 type_rust_path: None,
991 cfg: None,
992 typed_default: Some(DefaultValue::BoolLiteral(true)),
993 core_wrapper: CoreWrapper::None,
994 vec_inner_core_wrapper: CoreWrapper::None,
995 newtype_wrapper: None,
996 serde_rename: None,
997 serde_flatten: false,
998 };
999 assert_eq!(default_value_for_field(&field, "python"), "True");
1000 }
1001
1002 #[test]
1003 fn test_default_value_bool_false_go() {
1004 let field = FieldDef {
1005 name: "enabled".to_string(),
1006 ty: TypeRef::Primitive(PrimitiveType::Bool),
1007 optional: false,
1008 default: None,
1009 doc: String::new(),
1010 sanitized: false,
1011 is_boxed: false,
1012 type_rust_path: None,
1013 cfg: None,
1014 typed_default: Some(DefaultValue::BoolLiteral(false)),
1015 core_wrapper: CoreWrapper::None,
1016 vec_inner_core_wrapper: CoreWrapper::None,
1017 newtype_wrapper: None,
1018 serde_rename: None,
1019 serde_flatten: false,
1020 };
1021 assert_eq!(default_value_for_field(&field, "go"), "false");
1022 }
1023
1024 #[test]
1025 fn test_default_value_string_literal() {
1026 let field = FieldDef {
1027 name: "name".to_string(),
1028 ty: TypeRef::String,
1029 optional: false,
1030 default: None,
1031 doc: String::new(),
1032 sanitized: false,
1033 is_boxed: false,
1034 type_rust_path: None,
1035 cfg: None,
1036 typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1037 core_wrapper: CoreWrapper::None,
1038 vec_inner_core_wrapper: CoreWrapper::None,
1039 newtype_wrapper: None,
1040 serde_rename: None,
1041 serde_flatten: false,
1042 };
1043 assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
1044 assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
1045 }
1046
1047 #[test]
1048 fn test_default_value_int_literal() {
1049 let field = FieldDef {
1050 name: "timeout".to_string(),
1051 ty: TypeRef::Primitive(PrimitiveType::U64),
1052 optional: false,
1053 default: None,
1054 doc: String::new(),
1055 sanitized: false,
1056 is_boxed: false,
1057 type_rust_path: None,
1058 cfg: None,
1059 typed_default: Some(DefaultValue::IntLiteral(42)),
1060 core_wrapper: CoreWrapper::None,
1061 vec_inner_core_wrapper: CoreWrapper::None,
1062 newtype_wrapper: None,
1063 serde_rename: None,
1064 serde_flatten: false,
1065 };
1066 let result = default_value_for_field(&field, "python");
1067 assert_eq!(result, "42");
1068 }
1069
1070 #[test]
1071 fn test_default_value_none() {
1072 let field = FieldDef {
1073 name: "maybe".to_string(),
1074 ty: TypeRef::Optional(Box::new(TypeRef::String)),
1075 optional: true,
1076 default: None,
1077 doc: String::new(),
1078 sanitized: false,
1079 is_boxed: false,
1080 type_rust_path: None,
1081 cfg: None,
1082 typed_default: Some(DefaultValue::None),
1083 core_wrapper: CoreWrapper::None,
1084 vec_inner_core_wrapper: CoreWrapper::None,
1085 newtype_wrapper: None,
1086 serde_rename: None,
1087 serde_flatten: false,
1088 };
1089 assert_eq!(default_value_for_field(&field, "python"), "None");
1090 assert_eq!(default_value_for_field(&field, "go"), "nil");
1091 assert_eq!(default_value_for_field(&field, "java"), "null");
1092 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1093 }
1094
1095 #[test]
1096 fn test_default_value_fallback_string() {
1097 let field = FieldDef {
1098 name: "name".to_string(),
1099 ty: TypeRef::String,
1100 optional: false,
1101 default: Some("\"custom\"".to_string()),
1102 doc: String::new(),
1103 sanitized: false,
1104 is_boxed: false,
1105 type_rust_path: None,
1106 cfg: None,
1107 typed_default: None,
1108 core_wrapper: CoreWrapper::None,
1109 vec_inner_core_wrapper: CoreWrapper::None,
1110 newtype_wrapper: None,
1111 serde_rename: None,
1112 serde_flatten: false,
1113 };
1114 assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1115 }
1116
1117 #[test]
1118 fn test_gen_pyo3_kwargs_constructor() {
1119 let typ = make_test_type();
1120 let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1121 TypeRef::Primitive(p) => format!("{:?}", p),
1122 TypeRef::String | TypeRef::Char => "str".to_string(),
1123 _ => "Any".to_string(),
1124 });
1125
1126 assert!(output.contains("#[new]"));
1127 assert!(output.contains("#[pyo3(signature = ("));
1128 assert!(output.contains("timeout=30"));
1129 assert!(output.contains("enabled=True"));
1130 assert!(output.contains("name=\"default\""));
1131 assert!(output.contains("fn new("));
1132 }
1133
1134 #[test]
1135 fn test_gen_napi_defaults_constructor() {
1136 let typ = make_test_type();
1137 let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1138 TypeRef::Primitive(p) => format!("{:?}", p),
1139 TypeRef::String | TypeRef::Char => "String".to_string(),
1140 _ => "Value".to_string(),
1141 });
1142
1143 assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1144 assert!(output.contains("timeout"));
1145 assert!(output.contains("enabled"));
1146 assert!(output.contains("name"));
1147 }
1148
1149 #[test]
1150 fn test_gen_go_functional_options() {
1151 let typ = make_test_type();
1152 let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1153 TypeRef::Primitive(p) => match p {
1154 PrimitiveType::U64 => "uint64".to_string(),
1155 PrimitiveType::Bool => "bool".to_string(),
1156 _ => "interface{}".to_string(),
1157 },
1158 TypeRef::String | TypeRef::Char => "string".to_string(),
1159 _ => "interface{}".to_string(),
1160 });
1161
1162 assert!(output.contains("type Config struct {"));
1163 assert!(output.contains("type ConfigOption func(*Config)"));
1164 assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1165 assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1166 assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1167 assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1168 }
1169
1170 #[test]
1171 fn test_gen_java_builder() {
1172 let typ = make_test_type();
1173 let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1174 TypeRef::Primitive(p) => match p {
1175 PrimitiveType::U64 => "long".to_string(),
1176 PrimitiveType::Bool => "boolean".to_string(),
1177 _ => "int".to_string(),
1178 },
1179 TypeRef::String | TypeRef::Char => "String".to_string(),
1180 _ => "Object".to_string(),
1181 });
1182
1183 assert!(output.contains("package dev.test;"));
1184 assert!(output.contains("public class ConfigBuilder"));
1185 assert!(output.contains("withTimeout"));
1186 assert!(output.contains("withEnabled"));
1187 assert!(output.contains("withName"));
1188 assert!(output.contains("public Config build()"));
1189 }
1190
1191 #[test]
1192 fn test_gen_csharp_record() {
1193 let typ = make_test_type();
1194 let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1195 TypeRef::Primitive(p) => match p {
1196 PrimitiveType::U64 => "ulong".to_string(),
1197 PrimitiveType::Bool => "bool".to_string(),
1198 _ => "int".to_string(),
1199 },
1200 TypeRef::String | TypeRef::Char => "string".to_string(),
1201 _ => "object".to_string(),
1202 });
1203
1204 assert!(output.contains("namespace MyNamespace;"));
1205 assert!(output.contains("public record Config"));
1206 assert!(output.contains("public ulong Timeout"));
1207 assert!(output.contains("public bool Enabled"));
1208 assert!(output.contains("public string Name"));
1209 assert!(output.contains("init;"));
1210 }
1211
1212 #[test]
1213 fn test_default_value_float_literal() {
1214 let field = FieldDef {
1215 name: "ratio".to_string(),
1216 ty: TypeRef::Primitive(PrimitiveType::F64),
1217 optional: false,
1218 default: None,
1219 doc: String::new(),
1220 sanitized: false,
1221 is_boxed: false,
1222 type_rust_path: None,
1223 cfg: None,
1224 typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1225 core_wrapper: CoreWrapper::None,
1226 vec_inner_core_wrapper: CoreWrapper::None,
1227 newtype_wrapper: None,
1228 serde_rename: None,
1229 serde_flatten: false,
1230 };
1231 let result = default_value_for_field(&field, "python");
1232 assert!(result.contains("1.5"));
1233 }
1234
1235 #[test]
1236 fn test_default_value_no_typed_no_default() {
1237 let field = FieldDef {
1238 name: "count".to_string(),
1239 ty: TypeRef::Primitive(PrimitiveType::U32),
1240 optional: false,
1241 default: None,
1242 doc: String::new(),
1243 sanitized: false,
1244 is_boxed: false,
1245 type_rust_path: None,
1246 cfg: None,
1247 typed_default: None,
1248 core_wrapper: CoreWrapper::None,
1249 vec_inner_core_wrapper: CoreWrapper::None,
1250 newtype_wrapper: None,
1251 serde_rename: None,
1252 serde_flatten: false,
1253 };
1254 assert_eq!(default_value_for_field(&field, "python"), "0");
1256 assert_eq!(default_value_for_field(&field, "go"), "0");
1257 }
1258
1259 fn make_field(name: &str, ty: TypeRef) -> FieldDef {
1260 FieldDef {
1261 name: name.to_string(),
1262 ty,
1263 optional: false,
1264 default: None,
1265 doc: String::new(),
1266 sanitized: false,
1267 is_boxed: false,
1268 type_rust_path: None,
1269 cfg: None,
1270 typed_default: None,
1271 core_wrapper: CoreWrapper::None,
1272 vec_inner_core_wrapper: CoreWrapper::None,
1273 newtype_wrapper: None,
1274 serde_rename: None,
1275 serde_flatten: false,
1276 }
1277 }
1278
1279 fn simple_type_mapper(tr: &TypeRef) -> String {
1280 match tr {
1281 TypeRef::Primitive(p) => match p {
1282 PrimitiveType::U64 => "u64".to_string(),
1283 PrimitiveType::Bool => "bool".to_string(),
1284 PrimitiveType::U32 => "u32".to_string(),
1285 _ => "i64".to_string(),
1286 },
1287 TypeRef::String | TypeRef::Char => "String".to_string(),
1288 TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
1289 TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
1290 TypeRef::Named(n) => n.clone(),
1291 _ => "Value".to_string(),
1292 }
1293 }
1294
1295 #[test]
1300 fn test_default_value_bool_literal_ruby() {
1301 let field = FieldDef {
1302 name: "flag".to_string(),
1303 ty: TypeRef::Primitive(PrimitiveType::Bool),
1304 optional: false,
1305 default: None,
1306 doc: String::new(),
1307 sanitized: false,
1308 is_boxed: false,
1309 type_rust_path: None,
1310 cfg: None,
1311 typed_default: Some(DefaultValue::BoolLiteral(true)),
1312 core_wrapper: CoreWrapper::None,
1313 vec_inner_core_wrapper: CoreWrapper::None,
1314 newtype_wrapper: None,
1315 serde_rename: None,
1316 serde_flatten: false,
1317 };
1318 assert_eq!(default_value_for_field(&field, "ruby"), "true");
1319 assert_eq!(default_value_for_field(&field, "php"), "true");
1320 assert_eq!(default_value_for_field(&field, "csharp"), "true");
1321 assert_eq!(default_value_for_field(&field, "java"), "true");
1322 assert_eq!(default_value_for_field(&field, "rust"), "true");
1323 }
1324
1325 #[test]
1326 fn test_default_value_bool_literal_r() {
1327 let field = FieldDef {
1328 name: "flag".to_string(),
1329 ty: TypeRef::Primitive(PrimitiveType::Bool),
1330 optional: false,
1331 default: None,
1332 doc: String::new(),
1333 sanitized: false,
1334 is_boxed: false,
1335 type_rust_path: None,
1336 cfg: None,
1337 typed_default: Some(DefaultValue::BoolLiteral(false)),
1338 core_wrapper: CoreWrapper::None,
1339 vec_inner_core_wrapper: CoreWrapper::None,
1340 newtype_wrapper: None,
1341 serde_rename: None,
1342 serde_flatten: false,
1343 };
1344 assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1345 }
1346
1347 #[test]
1348 fn test_default_value_string_literal_rust() {
1349 let field = FieldDef {
1350 name: "label".to_string(),
1351 ty: TypeRef::String,
1352 optional: false,
1353 default: None,
1354 doc: String::new(),
1355 sanitized: false,
1356 is_boxed: false,
1357 type_rust_path: None,
1358 cfg: None,
1359 typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1360 core_wrapper: CoreWrapper::None,
1361 vec_inner_core_wrapper: CoreWrapper::None,
1362 newtype_wrapper: None,
1363 serde_rename: None,
1364 serde_flatten: false,
1365 };
1366 assert_eq!(default_value_for_field(&field, "rust"), "\"hello\".to_string()");
1367 }
1368
1369 #[test]
1370 fn test_default_value_string_literal_escapes_quotes() {
1371 let field = FieldDef {
1372 name: "label".to_string(),
1373 ty: TypeRef::String,
1374 optional: false,
1375 default: None,
1376 doc: String::new(),
1377 sanitized: false,
1378 is_boxed: false,
1379 type_rust_path: None,
1380 cfg: None,
1381 typed_default: Some(DefaultValue::StringLiteral("say \"hi\"".to_string())),
1382 core_wrapper: CoreWrapper::None,
1383 vec_inner_core_wrapper: CoreWrapper::None,
1384 newtype_wrapper: None,
1385 serde_rename: None,
1386 serde_flatten: false,
1387 };
1388 assert_eq!(default_value_for_field(&field, "python"), "\"say \\\"hi\\\"\"");
1389 }
1390
1391 #[test]
1392 fn test_default_value_float_literal_whole_number() {
1393 let field = FieldDef {
1395 name: "scale".to_string(),
1396 ty: TypeRef::Primitive(PrimitiveType::F32),
1397 optional: false,
1398 default: None,
1399 doc: String::new(),
1400 sanitized: false,
1401 is_boxed: false,
1402 type_rust_path: None,
1403 cfg: None,
1404 typed_default: Some(DefaultValue::FloatLiteral(2.0)),
1405 core_wrapper: CoreWrapper::None,
1406 vec_inner_core_wrapper: CoreWrapper::None,
1407 newtype_wrapper: None,
1408 serde_rename: None,
1409 serde_flatten: false,
1410 };
1411 let result = default_value_for_field(&field, "python");
1412 assert!(result.contains('.'), "whole-number float should contain '.': {result}");
1413 }
1414
1415 #[test]
1416 fn test_default_value_enum_variant_per_language() {
1417 let field = FieldDef {
1418 name: "format".to_string(),
1419 ty: TypeRef::Named("OutputFormat".to_string()),
1420 optional: false,
1421 default: None,
1422 doc: String::new(),
1423 sanitized: false,
1424 is_boxed: false,
1425 type_rust_path: None,
1426 cfg: None,
1427 typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
1428 core_wrapper: CoreWrapper::None,
1429 vec_inner_core_wrapper: CoreWrapper::None,
1430 newtype_wrapper: None,
1431 serde_rename: None,
1432 serde_flatten: false,
1433 };
1434 assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
1435 assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
1436 assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
1437 assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
1438 assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
1439 assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
1440 assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
1441 assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
1442 }
1443
1444 #[test]
1445 fn test_default_value_empty_vec_per_language() {
1446 let field = FieldDef {
1447 name: "items".to_string(),
1448 ty: TypeRef::Vec(Box::new(TypeRef::String)),
1449 optional: false,
1450 default: None,
1451 doc: String::new(),
1452 sanitized: false,
1453 is_boxed: false,
1454 type_rust_path: None,
1455 cfg: None,
1456 typed_default: Some(DefaultValue::Empty),
1457 core_wrapper: CoreWrapper::None,
1458 vec_inner_core_wrapper: CoreWrapper::None,
1459 newtype_wrapper: None,
1460 serde_rename: None,
1461 serde_flatten: false,
1462 };
1463 assert_eq!(default_value_for_field(&field, "python"), "[]");
1464 assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1465 assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1466 assert_eq!(default_value_for_field(&field, "go"), "nil");
1467 assert_eq!(default_value_for_field(&field, "java"), "List.of()");
1468 assert_eq!(default_value_for_field(&field, "php"), "[]");
1469 assert_eq!(default_value_for_field(&field, "r"), "c()");
1470 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1471 }
1472
1473 #[test]
1474 fn test_default_value_empty_map_per_language() {
1475 let field = FieldDef {
1476 name: "meta".to_string(),
1477 ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1478 optional: false,
1479 default: None,
1480 doc: String::new(),
1481 sanitized: false,
1482 is_boxed: false,
1483 type_rust_path: None,
1484 cfg: None,
1485 typed_default: Some(DefaultValue::Empty),
1486 core_wrapper: CoreWrapper::None,
1487 vec_inner_core_wrapper: CoreWrapper::None,
1488 newtype_wrapper: None,
1489 serde_rename: None,
1490 serde_flatten: false,
1491 };
1492 assert_eq!(default_value_for_field(&field, "python"), "{}");
1493 assert_eq!(default_value_for_field(&field, "go"), "nil");
1494 assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
1495 assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1496 }
1497
1498 #[test]
1499 fn test_default_value_empty_bool_primitive() {
1500 let field = FieldDef {
1501 name: "flag".to_string(),
1502 ty: TypeRef::Primitive(PrimitiveType::Bool),
1503 optional: false,
1504 default: None,
1505 doc: String::new(),
1506 sanitized: false,
1507 is_boxed: false,
1508 type_rust_path: None,
1509 cfg: None,
1510 typed_default: Some(DefaultValue::Empty),
1511 core_wrapper: CoreWrapper::None,
1512 vec_inner_core_wrapper: CoreWrapper::None,
1513 newtype_wrapper: None,
1514 serde_rename: None,
1515 serde_flatten: false,
1516 };
1517 assert_eq!(default_value_for_field(&field, "python"), "False");
1518 assert_eq!(default_value_for_field(&field, "ruby"), "false");
1519 assert_eq!(default_value_for_field(&field, "go"), "false");
1520 }
1521
1522 #[test]
1523 fn test_default_value_empty_float_primitive() {
1524 let field = FieldDef {
1525 name: "ratio".to_string(),
1526 ty: TypeRef::Primitive(PrimitiveType::F64),
1527 optional: false,
1528 default: None,
1529 doc: String::new(),
1530 sanitized: false,
1531 is_boxed: false,
1532 type_rust_path: None,
1533 cfg: None,
1534 typed_default: Some(DefaultValue::Empty),
1535 core_wrapper: CoreWrapper::None,
1536 vec_inner_core_wrapper: CoreWrapper::None,
1537 newtype_wrapper: None,
1538 serde_rename: None,
1539 serde_flatten: false,
1540 };
1541 assert_eq!(default_value_for_field(&field, "python"), "0.0");
1542 }
1543
1544 #[test]
1545 fn test_default_value_empty_string_type() {
1546 let field = FieldDef {
1547 name: "label".to_string(),
1548 ty: TypeRef::String,
1549 optional: false,
1550 default: None,
1551 doc: String::new(),
1552 sanitized: false,
1553 is_boxed: false,
1554 type_rust_path: None,
1555 cfg: None,
1556 typed_default: Some(DefaultValue::Empty),
1557 core_wrapper: CoreWrapper::None,
1558 vec_inner_core_wrapper: CoreWrapper::None,
1559 newtype_wrapper: None,
1560 serde_rename: None,
1561 serde_flatten: false,
1562 };
1563 assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1564 assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1565 }
1566
1567 #[test]
1568 fn test_default_value_empty_bytes_type() {
1569 let field = FieldDef {
1570 name: "data".to_string(),
1571 ty: TypeRef::Bytes,
1572 optional: false,
1573 default: None,
1574 doc: String::new(),
1575 sanitized: false,
1576 is_boxed: false,
1577 type_rust_path: None,
1578 cfg: None,
1579 typed_default: Some(DefaultValue::Empty),
1580 core_wrapper: CoreWrapper::None,
1581 vec_inner_core_wrapper: CoreWrapper::None,
1582 newtype_wrapper: None,
1583 serde_rename: None,
1584 serde_flatten: false,
1585 };
1586 assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1587 assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1588 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1589 }
1590
1591 #[test]
1592 fn test_default_value_empty_json_type() {
1593 let field = FieldDef {
1594 name: "payload".to_string(),
1595 ty: TypeRef::Json,
1596 optional: false,
1597 default: None,
1598 doc: String::new(),
1599 sanitized: false,
1600 is_boxed: false,
1601 type_rust_path: None,
1602 cfg: None,
1603 typed_default: Some(DefaultValue::Empty),
1604 core_wrapper: CoreWrapper::None,
1605 vec_inner_core_wrapper: CoreWrapper::None,
1606 newtype_wrapper: None,
1607 serde_rename: None,
1608 serde_flatten: false,
1609 };
1610 assert_eq!(default_value_for_field(&field, "python"), "{}");
1611 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1612 assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1613 assert_eq!(default_value_for_field(&field, "r"), "list()");
1614 assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1615 }
1616
1617 #[test]
1618 fn test_default_value_none_ruby_php_r() {
1619 let field = FieldDef {
1620 name: "maybe".to_string(),
1621 ty: TypeRef::Optional(Box::new(TypeRef::String)),
1622 optional: true,
1623 default: None,
1624 doc: String::new(),
1625 sanitized: false,
1626 is_boxed: false,
1627 type_rust_path: None,
1628 cfg: None,
1629 typed_default: Some(DefaultValue::None),
1630 core_wrapper: CoreWrapper::None,
1631 vec_inner_core_wrapper: CoreWrapper::None,
1632 newtype_wrapper: None,
1633 serde_rename: None,
1634 serde_flatten: false,
1635 };
1636 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1637 assert_eq!(default_value_for_field(&field, "php"), "null");
1638 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1639 assert_eq!(default_value_for_field(&field, "rust"), "None");
1640 }
1641
1642 #[test]
1647 fn test_default_value_fallback_bool_all_languages() {
1648 let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
1649 assert_eq!(default_value_for_field(&field, "python"), "False");
1650 assert_eq!(default_value_for_field(&field, "ruby"), "false");
1651 assert_eq!(default_value_for_field(&field, "csharp"), "false");
1652 assert_eq!(default_value_for_field(&field, "java"), "false");
1653 assert_eq!(default_value_for_field(&field, "php"), "false");
1654 assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1655 assert_eq!(default_value_for_field(&field, "rust"), "false");
1656 }
1657
1658 #[test]
1659 fn test_default_value_fallback_float() {
1660 let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
1661 assert_eq!(default_value_for_field(&field, "python"), "0.0");
1662 assert_eq!(default_value_for_field(&field, "rust"), "0.0");
1663 }
1664
1665 #[test]
1666 fn test_default_value_fallback_string_all_languages() {
1667 let field = make_field("name", TypeRef::String);
1668 assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1669 assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1670 assert_eq!(default_value_for_field(&field, "go"), "\"\"");
1671 assert_eq!(default_value_for_field(&field, "java"), "\"\"");
1672 assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
1673 assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1674 assert_eq!(default_value_for_field(&field, "r"), "\"\"");
1675 assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1676 }
1677
1678 #[test]
1679 fn test_default_value_fallback_bytes_all_languages() {
1680 let field = make_field("data", TypeRef::Bytes);
1681 assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1682 assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1683 assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1684 assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
1685 assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
1686 assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1687 assert_eq!(default_value_for_field(&field, "r"), "raw()");
1688 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1689 }
1690
1691 #[test]
1692 fn test_default_value_fallback_optional() {
1693 let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
1694 assert_eq!(default_value_for_field(&field, "python"), "None");
1695 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1696 assert_eq!(default_value_for_field(&field, "go"), "nil");
1697 assert_eq!(default_value_for_field(&field, "java"), "null");
1698 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1699 assert_eq!(default_value_for_field(&field, "php"), "null");
1700 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1701 assert_eq!(default_value_for_field(&field, "rust"), "None");
1702 }
1703
1704 #[test]
1705 fn test_default_value_fallback_vec_all_languages() {
1706 let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
1707 assert_eq!(default_value_for_field(&field, "python"), "[]");
1708 assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1709 assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
1710 assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
1711 assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1712 assert_eq!(default_value_for_field(&field, "php"), "[]");
1713 assert_eq!(default_value_for_field(&field, "r"), "c()");
1714 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1715 }
1716
1717 #[test]
1718 fn test_default_value_fallback_map_all_languages() {
1719 let field = make_field(
1720 "meta",
1721 TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1722 );
1723 assert_eq!(default_value_for_field(&field, "python"), "{}");
1724 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1725 assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
1726 assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
1727 assert_eq!(
1728 default_value_for_field(&field, "csharp"),
1729 "new Dictionary<string, object>()"
1730 );
1731 assert_eq!(default_value_for_field(&field, "php"), "[]");
1732 assert_eq!(default_value_for_field(&field, "r"), "list()");
1733 assert_eq!(
1734 default_value_for_field(&field, "rust"),
1735 "std::collections::HashMap::new()"
1736 );
1737 }
1738
1739 #[test]
1740 fn test_default_value_fallback_json_all_languages() {
1741 let field = make_field("payload", TypeRef::Json);
1742 assert_eq!(default_value_for_field(&field, "python"), "{}");
1743 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1744 assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1745 assert_eq!(default_value_for_field(&field, "r"), "list()");
1746 assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1747 }
1748
1749 #[test]
1750 fn test_default_value_fallback_named_type() {
1751 let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
1752 assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
1753 assert_eq!(default_value_for_field(&field, "python"), "None");
1754 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1755 assert_eq!(default_value_for_field(&field, "go"), "nil");
1756 assert_eq!(default_value_for_field(&field, "java"), "null");
1757 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1758 assert_eq!(default_value_for_field(&field, "php"), "null");
1759 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1760 }
1761
1762 #[test]
1763 fn test_default_value_fallback_duration() {
1764 let field = make_field("timeout", TypeRef::Duration);
1766 assert_eq!(default_value_for_field(&field, "python"), "None");
1767 assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1768 }
1769
1770 #[test]
1775 fn test_gen_magnus_kwargs_constructor_positional_basic() {
1776 let typ = make_test_type();
1777 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1778
1779 assert!(output.contains("fn new("), "should have fn new");
1780 assert!(output.contains("Option<u64>"), "timeout should be Option<u64>");
1782 assert!(output.contains("Option<bool>"), "enabled should be Option<bool>");
1783 assert!(output.contains("Option<String>"), "name should be Option<String>");
1784 assert!(output.contains("-> Self {"), "should return Self");
1785 assert!(
1787 output.contains("timeout: timeout.unwrap_or(30),"),
1788 "should apply int default"
1789 );
1790 assert!(
1792 output.contains("enabled: enabled.unwrap_or(true),"),
1793 "should apply bool default"
1794 );
1795 assert!(
1797 output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1798 "should apply string default"
1799 );
1800 }
1801
1802 #[test]
1803 fn test_gen_magnus_kwargs_constructor_positional_optional_field() {
1804 let mut typ = make_test_type();
1806 typ.fields.push(FieldDef {
1807 name: "extra".to_string(),
1808 ty: TypeRef::String,
1809 optional: true,
1810 default: None,
1811 doc: String::new(),
1812 sanitized: false,
1813 is_boxed: false,
1814 type_rust_path: None,
1815 cfg: None,
1816 typed_default: None,
1817 core_wrapper: CoreWrapper::None,
1818 vec_inner_core_wrapper: CoreWrapper::None,
1819 newtype_wrapper: None,
1820 serde_rename: None,
1821 serde_flatten: false,
1822 });
1823 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1824 assert!(output.contains("extra,"), "optional field should be assigned directly");
1826 assert!(!output.contains("extra.unwrap"), "optional field should not use unwrap");
1827 }
1828
1829 #[test]
1830 fn test_gen_magnus_kwargs_constructor_unwrap_or_default() {
1831 let mut typ = make_test_type();
1833 typ.fields.push(FieldDef {
1834 name: "count".to_string(),
1835 ty: TypeRef::Primitive(PrimitiveType::U32),
1836 optional: false,
1837 default: None,
1838 doc: String::new(),
1839 sanitized: false,
1840 is_boxed: false,
1841 type_rust_path: None,
1842 cfg: None,
1843 typed_default: None,
1844 core_wrapper: CoreWrapper::None,
1845 vec_inner_core_wrapper: CoreWrapper::None,
1846 newtype_wrapper: None,
1847 serde_rename: None,
1848 serde_flatten: false,
1849 });
1850 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1851 assert!(
1852 output.contains("count: count.unwrap_or_default(),"),
1853 "plain primitive with no default should use unwrap_or_default"
1854 );
1855 }
1856
1857 #[test]
1858 fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
1859 let mut fields: Vec<FieldDef> = (0..16)
1861 .map(|i| FieldDef {
1862 name: format!("field_{i}"),
1863 ty: TypeRef::Primitive(PrimitiveType::U32),
1864 optional: false,
1865 default: None,
1866 doc: String::new(),
1867 sanitized: false,
1868 is_boxed: false,
1869 type_rust_path: None,
1870 cfg: None,
1871 typed_default: None,
1872 core_wrapper: CoreWrapper::None,
1873 vec_inner_core_wrapper: CoreWrapper::None,
1874 newtype_wrapper: None,
1875 serde_rename: None,
1876 serde_flatten: false,
1877 })
1878 .collect();
1879 fields[0].optional = true;
1881
1882 let typ = TypeDef {
1883 name: "BigConfig".to_string(),
1884 rust_path: "crate::BigConfig".to_string(),
1885 original_rust_path: String::new(),
1886 fields,
1887 methods: vec![],
1888 is_opaque: false,
1889 is_clone: true,
1890 is_copy: false,
1891 doc: String::new(),
1892 cfg: None,
1893 is_trait: false,
1894 has_default: true,
1895 has_stripped_cfg_fields: false,
1896 is_return_type: false,
1897 serde_rename_all: None,
1898 has_serde: false,
1899 super_traits: vec![],
1900 };
1901 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1902
1903 assert!(
1904 output.contains("Option<magnus::RHash>"),
1905 "should accept RHash via scan_args"
1906 );
1907 assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
1908 assert!(
1910 output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
1911 "optional field should use and_then"
1912 );
1913 assert!(
1914 output.contains("field_0:").then_some(()).is_some(),
1915 "field_0 should appear in output"
1916 );
1917 }
1918
1919 #[test]
1924 fn test_gen_php_kwargs_constructor_basic() {
1925 let typ = make_test_type();
1926 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1927
1928 assert!(
1929 output.contains("pub fn __construct("),
1930 "should use PHP constructor name"
1931 );
1932 assert!(
1934 output.contains("timeout: Option<u64>"),
1935 "timeout param should be Option<u64>"
1936 );
1937 assert!(
1938 output.contains("enabled: Option<bool>"),
1939 "enabled param should be Option<bool>"
1940 );
1941 assert!(
1942 output.contains("name: Option<String>"),
1943 "name param should be Option<String>"
1944 );
1945 assert!(output.contains("-> Self {"), "should return Self");
1946 assert!(
1947 output.contains("timeout: timeout.unwrap_or(30),"),
1948 "should apply int default for timeout"
1949 );
1950 assert!(
1951 output.contains("enabled: enabled.unwrap_or(true),"),
1952 "should apply bool default for enabled"
1953 );
1954 assert!(
1955 output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1956 "should apply string default for name"
1957 );
1958 }
1959
1960 #[test]
1961 fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
1962 let mut typ = make_test_type();
1963 typ.fields.push(FieldDef {
1964 name: "tag".to_string(),
1965 ty: TypeRef::String,
1966 optional: true,
1967 default: None,
1968 doc: String::new(),
1969 sanitized: false,
1970 is_boxed: false,
1971 type_rust_path: None,
1972 cfg: None,
1973 typed_default: None,
1974 core_wrapper: CoreWrapper::None,
1975 vec_inner_core_wrapper: CoreWrapper::None,
1976 newtype_wrapper: None,
1977 serde_rename: None,
1978 serde_flatten: false,
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 serde_rename: None,
2006 serde_flatten: false,
2007 });
2008 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2009 assert!(
2010 output.contains("retries: retries.unwrap_or_default(),"),
2011 "primitive with no default should use unwrap_or_default"
2012 );
2013 }
2014
2015 #[test]
2020 fn test_gen_rustler_kwargs_constructor_basic() {
2021 let typ = make_test_type();
2022 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2023
2024 assert!(
2025 output.contains("pub fn new(opts: std::collections::HashMap<String, rustler::Term>)"),
2026 "should accept HashMap of Terms"
2027 );
2028 assert!(output.contains("Self {"), "should construct Self");
2029 assert!(
2031 output.contains("timeout: opts.get(\"timeout\").and_then(|t| t.decode().ok()).unwrap_or(30),"),
2032 "should apply int default for timeout"
2033 );
2034 assert!(
2036 output.contains("enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),"),
2037 "should apply bool default for enabled"
2038 );
2039 }
2040
2041 #[test]
2042 fn test_gen_rustler_kwargs_constructor_optional_field() {
2043 let mut typ = make_test_type();
2044 typ.fields.push(FieldDef {
2045 name: "extra".to_string(),
2046 ty: TypeRef::String,
2047 optional: true,
2048 default: None,
2049 doc: String::new(),
2050 sanitized: false,
2051 is_boxed: false,
2052 type_rust_path: None,
2053 cfg: None,
2054 typed_default: None,
2055 core_wrapper: CoreWrapper::None,
2056 vec_inner_core_wrapper: CoreWrapper::None,
2057 newtype_wrapper: None,
2058 serde_rename: None,
2059 serde_flatten: false,
2060 });
2061 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2062 assert!(
2063 output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
2064 "optional field should decode without unwrap"
2065 );
2066 }
2067
2068 #[test]
2069 fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
2070 let mut typ = make_test_type();
2071 typ.fields.push(FieldDef {
2072 name: "inner".to_string(),
2073 ty: TypeRef::Named("InnerConfig".to_string()),
2074 optional: false,
2075 default: None,
2076 doc: String::new(),
2077 sanitized: false,
2078 is_boxed: false,
2079 type_rust_path: None,
2080 cfg: None,
2081 typed_default: None,
2082 core_wrapper: CoreWrapper::None,
2083 vec_inner_core_wrapper: CoreWrapper::None,
2084 newtype_wrapper: None,
2085 serde_rename: None,
2086 serde_flatten: false,
2087 });
2088 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2089 assert!(
2090 output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2091 "Named type with no default should use unwrap_or_default"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
2097 let mut typ = make_test_type();
2100 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2102 assert!(
2103 output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2104 "String field with quoted default should use unwrap_or_default"
2105 );
2106 typ.fields.push(FieldDef {
2108 name: "label".to_string(),
2109 ty: TypeRef::String,
2110 optional: false,
2111 default: None,
2112 doc: String::new(),
2113 sanitized: false,
2114 is_boxed: false,
2115 type_rust_path: None,
2116 cfg: None,
2117 typed_default: None,
2118 core_wrapper: CoreWrapper::None,
2119 vec_inner_core_wrapper: CoreWrapper::None,
2120 newtype_wrapper: None,
2121 serde_rename: None,
2122 serde_flatten: false,
2123 });
2124 let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2125 assert!(
2126 output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2127 "String field with no default should use unwrap_or_default"
2128 );
2129 }
2130
2131 #[test]
2136 fn test_gen_extendr_kwargs_constructor_basic() {
2137 let typ = make_test_type();
2138 let empty_enums = ahash::AHashSet::new();
2139 let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2140
2141 assert!(output.contains("#[extendr]"), "should have extendr attribute");
2142 assert!(
2143 output.contains("pub fn new_config("),
2144 "function name should be lowercase type name"
2145 );
2146 assert!(
2148 output.contains("timeout: Option<u64>"),
2149 "should accept timeout as Option<u64>: {output}"
2150 );
2151 assert!(
2152 output.contains("enabled: Option<bool>"),
2153 "should accept enabled as Option<bool>: {output}"
2154 );
2155 assert!(
2156 output.contains("name: Option<String>"),
2157 "should accept name as Option<String>: {output}"
2158 );
2159 assert!(output.contains("-> Config {"), "should return Config");
2160 assert!(
2161 output.contains("let mut __out = <Config>::default();"),
2162 "should base on Default impl: {output}"
2163 );
2164 assert!(
2165 output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
2166 "should overlay caller-provided timeout"
2167 );
2168 assert!(
2169 output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
2170 "should overlay caller-provided enabled"
2171 );
2172 assert!(
2173 output.contains("if let Some(v) = name { __out.name = v; }"),
2174 "should overlay caller-provided name"
2175 );
2176 }
2177
2178 #[test]
2179 fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
2180 let typ = make_test_type();
2184 let empty_enums = ahash::AHashSet::new();
2185 let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2186 assert!(
2187 !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
2188 "constructor must not use Rust-syntax param defaults: {output}"
2189 );
2190 }
2191
2192 #[test]
2197 fn test_gen_go_functional_options_skips_tuple_fields() {
2198 let mut typ = make_test_type();
2199 typ.fields.push(FieldDef {
2200 name: "_0".to_string(),
2201 ty: TypeRef::Primitive(PrimitiveType::U32),
2202 optional: false,
2203 default: None,
2204 doc: String::new(),
2205 sanitized: false,
2206 is_boxed: false,
2207 type_rust_path: None,
2208 cfg: None,
2209 typed_default: None,
2210 core_wrapper: CoreWrapper::None,
2211 vec_inner_core_wrapper: CoreWrapper::None,
2212 newtype_wrapper: None,
2213 serde_rename: None,
2214 serde_flatten: false,
2215 });
2216 let output = gen_go_functional_options(&typ, &simple_type_mapper);
2217 assert!(
2218 !output.contains("_0"),
2219 "tuple field _0 should be filtered out from Go output"
2220 );
2221 }
2222
2223 #[test]
2228 fn test_gen_magnus_hash_constructor_generic_type_prefix() {
2229 let fields: Vec<FieldDef> = (0..16)
2231 .map(|i| FieldDef {
2232 name: format!("field_{i}"),
2233 ty: if i == 0 {
2234 TypeRef::Vec(Box::new(TypeRef::String))
2235 } else {
2236 TypeRef::Primitive(PrimitiveType::U32)
2237 },
2238 optional: false,
2239 default: None,
2240 doc: String::new(),
2241 sanitized: false,
2242 is_boxed: false,
2243 type_rust_path: None,
2244 cfg: None,
2245 typed_default: None,
2246 core_wrapper: CoreWrapper::None,
2247 vec_inner_core_wrapper: CoreWrapper::None,
2248 newtype_wrapper: None,
2249 serde_rename: None,
2250 serde_flatten: false,
2251 })
2252 .collect();
2253 let typ = TypeDef {
2254 name: "WideConfig".to_string(),
2255 rust_path: "crate::WideConfig".to_string(),
2256 original_rust_path: String::new(),
2257 fields,
2258 methods: vec![],
2259 is_opaque: false,
2260 is_clone: true,
2261 is_copy: false,
2262 doc: String::new(),
2263 cfg: None,
2264 is_trait: false,
2265 has_default: true,
2266 has_stripped_cfg_fields: false,
2267 is_return_type: false,
2268 serde_rename_all: None,
2269 has_serde: false,
2270 super_traits: vec![],
2271 };
2272 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2273 assert!(
2275 output.contains("<Vec<String>>::try_convert"),
2276 "generic types should use UFCS angle-bracket prefix: {output}"
2277 );
2278 }
2279
2280 #[test]
2287 fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
2288 let field = FieldDef {
2292 name: "max_depth".to_string(),
2293 ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2294 optional: true,
2295 default: None,
2296 doc: String::new(),
2297 sanitized: false,
2298 is_boxed: false,
2299 type_rust_path: None,
2300 cfg: None,
2301 typed_default: None,
2302 core_wrapper: CoreWrapper::None,
2303 vec_inner_core_wrapper: CoreWrapper::None,
2304 newtype_wrapper: None,
2305 serde_rename: None,
2306 serde_flatten: false,
2307 };
2308 let mut fields: Vec<FieldDef> = (0..15)
2310 .map(|i| FieldDef {
2311 name: format!("field_{i}"),
2312 ty: TypeRef::Primitive(PrimitiveType::U32),
2313 optional: false,
2314 default: None,
2315 doc: String::new(),
2316 sanitized: false,
2317 is_boxed: false,
2318 type_rust_path: None,
2319 cfg: None,
2320 typed_default: None,
2321 core_wrapper: CoreWrapper::None,
2322 vec_inner_core_wrapper: CoreWrapper::None,
2323 newtype_wrapper: None,
2324 serde_rename: None,
2325 serde_flatten: false,
2326 })
2327 .collect();
2328 fields.push(field);
2329 let typ = TypeDef {
2330 name: "UpdateConfig".to_string(),
2331 rust_path: "crate::UpdateConfig".to_string(),
2332 original_rust_path: String::new(),
2333 fields,
2334 methods: vec![],
2335 is_opaque: false,
2336 is_clone: true,
2337 is_copy: false,
2338 doc: String::new(),
2339 cfg: None,
2340 is_trait: false,
2341 has_default: true,
2342 has_stripped_cfg_fields: false,
2343 is_return_type: false,
2344 serde_rename_all: None,
2345 has_serde: false,
2346 super_traits: vec![],
2347 };
2348 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2349 assert!(
2352 !output.contains("Option<Option<"),
2353 "hash constructor must not emit double Option: {output}"
2354 );
2355 assert!(
2356 output.contains("i64::try_convert"),
2357 "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
2358 );
2359 }
2360
2361 #[test]
2362 fn test_magnus_positional_constructor_no_double_option_when_ty_is_optional() {
2363 let field = FieldDef {
2366 name: "max_depth".to_string(),
2367 ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2368 optional: true,
2369 default: None,
2370 doc: String::new(),
2371 sanitized: false,
2372 is_boxed: false,
2373 type_rust_path: None,
2374 cfg: None,
2375 typed_default: None,
2376 core_wrapper: CoreWrapper::None,
2377 vec_inner_core_wrapper: CoreWrapper::None,
2378 newtype_wrapper: None,
2379 serde_rename: None,
2380 serde_flatten: false,
2381 };
2382 let typ = TypeDef {
2383 name: "SmallUpdate".to_string(),
2384 rust_path: "crate::SmallUpdate".to_string(),
2385 original_rust_path: String::new(),
2386 fields: vec![field],
2387 methods: vec![],
2388 is_opaque: false,
2389 is_clone: true,
2390 is_copy: false,
2391 doc: String::new(),
2392 cfg: None,
2393 is_trait: false,
2394 has_default: true,
2395 has_stripped_cfg_fields: false,
2396 is_return_type: false,
2397 serde_rename_all: None,
2398 has_serde: false,
2399 super_traits: vec![],
2400 };
2401 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
2402 assert!(
2405 !output.contains("Option<Option<"),
2406 "positional constructor must not emit double Option: {output}"
2407 );
2408 assert!(
2409 output.contains("Option<i64>"),
2410 "positional constructor should emit Option<inner> for optional Optional(T): {output}"
2411 );
2412 }
2413}