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 binding_excluded: false,
929 binding_exclusion_reason: None,
930 },
931 FieldDef {
932 name: "enabled".to_string(),
933 ty: TypeRef::Primitive(PrimitiveType::Bool),
934 optional: false,
935 default: None,
936 doc: "Enable feature".to_string(),
937 sanitized: false,
938 is_boxed: false,
939 type_rust_path: None,
940 cfg: None,
941 typed_default: Some(DefaultValue::BoolLiteral(true)),
942 core_wrapper: CoreWrapper::None,
943 vec_inner_core_wrapper: CoreWrapper::None,
944 newtype_wrapper: None,
945 serde_rename: None,
946 serde_flatten: false,
947 binding_excluded: false,
948 binding_exclusion_reason: None,
949 },
950 FieldDef {
951 name: "name".to_string(),
952 ty: TypeRef::String,
953 optional: false,
954 default: None,
955 doc: "Config name".to_string(),
956 sanitized: false,
957 is_boxed: false,
958 type_rust_path: None,
959 cfg: None,
960 typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
961 core_wrapper: CoreWrapper::None,
962 vec_inner_core_wrapper: CoreWrapper::None,
963 newtype_wrapper: None,
964 serde_rename: None,
965 serde_flatten: false,
966 binding_excluded: false,
967 binding_exclusion_reason: None,
968 },
969 ],
970 methods: vec![],
971 is_opaque: false,
972 is_clone: true,
973 is_copy: false,
974 doc: "Configuration type".to_string(),
975 cfg: None,
976 is_trait: false,
977 has_default: true,
978 has_stripped_cfg_fields: false,
979 is_return_type: false,
980 serde_rename_all: None,
981 has_serde: false,
982 super_traits: vec![],
983 binding_excluded: false,
984 binding_exclusion_reason: None,
985 }
986 }
987
988 #[test]
989 fn test_default_value_bool_true_python() {
990 let field = FieldDef {
991 name: "enabled".to_string(),
992 ty: TypeRef::Primitive(PrimitiveType::Bool),
993 optional: false,
994 default: None,
995 doc: String::new(),
996 sanitized: false,
997 is_boxed: false,
998 type_rust_path: None,
999 cfg: None,
1000 typed_default: Some(DefaultValue::BoolLiteral(true)),
1001 core_wrapper: CoreWrapper::None,
1002 vec_inner_core_wrapper: CoreWrapper::None,
1003 newtype_wrapper: None,
1004 serde_rename: None,
1005 serde_flatten: false,
1006 binding_excluded: false,
1007 binding_exclusion_reason: None,
1008 };
1009 assert_eq!(default_value_for_field(&field, "python"), "True");
1010 }
1011
1012 #[test]
1013 fn test_default_value_bool_false_go() {
1014 let field = FieldDef {
1015 name: "enabled".to_string(),
1016 ty: TypeRef::Primitive(PrimitiveType::Bool),
1017 optional: false,
1018 default: None,
1019 doc: String::new(),
1020 sanitized: false,
1021 is_boxed: false,
1022 type_rust_path: None,
1023 cfg: None,
1024 typed_default: Some(DefaultValue::BoolLiteral(false)),
1025 core_wrapper: CoreWrapper::None,
1026 vec_inner_core_wrapper: CoreWrapper::None,
1027 newtype_wrapper: None,
1028 serde_rename: None,
1029 serde_flatten: false,
1030 binding_excluded: false,
1031 binding_exclusion_reason: None,
1032 };
1033 assert_eq!(default_value_for_field(&field, "go"), "false");
1034 }
1035
1036 #[test]
1037 fn test_default_value_string_literal() {
1038 let field = FieldDef {
1039 name: "name".to_string(),
1040 ty: TypeRef::String,
1041 optional: false,
1042 default: None,
1043 doc: String::new(),
1044 sanitized: false,
1045 is_boxed: false,
1046 type_rust_path: None,
1047 cfg: None,
1048 typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1049 core_wrapper: CoreWrapper::None,
1050 vec_inner_core_wrapper: CoreWrapper::None,
1051 newtype_wrapper: None,
1052 serde_rename: None,
1053 serde_flatten: false,
1054 binding_excluded: false,
1055 binding_exclusion_reason: None,
1056 };
1057 assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
1058 assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
1059 }
1060
1061 #[test]
1062 fn test_default_value_int_literal() {
1063 let field = FieldDef {
1064 name: "timeout".to_string(),
1065 ty: TypeRef::Primitive(PrimitiveType::U64),
1066 optional: false,
1067 default: None,
1068 doc: String::new(),
1069 sanitized: false,
1070 is_boxed: false,
1071 type_rust_path: None,
1072 cfg: None,
1073 typed_default: Some(DefaultValue::IntLiteral(42)),
1074 core_wrapper: CoreWrapper::None,
1075 vec_inner_core_wrapper: CoreWrapper::None,
1076 newtype_wrapper: None,
1077 serde_rename: None,
1078 serde_flatten: false,
1079 binding_excluded: false,
1080 binding_exclusion_reason: None,
1081 };
1082 let result = default_value_for_field(&field, "python");
1083 assert_eq!(result, "42");
1084 }
1085
1086 #[test]
1087 fn test_default_value_none() {
1088 let field = FieldDef {
1089 name: "maybe".to_string(),
1090 ty: TypeRef::Optional(Box::new(TypeRef::String)),
1091 optional: true,
1092 default: None,
1093 doc: String::new(),
1094 sanitized: false,
1095 is_boxed: false,
1096 type_rust_path: None,
1097 cfg: None,
1098 typed_default: Some(DefaultValue::None),
1099 core_wrapper: CoreWrapper::None,
1100 vec_inner_core_wrapper: CoreWrapper::None,
1101 newtype_wrapper: None,
1102 serde_rename: None,
1103 serde_flatten: false,
1104 binding_excluded: false,
1105 binding_exclusion_reason: None,
1106 };
1107 assert_eq!(default_value_for_field(&field, "python"), "None");
1108 assert_eq!(default_value_for_field(&field, "go"), "nil");
1109 assert_eq!(default_value_for_field(&field, "java"), "null");
1110 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1111 }
1112
1113 #[test]
1114 fn test_default_value_fallback_string() {
1115 let field = FieldDef {
1116 name: "name".to_string(),
1117 ty: TypeRef::String,
1118 optional: false,
1119 default: Some("\"custom\"".to_string()),
1120 doc: String::new(),
1121 sanitized: false,
1122 is_boxed: false,
1123 type_rust_path: None,
1124 cfg: None,
1125 typed_default: None,
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 binding_excluded: false,
1132 binding_exclusion_reason: None,
1133 };
1134 assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
1135 }
1136
1137 #[test]
1138 fn test_gen_pyo3_kwargs_constructor() {
1139 let typ = make_test_type();
1140 let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
1141 TypeRef::Primitive(p) => format!("{:?}", p),
1142 TypeRef::String | TypeRef::Char => "str".to_string(),
1143 _ => "Any".to_string(),
1144 });
1145
1146 assert!(output.contains("#[new]"));
1147 assert!(output.contains("#[pyo3(signature = ("));
1148 assert!(output.contains("timeout=30"));
1149 assert!(output.contains("enabled=True"));
1150 assert!(output.contains("name=\"default\""));
1151 assert!(output.contains("fn new("));
1152 }
1153
1154 #[test]
1155 fn test_gen_napi_defaults_constructor() {
1156 let typ = make_test_type();
1157 let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
1158 TypeRef::Primitive(p) => format!("{:?}", p),
1159 TypeRef::String | TypeRef::Char => "String".to_string(),
1160 _ => "Value".to_string(),
1161 });
1162
1163 assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
1164 assert!(output.contains("timeout"));
1165 assert!(output.contains("enabled"));
1166 assert!(output.contains("name"));
1167 }
1168
1169 #[test]
1170 fn test_gen_go_functional_options() {
1171 let typ = make_test_type();
1172 let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
1173 TypeRef::Primitive(p) => match p {
1174 PrimitiveType::U64 => "uint64".to_string(),
1175 PrimitiveType::Bool => "bool".to_string(),
1176 _ => "interface{}".to_string(),
1177 },
1178 TypeRef::String | TypeRef::Char => "string".to_string(),
1179 _ => "interface{}".to_string(),
1180 });
1181
1182 assert!(output.contains("type Config struct {"));
1183 assert!(output.contains("type ConfigOption func(*Config)"));
1184 assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
1185 assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
1186 assert!(output.contains("func WithConfigName(val string) ConfigOption"));
1187 assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
1188 }
1189
1190 #[test]
1191 fn test_gen_java_builder() {
1192 let typ = make_test_type();
1193 let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
1194 TypeRef::Primitive(p) => match p {
1195 PrimitiveType::U64 => "long".to_string(),
1196 PrimitiveType::Bool => "boolean".to_string(),
1197 _ => "int".to_string(),
1198 },
1199 TypeRef::String | TypeRef::Char => "String".to_string(),
1200 _ => "Object".to_string(),
1201 });
1202
1203 assert!(output.contains("package dev.test;"));
1204 assert!(output.contains("public class ConfigBuilder"));
1205 assert!(output.contains("withTimeout"));
1206 assert!(output.contains("withEnabled"));
1207 assert!(output.contains("withName"));
1208 assert!(output.contains("public Config build()"));
1209 }
1210
1211 #[test]
1212 fn test_gen_csharp_record() {
1213 let typ = make_test_type();
1214 let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
1215 TypeRef::Primitive(p) => match p {
1216 PrimitiveType::U64 => "ulong".to_string(),
1217 PrimitiveType::Bool => "bool".to_string(),
1218 _ => "int".to_string(),
1219 },
1220 TypeRef::String | TypeRef::Char => "string".to_string(),
1221 _ => "object".to_string(),
1222 });
1223
1224 assert!(output.contains("namespace MyNamespace;"));
1225 assert!(output.contains("public record Config"));
1226 assert!(output.contains("public ulong Timeout"));
1227 assert!(output.contains("public bool Enabled"));
1228 assert!(output.contains("public string Name"));
1229 assert!(output.contains("init;"));
1230 }
1231
1232 #[test]
1233 fn test_default_value_float_literal() {
1234 let field = FieldDef {
1235 name: "ratio".to_string(),
1236 ty: TypeRef::Primitive(PrimitiveType::F64),
1237 optional: false,
1238 default: None,
1239 doc: String::new(),
1240 sanitized: false,
1241 is_boxed: false,
1242 type_rust_path: None,
1243 cfg: None,
1244 typed_default: Some(DefaultValue::FloatLiteral(1.5)),
1245 core_wrapper: CoreWrapper::None,
1246 vec_inner_core_wrapper: CoreWrapper::None,
1247 newtype_wrapper: None,
1248 serde_rename: None,
1249 serde_flatten: false,
1250 binding_excluded: false,
1251 binding_exclusion_reason: None,
1252 };
1253 let result = default_value_for_field(&field, "python");
1254 assert!(result.contains("1.5"));
1255 }
1256
1257 #[test]
1258 fn test_default_value_no_typed_no_default() {
1259 let field = FieldDef {
1260 name: "count".to_string(),
1261 ty: TypeRef::Primitive(PrimitiveType::U32),
1262 optional: false,
1263 default: None,
1264 doc: String::new(),
1265 sanitized: false,
1266 is_boxed: false,
1267 type_rust_path: None,
1268 cfg: None,
1269 typed_default: None,
1270 core_wrapper: CoreWrapper::None,
1271 vec_inner_core_wrapper: CoreWrapper::None,
1272 newtype_wrapper: None,
1273 serde_rename: None,
1274 serde_flatten: false,
1275 binding_excluded: false,
1276 binding_exclusion_reason: None,
1277 };
1278 assert_eq!(default_value_for_field(&field, "python"), "0");
1280 assert_eq!(default_value_for_field(&field, "go"), "0");
1281 }
1282
1283 fn make_field(name: &str, ty: TypeRef) -> FieldDef {
1284 FieldDef {
1285 name: name.to_string(),
1286 ty,
1287 optional: false,
1288 default: None,
1289 doc: String::new(),
1290 sanitized: false,
1291 is_boxed: false,
1292 type_rust_path: None,
1293 cfg: None,
1294 typed_default: None,
1295 core_wrapper: CoreWrapper::None,
1296 vec_inner_core_wrapper: CoreWrapper::None,
1297 newtype_wrapper: None,
1298 serde_rename: None,
1299 serde_flatten: false,
1300 binding_excluded: false,
1301 binding_exclusion_reason: None,
1302 }
1303 }
1304
1305 fn simple_type_mapper(tr: &TypeRef) -> String {
1306 match tr {
1307 TypeRef::Primitive(p) => match p {
1308 PrimitiveType::U64 => "u64".to_string(),
1309 PrimitiveType::Bool => "bool".to_string(),
1310 PrimitiveType::U32 => "u32".to_string(),
1311 _ => "i64".to_string(),
1312 },
1313 TypeRef::String | TypeRef::Char => "String".to_string(),
1314 TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
1315 TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
1316 TypeRef::Named(n) => n.clone(),
1317 _ => "Value".to_string(),
1318 }
1319 }
1320
1321 #[test]
1326 fn test_default_value_bool_literal_ruby() {
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(true)),
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 binding_excluded: false,
1344 binding_exclusion_reason: None,
1345 };
1346 assert_eq!(default_value_for_field(&field, "ruby"), "true");
1347 assert_eq!(default_value_for_field(&field, "php"), "true");
1348 assert_eq!(default_value_for_field(&field, "csharp"), "true");
1349 assert_eq!(default_value_for_field(&field, "java"), "true");
1350 assert_eq!(default_value_for_field(&field, "rust"), "true");
1351 }
1352
1353 #[test]
1354 fn test_default_value_bool_literal_r() {
1355 let field = FieldDef {
1356 name: "flag".to_string(),
1357 ty: TypeRef::Primitive(PrimitiveType::Bool),
1358 optional: false,
1359 default: None,
1360 doc: String::new(),
1361 sanitized: false,
1362 is_boxed: false,
1363 type_rust_path: None,
1364 cfg: None,
1365 typed_default: Some(DefaultValue::BoolLiteral(false)),
1366 core_wrapper: CoreWrapper::None,
1367 vec_inner_core_wrapper: CoreWrapper::None,
1368 newtype_wrapper: None,
1369 serde_rename: None,
1370 serde_flatten: false,
1371 binding_excluded: false,
1372 binding_exclusion_reason: None,
1373 };
1374 assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1375 }
1376
1377 #[test]
1378 fn test_default_value_string_literal_rust() {
1379 let field = FieldDef {
1380 name: "label".to_string(),
1381 ty: TypeRef::String,
1382 optional: false,
1383 default: None,
1384 doc: String::new(),
1385 sanitized: false,
1386 is_boxed: false,
1387 type_rust_path: None,
1388 cfg: None,
1389 typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
1390 core_wrapper: CoreWrapper::None,
1391 vec_inner_core_wrapper: CoreWrapper::None,
1392 newtype_wrapper: None,
1393 serde_rename: None,
1394 serde_flatten: false,
1395 binding_excluded: false,
1396 binding_exclusion_reason: None,
1397 };
1398 assert_eq!(default_value_for_field(&field, "rust"), "\"hello\".to_string()");
1399 }
1400
1401 #[test]
1402 fn test_default_value_string_literal_escapes_quotes() {
1403 let field = FieldDef {
1404 name: "label".to_string(),
1405 ty: TypeRef::String,
1406 optional: false,
1407 default: None,
1408 doc: String::new(),
1409 sanitized: false,
1410 is_boxed: false,
1411 type_rust_path: None,
1412 cfg: None,
1413 typed_default: Some(DefaultValue::StringLiteral("say \"hi\"".to_string())),
1414 core_wrapper: CoreWrapper::None,
1415 vec_inner_core_wrapper: CoreWrapper::None,
1416 newtype_wrapper: None,
1417 serde_rename: None,
1418 serde_flatten: false,
1419 binding_excluded: false,
1420 binding_exclusion_reason: None,
1421 };
1422 assert_eq!(default_value_for_field(&field, "python"), "\"say \\\"hi\\\"\"");
1423 }
1424
1425 #[test]
1426 fn test_default_value_float_literal_whole_number() {
1427 let field = FieldDef {
1429 name: "scale".to_string(),
1430 ty: TypeRef::Primitive(PrimitiveType::F32),
1431 optional: false,
1432 default: None,
1433 doc: String::new(),
1434 sanitized: false,
1435 is_boxed: false,
1436 type_rust_path: None,
1437 cfg: None,
1438 typed_default: Some(DefaultValue::FloatLiteral(2.0)),
1439 core_wrapper: CoreWrapper::None,
1440 vec_inner_core_wrapper: CoreWrapper::None,
1441 newtype_wrapper: None,
1442 serde_rename: None,
1443 serde_flatten: false,
1444 binding_excluded: false,
1445 binding_exclusion_reason: None,
1446 };
1447 let result = default_value_for_field(&field, "python");
1448 assert!(result.contains('.'), "whole-number float should contain '.': {result}");
1449 }
1450
1451 #[test]
1452 fn test_default_value_enum_variant_per_language() {
1453 let field = FieldDef {
1454 name: "format".to_string(),
1455 ty: TypeRef::Named("OutputFormat".to_string()),
1456 optional: false,
1457 default: None,
1458 doc: String::new(),
1459 sanitized: false,
1460 is_boxed: false,
1461 type_rust_path: None,
1462 cfg: None,
1463 typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
1464 core_wrapper: CoreWrapper::None,
1465 vec_inner_core_wrapper: CoreWrapper::None,
1466 newtype_wrapper: None,
1467 serde_rename: None,
1468 serde_flatten: false,
1469 binding_excluded: false,
1470 binding_exclusion_reason: None,
1471 };
1472 assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
1473 assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
1474 assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
1475 assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
1476 assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
1477 assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
1478 assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
1479 assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
1480 }
1481
1482 #[test]
1483 fn test_default_value_empty_vec_per_language() {
1484 let field = FieldDef {
1485 name: "items".to_string(),
1486 ty: TypeRef::Vec(Box::new(TypeRef::String)),
1487 optional: false,
1488 default: None,
1489 doc: String::new(),
1490 sanitized: false,
1491 is_boxed: false,
1492 type_rust_path: None,
1493 cfg: None,
1494 typed_default: Some(DefaultValue::Empty),
1495 core_wrapper: CoreWrapper::None,
1496 vec_inner_core_wrapper: CoreWrapper::None,
1497 newtype_wrapper: None,
1498 serde_rename: None,
1499 serde_flatten: false,
1500 binding_excluded: false,
1501 binding_exclusion_reason: None,
1502 };
1503 assert_eq!(default_value_for_field(&field, "python"), "[]");
1504 assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1505 assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1506 assert_eq!(default_value_for_field(&field, "go"), "nil");
1507 assert_eq!(default_value_for_field(&field, "java"), "List.of()");
1508 assert_eq!(default_value_for_field(&field, "php"), "[]");
1509 assert_eq!(default_value_for_field(&field, "r"), "c()");
1510 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1511 }
1512
1513 #[test]
1514 fn test_default_value_empty_map_per_language() {
1515 let field = FieldDef {
1516 name: "meta".to_string(),
1517 ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1518 optional: false,
1519 default: None,
1520 doc: String::new(),
1521 sanitized: false,
1522 is_boxed: false,
1523 type_rust_path: None,
1524 cfg: None,
1525 typed_default: Some(DefaultValue::Empty),
1526 core_wrapper: CoreWrapper::None,
1527 vec_inner_core_wrapper: CoreWrapper::None,
1528 newtype_wrapper: None,
1529 serde_rename: None,
1530 serde_flatten: false,
1531 binding_excluded: false,
1532 binding_exclusion_reason: None,
1533 };
1534 assert_eq!(default_value_for_field(&field, "python"), "{}");
1535 assert_eq!(default_value_for_field(&field, "go"), "nil");
1536 assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
1537 assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1538 }
1539
1540 #[test]
1541 fn test_default_value_empty_bool_primitive() {
1542 let field = FieldDef {
1543 name: "flag".to_string(),
1544 ty: TypeRef::Primitive(PrimitiveType::Bool),
1545 optional: false,
1546 default: None,
1547 doc: String::new(),
1548 sanitized: false,
1549 is_boxed: false,
1550 type_rust_path: None,
1551 cfg: None,
1552 typed_default: Some(DefaultValue::Empty),
1553 core_wrapper: CoreWrapper::None,
1554 vec_inner_core_wrapper: CoreWrapper::None,
1555 newtype_wrapper: None,
1556 serde_rename: None,
1557 serde_flatten: false,
1558 binding_excluded: false,
1559 binding_exclusion_reason: None,
1560 };
1561 assert_eq!(default_value_for_field(&field, "python"), "False");
1562 assert_eq!(default_value_for_field(&field, "ruby"), "false");
1563 assert_eq!(default_value_for_field(&field, "go"), "false");
1564 }
1565
1566 #[test]
1567 fn test_default_value_empty_float_primitive() {
1568 let field = FieldDef {
1569 name: "ratio".to_string(),
1570 ty: TypeRef::Primitive(PrimitiveType::F64),
1571 optional: false,
1572 default: None,
1573 doc: String::new(),
1574 sanitized: false,
1575 is_boxed: false,
1576 type_rust_path: None,
1577 cfg: None,
1578 typed_default: Some(DefaultValue::Empty),
1579 core_wrapper: CoreWrapper::None,
1580 vec_inner_core_wrapper: CoreWrapper::None,
1581 newtype_wrapper: None,
1582 serde_rename: None,
1583 serde_flatten: false,
1584 binding_excluded: false,
1585 binding_exclusion_reason: None,
1586 };
1587 assert_eq!(default_value_for_field(&field, "python"), "0.0");
1588 }
1589
1590 #[test]
1591 fn test_default_value_empty_string_type() {
1592 let field = FieldDef {
1593 name: "label".to_string(),
1594 ty: TypeRef::String,
1595 optional: false,
1596 default: None,
1597 doc: String::new(),
1598 sanitized: false,
1599 is_boxed: false,
1600 type_rust_path: None,
1601 cfg: None,
1602 typed_default: Some(DefaultValue::Empty),
1603 core_wrapper: CoreWrapper::None,
1604 vec_inner_core_wrapper: CoreWrapper::None,
1605 newtype_wrapper: None,
1606 serde_rename: None,
1607 serde_flatten: false,
1608 binding_excluded: false,
1609 binding_exclusion_reason: None,
1610 };
1611 assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1612 assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1613 }
1614
1615 #[test]
1616 fn test_default_value_empty_bytes_type() {
1617 let field = FieldDef {
1618 name: "data".to_string(),
1619 ty: TypeRef::Bytes,
1620 optional: false,
1621 default: None,
1622 doc: String::new(),
1623 sanitized: false,
1624 is_boxed: false,
1625 type_rust_path: None,
1626 cfg: None,
1627 typed_default: Some(DefaultValue::Empty),
1628 core_wrapper: CoreWrapper::None,
1629 vec_inner_core_wrapper: CoreWrapper::None,
1630 newtype_wrapper: None,
1631 serde_rename: None,
1632 serde_flatten: false,
1633 binding_excluded: false,
1634 binding_exclusion_reason: None,
1635 };
1636 assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1637 assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1638 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1639 }
1640
1641 #[test]
1642 fn test_default_value_empty_json_type() {
1643 let field = FieldDef {
1644 name: "payload".to_string(),
1645 ty: TypeRef::Json,
1646 optional: false,
1647 default: None,
1648 doc: String::new(),
1649 sanitized: false,
1650 is_boxed: false,
1651 type_rust_path: None,
1652 cfg: None,
1653 typed_default: Some(DefaultValue::Empty),
1654 core_wrapper: CoreWrapper::None,
1655 vec_inner_core_wrapper: CoreWrapper::None,
1656 newtype_wrapper: None,
1657 serde_rename: None,
1658 serde_flatten: false,
1659 binding_excluded: false,
1660 binding_exclusion_reason: None,
1661 };
1662 assert_eq!(default_value_for_field(&field, "python"), "{}");
1663 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1664 assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1665 assert_eq!(default_value_for_field(&field, "r"), "list()");
1666 assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1667 }
1668
1669 #[test]
1670 fn test_default_value_none_ruby_php_r() {
1671 let field = FieldDef {
1672 name: "maybe".to_string(),
1673 ty: TypeRef::Optional(Box::new(TypeRef::String)),
1674 optional: true,
1675 default: None,
1676 doc: String::new(),
1677 sanitized: false,
1678 is_boxed: false,
1679 type_rust_path: None,
1680 cfg: None,
1681 typed_default: Some(DefaultValue::None),
1682 core_wrapper: CoreWrapper::None,
1683 vec_inner_core_wrapper: CoreWrapper::None,
1684 newtype_wrapper: None,
1685 serde_rename: None,
1686 serde_flatten: false,
1687 binding_excluded: false,
1688 binding_exclusion_reason: None,
1689 };
1690 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1691 assert_eq!(default_value_for_field(&field, "php"), "null");
1692 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1693 assert_eq!(default_value_for_field(&field, "rust"), "None");
1694 }
1695
1696 #[test]
1701 fn test_default_value_fallback_bool_all_languages() {
1702 let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
1703 assert_eq!(default_value_for_field(&field, "python"), "False");
1704 assert_eq!(default_value_for_field(&field, "ruby"), "false");
1705 assert_eq!(default_value_for_field(&field, "csharp"), "false");
1706 assert_eq!(default_value_for_field(&field, "java"), "false");
1707 assert_eq!(default_value_for_field(&field, "php"), "false");
1708 assert_eq!(default_value_for_field(&field, "r"), "FALSE");
1709 assert_eq!(default_value_for_field(&field, "rust"), "false");
1710 }
1711
1712 #[test]
1713 fn test_default_value_fallback_float() {
1714 let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
1715 assert_eq!(default_value_for_field(&field, "python"), "0.0");
1716 assert_eq!(default_value_for_field(&field, "rust"), "0.0");
1717 }
1718
1719 #[test]
1720 fn test_default_value_fallback_string_all_languages() {
1721 let field = make_field("name", TypeRef::String);
1722 assert_eq!(default_value_for_field(&field, "python"), "\"\"");
1723 assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1724 assert_eq!(default_value_for_field(&field, "go"), "\"\"");
1725 assert_eq!(default_value_for_field(&field, "java"), "\"\"");
1726 assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
1727 assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1728 assert_eq!(default_value_for_field(&field, "r"), "\"\"");
1729 assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
1730 }
1731
1732 #[test]
1733 fn test_default_value_fallback_bytes_all_languages() {
1734 let field = make_field("data", TypeRef::Bytes);
1735 assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
1736 assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
1737 assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
1738 assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
1739 assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
1740 assert_eq!(default_value_for_field(&field, "php"), "\"\"");
1741 assert_eq!(default_value_for_field(&field, "r"), "raw()");
1742 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1743 }
1744
1745 #[test]
1746 fn test_default_value_fallback_optional() {
1747 let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
1748 assert_eq!(default_value_for_field(&field, "python"), "None");
1749 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1750 assert_eq!(default_value_for_field(&field, "go"), "nil");
1751 assert_eq!(default_value_for_field(&field, "java"), "null");
1752 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1753 assert_eq!(default_value_for_field(&field, "php"), "null");
1754 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1755 assert_eq!(default_value_for_field(&field, "rust"), "None");
1756 }
1757
1758 #[test]
1759 fn test_default_value_fallback_vec_all_languages() {
1760 let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
1761 assert_eq!(default_value_for_field(&field, "python"), "[]");
1762 assert_eq!(default_value_for_field(&field, "ruby"), "[]");
1763 assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
1764 assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
1765 assert_eq!(default_value_for_field(&field, "csharp"), "[]");
1766 assert_eq!(default_value_for_field(&field, "php"), "[]");
1767 assert_eq!(default_value_for_field(&field, "r"), "c()");
1768 assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
1769 }
1770
1771 #[test]
1772 fn test_default_value_fallback_map_all_languages() {
1773 let field = make_field(
1774 "meta",
1775 TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
1776 );
1777 assert_eq!(default_value_for_field(&field, "python"), "{}");
1778 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1779 assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
1780 assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
1781 assert_eq!(
1782 default_value_for_field(&field, "csharp"),
1783 "new Dictionary<string, object>()"
1784 );
1785 assert_eq!(default_value_for_field(&field, "php"), "[]");
1786 assert_eq!(default_value_for_field(&field, "r"), "list()");
1787 assert_eq!(
1788 default_value_for_field(&field, "rust"),
1789 "std::collections::HashMap::new()"
1790 );
1791 }
1792
1793 #[test]
1794 fn test_default_value_fallback_json_all_languages() {
1795 let field = make_field("payload", TypeRef::Json);
1796 assert_eq!(default_value_for_field(&field, "python"), "{}");
1797 assert_eq!(default_value_for_field(&field, "ruby"), "{}");
1798 assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
1799 assert_eq!(default_value_for_field(&field, "r"), "list()");
1800 assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
1801 }
1802
1803 #[test]
1804 fn test_default_value_fallback_named_type() {
1805 let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
1806 assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
1807 assert_eq!(default_value_for_field(&field, "python"), "None");
1808 assert_eq!(default_value_for_field(&field, "ruby"), "nil");
1809 assert_eq!(default_value_for_field(&field, "go"), "nil");
1810 assert_eq!(default_value_for_field(&field, "java"), "null");
1811 assert_eq!(default_value_for_field(&field, "csharp"), "null");
1812 assert_eq!(default_value_for_field(&field, "php"), "null");
1813 assert_eq!(default_value_for_field(&field, "r"), "NULL");
1814 }
1815
1816 #[test]
1817 fn test_default_value_fallback_duration() {
1818 let field = make_field("timeout", TypeRef::Duration);
1820 assert_eq!(default_value_for_field(&field, "python"), "None");
1821 assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
1822 }
1823
1824 #[test]
1829 fn test_gen_magnus_kwargs_constructor_positional_basic() {
1830 let typ = make_test_type();
1831 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1832
1833 assert!(output.contains("fn new("), "should have fn new");
1834 assert!(output.contains("Option<u64>"), "timeout should be Option<u64>");
1836 assert!(output.contains("Option<bool>"), "enabled should be Option<bool>");
1837 assert!(output.contains("Option<String>"), "name should be Option<String>");
1838 assert!(output.contains("-> Self {"), "should return Self");
1839 assert!(
1841 output.contains("timeout: timeout.unwrap_or(30),"),
1842 "should apply int default"
1843 );
1844 assert!(
1846 output.contains("enabled: enabled.unwrap_or(true),"),
1847 "should apply bool default"
1848 );
1849 assert!(
1851 output.contains("name: name.unwrap_or(\"default\".to_string()),"),
1852 "should apply string default"
1853 );
1854 }
1855
1856 #[test]
1857 fn test_gen_magnus_kwargs_constructor_positional_optional_field() {
1858 let mut typ = make_test_type();
1860 typ.fields.push(FieldDef {
1861 name: "extra".to_string(),
1862 ty: TypeRef::String,
1863 optional: true,
1864 default: None,
1865 doc: String::new(),
1866 sanitized: false,
1867 is_boxed: false,
1868 type_rust_path: None,
1869 cfg: None,
1870 typed_default: None,
1871 core_wrapper: CoreWrapper::None,
1872 vec_inner_core_wrapper: CoreWrapper::None,
1873 newtype_wrapper: None,
1874 serde_rename: None,
1875 serde_flatten: false,
1876 binding_excluded: false,
1877 binding_exclusion_reason: None,
1878 });
1879 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1880 assert!(output.contains("extra,"), "optional field should be assigned directly");
1882 assert!(!output.contains("extra.unwrap"), "optional field should not use unwrap");
1883 }
1884
1885 #[test]
1886 fn test_gen_magnus_kwargs_constructor_unwrap_or_default() {
1887 let mut typ = make_test_type();
1889 typ.fields.push(FieldDef {
1890 name: "count".to_string(),
1891 ty: TypeRef::Primitive(PrimitiveType::U32),
1892 optional: false,
1893 default: None,
1894 doc: String::new(),
1895 sanitized: false,
1896 is_boxed: false,
1897 type_rust_path: None,
1898 cfg: None,
1899 typed_default: None,
1900 core_wrapper: CoreWrapper::None,
1901 vec_inner_core_wrapper: CoreWrapper::None,
1902 newtype_wrapper: None,
1903 serde_rename: None,
1904 serde_flatten: false,
1905 binding_excluded: false,
1906 binding_exclusion_reason: None,
1907 });
1908 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
1909 assert!(
1910 output.contains("count: count.unwrap_or_default(),"),
1911 "plain primitive with no default should use unwrap_or_default"
1912 );
1913 }
1914
1915 #[test]
1916 fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
1917 let mut fields: Vec<FieldDef> = (0..16)
1919 .map(|i| FieldDef {
1920 name: format!("field_{i}"),
1921 ty: TypeRef::Primitive(PrimitiveType::U32),
1922 optional: false,
1923 default: None,
1924 doc: String::new(),
1925 sanitized: false,
1926 is_boxed: false,
1927 type_rust_path: None,
1928 cfg: None,
1929 typed_default: None,
1930 core_wrapper: CoreWrapper::None,
1931 vec_inner_core_wrapper: CoreWrapper::None,
1932 newtype_wrapper: None,
1933 serde_rename: None,
1934 serde_flatten: false,
1935 binding_excluded: false,
1936 binding_exclusion_reason: None,
1937 })
1938 .collect();
1939 fields[0].optional = true;
1941
1942 let typ = TypeDef {
1943 name: "BigConfig".to_string(),
1944 rust_path: "crate::BigConfig".to_string(),
1945 original_rust_path: String::new(),
1946 fields,
1947 methods: vec![],
1948 is_opaque: false,
1949 is_clone: true,
1950 is_copy: false,
1951 doc: String::new(),
1952 cfg: None,
1953 is_trait: false,
1954 has_default: true,
1955 has_stripped_cfg_fields: false,
1956 is_return_type: false,
1957 serde_rename_all: None,
1958 has_serde: false,
1959 super_traits: vec![],
1960 binding_excluded: false,
1961 binding_exclusion_reason: None,
1962 };
1963 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
1964
1965 assert!(
1966 output.contains("Option<magnus::RHash>"),
1967 "should accept RHash via scan_args"
1968 );
1969 assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
1970 assert!(
1972 output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
1973 "optional field should use and_then"
1974 );
1975 assert!(
1976 output.contains("field_0:").then_some(()).is_some(),
1977 "field_0 should appear in output"
1978 );
1979 }
1980
1981 #[test]
1986 fn test_gen_php_kwargs_constructor_basic() {
1987 let typ = make_test_type();
1988 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
1989
1990 assert!(
1991 output.contains("pub fn __construct("),
1992 "should use PHP constructor name"
1993 );
1994 assert!(
1996 output.contains("timeout: Option<u64>"),
1997 "timeout param should be Option<u64>"
1998 );
1999 assert!(
2000 output.contains("enabled: Option<bool>"),
2001 "enabled param should be Option<bool>"
2002 );
2003 assert!(
2004 output.contains("name: Option<String>"),
2005 "name param should be Option<String>"
2006 );
2007 assert!(output.contains("-> Self {"), "should return Self");
2008 assert!(
2009 output.contains("timeout: timeout.unwrap_or(30),"),
2010 "should apply int default for timeout"
2011 );
2012 assert!(
2013 output.contains("enabled: enabled.unwrap_or(true),"),
2014 "should apply bool default for enabled"
2015 );
2016 assert!(
2017 output.contains("name: name.unwrap_or(\"default\".to_string()),"),
2018 "should apply string default for name"
2019 );
2020 }
2021
2022 #[test]
2023 fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
2024 let mut typ = make_test_type();
2025 typ.fields.push(FieldDef {
2026 name: "tag".to_string(),
2027 ty: TypeRef::String,
2028 optional: true,
2029 default: None,
2030 doc: String::new(),
2031 sanitized: false,
2032 is_boxed: false,
2033 type_rust_path: None,
2034 cfg: None,
2035 typed_default: None,
2036 core_wrapper: CoreWrapper::None,
2037 vec_inner_core_wrapper: CoreWrapper::None,
2038 newtype_wrapper: None,
2039 serde_rename: None,
2040 serde_flatten: false,
2041 binding_excluded: false,
2042 binding_exclusion_reason: None,
2043 });
2044 let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
2045 assert!(
2046 output.contains("tag,"),
2047 "optional field should be passed through directly"
2048 );
2049 assert!(!output.contains("tag.unwrap"), "optional field should not call unwrap");
2050 }
2051
2052 #[test]
2053 fn test_gen_php_kwargs_constructor_unwrap_or_default_for_primitive() {
2054 let mut typ = make_test_type();
2055 typ.fields.push(FieldDef {
2056 name: "retries".to_string(),
2057 ty: TypeRef::Primitive(PrimitiveType::U32),
2058 optional: false,
2059 default: None,
2060 doc: String::new(),
2061 sanitized: false,
2062 is_boxed: false,
2063 type_rust_path: None,
2064 cfg: None,
2065 typed_default: None,
2066 core_wrapper: CoreWrapper::None,
2067 vec_inner_core_wrapper: CoreWrapper::None,
2068 newtype_wrapper: None,
2069 serde_rename: None,
2070 serde_flatten: false,
2071 binding_excluded: false,
2072 binding_exclusion_reason: None,
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 binding_excluded: false,
2127 binding_exclusion_reason: None,
2128 });
2129 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2130 assert!(
2131 output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
2132 "optional field should decode without unwrap"
2133 );
2134 }
2135
2136 #[test]
2137 fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
2138 let mut typ = make_test_type();
2139 typ.fields.push(FieldDef {
2140 name: "inner".to_string(),
2141 ty: TypeRef::Named("InnerConfig".to_string()),
2142 optional: false,
2143 default: None,
2144 doc: String::new(),
2145 sanitized: false,
2146 is_boxed: false,
2147 type_rust_path: None,
2148 cfg: None,
2149 typed_default: None,
2150 core_wrapper: CoreWrapper::None,
2151 vec_inner_core_wrapper: CoreWrapper::None,
2152 newtype_wrapper: None,
2153 serde_rename: None,
2154 serde_flatten: false,
2155 binding_excluded: false,
2156 binding_exclusion_reason: None,
2157 });
2158 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2159 assert!(
2160 output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2161 "Named type with no default should use unwrap_or_default"
2162 );
2163 }
2164
2165 #[test]
2166 fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
2167 let mut typ = make_test_type();
2170 let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2172 assert!(
2173 output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2174 "String field with quoted default should use unwrap_or_default"
2175 );
2176 typ.fields.push(FieldDef {
2178 name: "label".to_string(),
2179 ty: TypeRef::String,
2180 optional: false,
2181 default: None,
2182 doc: String::new(),
2183 sanitized: false,
2184 is_boxed: false,
2185 type_rust_path: None,
2186 cfg: None,
2187 typed_default: None,
2188 core_wrapper: CoreWrapper::None,
2189 vec_inner_core_wrapper: CoreWrapper::None,
2190 newtype_wrapper: None,
2191 serde_rename: None,
2192 serde_flatten: false,
2193 binding_excluded: false,
2194 binding_exclusion_reason: None,
2195 });
2196 let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
2197 assert!(
2198 output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
2199 "String field with no default should use unwrap_or_default"
2200 );
2201 }
2202
2203 #[test]
2208 fn test_gen_extendr_kwargs_constructor_basic() {
2209 let typ = make_test_type();
2210 let empty_enums = ahash::AHashSet::new();
2211 let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2212
2213 assert!(output.contains("#[extendr]"), "should have extendr attribute");
2214 assert!(
2215 output.contains("pub fn new_config("),
2216 "function name should be lowercase type name"
2217 );
2218 assert!(
2220 output.contains("timeout: Option<u64>"),
2221 "should accept timeout as Option<u64>: {output}"
2222 );
2223 assert!(
2224 output.contains("enabled: Option<bool>"),
2225 "should accept enabled as Option<bool>: {output}"
2226 );
2227 assert!(
2228 output.contains("name: Option<String>"),
2229 "should accept name as Option<String>: {output}"
2230 );
2231 assert!(output.contains("-> Config {"), "should return Config");
2232 assert!(
2233 output.contains("let mut __out = <Config>::default();"),
2234 "should base on Default impl: {output}"
2235 );
2236 assert!(
2237 output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
2238 "should overlay caller-provided timeout"
2239 );
2240 assert!(
2241 output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
2242 "should overlay caller-provided enabled"
2243 );
2244 assert!(
2245 output.contains("if let Some(v) = name { __out.name = v; }"),
2246 "should overlay caller-provided name"
2247 );
2248 }
2249
2250 #[test]
2251 fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
2252 let typ = make_test_type();
2256 let empty_enums = ahash::AHashSet::new();
2257 let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
2258 assert!(
2259 !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
2260 "constructor must not use Rust-syntax param defaults: {output}"
2261 );
2262 }
2263
2264 #[test]
2269 fn test_gen_go_functional_options_skips_tuple_fields() {
2270 let mut typ = make_test_type();
2271 typ.fields.push(FieldDef {
2272 name: "_0".to_string(),
2273 ty: TypeRef::Primitive(PrimitiveType::U32),
2274 optional: false,
2275 default: None,
2276 doc: String::new(),
2277 sanitized: false,
2278 is_boxed: false,
2279 type_rust_path: None,
2280 cfg: None,
2281 typed_default: None,
2282 core_wrapper: CoreWrapper::None,
2283 vec_inner_core_wrapper: CoreWrapper::None,
2284 newtype_wrapper: None,
2285 serde_rename: None,
2286 serde_flatten: false,
2287 binding_excluded: false,
2288 binding_exclusion_reason: None,
2289 });
2290 let output = gen_go_functional_options(&typ, &simple_type_mapper);
2291 assert!(
2292 !output.contains("_0"),
2293 "tuple field _0 should be filtered out from Go output"
2294 );
2295 }
2296
2297 #[test]
2302 fn test_gen_magnus_hash_constructor_generic_type_prefix() {
2303 let fields: Vec<FieldDef> = (0..16)
2305 .map(|i| FieldDef {
2306 name: format!("field_{i}"),
2307 ty: if i == 0 {
2308 TypeRef::Vec(Box::new(TypeRef::String))
2309 } else {
2310 TypeRef::Primitive(PrimitiveType::U32)
2311 },
2312 optional: false,
2313 default: None,
2314 doc: String::new(),
2315 sanitized: false,
2316 is_boxed: false,
2317 type_rust_path: None,
2318 cfg: None,
2319 typed_default: None,
2320 core_wrapper: CoreWrapper::None,
2321 vec_inner_core_wrapper: CoreWrapper::None,
2322 newtype_wrapper: None,
2323 serde_rename: None,
2324 serde_flatten: false,
2325 binding_excluded: false,
2326 binding_exclusion_reason: None,
2327 })
2328 .collect();
2329 let typ = TypeDef {
2330 name: "WideConfig".to_string(),
2331 rust_path: "crate::WideConfig".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 binding_excluded: false,
2348 binding_exclusion_reason: None,
2349 };
2350 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2351 assert!(
2353 output.contains("<Vec<String>>::try_convert"),
2354 "generic types should use UFCS angle-bracket prefix: {output}"
2355 );
2356 }
2357
2358 #[test]
2365 fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
2366 let field = FieldDef {
2370 name: "max_depth".to_string(),
2371 ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2372 optional: true,
2373 default: None,
2374 doc: String::new(),
2375 sanitized: false,
2376 is_boxed: false,
2377 type_rust_path: None,
2378 cfg: None,
2379 typed_default: None,
2380 core_wrapper: CoreWrapper::None,
2381 vec_inner_core_wrapper: CoreWrapper::None,
2382 newtype_wrapper: None,
2383 serde_rename: None,
2384 serde_flatten: false,
2385 binding_excluded: false,
2386 binding_exclusion_reason: None,
2387 };
2388 let mut fields: Vec<FieldDef> = (0..15)
2390 .map(|i| FieldDef {
2391 name: format!("field_{i}"),
2392 ty: TypeRef::Primitive(PrimitiveType::U32),
2393 optional: false,
2394 default: None,
2395 doc: String::new(),
2396 sanitized: false,
2397 is_boxed: false,
2398 type_rust_path: None,
2399 cfg: None,
2400 typed_default: None,
2401 core_wrapper: CoreWrapper::None,
2402 vec_inner_core_wrapper: CoreWrapper::None,
2403 newtype_wrapper: None,
2404 serde_rename: None,
2405 serde_flatten: false,
2406 binding_excluded: false,
2407 binding_exclusion_reason: None,
2408 })
2409 .collect();
2410 fields.push(field);
2411 let typ = TypeDef {
2412 name: "UpdateConfig".to_string(),
2413 rust_path: "crate::UpdateConfig".to_string(),
2414 original_rust_path: String::new(),
2415 fields,
2416 methods: vec![],
2417 is_opaque: false,
2418 is_clone: true,
2419 is_copy: false,
2420 doc: String::new(),
2421 cfg: None,
2422 is_trait: false,
2423 has_default: true,
2424 has_stripped_cfg_fields: false,
2425 is_return_type: false,
2426 serde_rename_all: None,
2427 has_serde: false,
2428 super_traits: vec![],
2429 binding_excluded: false,
2430 binding_exclusion_reason: None,
2431 };
2432 let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
2433 assert!(
2436 !output.contains("Option<Option<"),
2437 "hash constructor must not emit double Option: {output}"
2438 );
2439 assert!(
2440 output.contains("i64::try_convert"),
2441 "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
2442 );
2443 }
2444
2445 #[test]
2446 fn test_magnus_positional_constructor_no_double_option_when_ty_is_optional() {
2447 let field = FieldDef {
2450 name: "max_depth".to_string(),
2451 ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
2452 optional: true,
2453 default: None,
2454 doc: String::new(),
2455 sanitized: false,
2456 is_boxed: false,
2457 type_rust_path: None,
2458 cfg: None,
2459 typed_default: None,
2460 core_wrapper: CoreWrapper::None,
2461 vec_inner_core_wrapper: CoreWrapper::None,
2462 newtype_wrapper: None,
2463 serde_rename: None,
2464 serde_flatten: false,
2465 binding_excluded: false,
2466 binding_exclusion_reason: None,
2467 };
2468 let typ = TypeDef {
2469 name: "SmallUpdate".to_string(),
2470 rust_path: "crate::SmallUpdate".to_string(),
2471 original_rust_path: String::new(),
2472 fields: vec![field],
2473 methods: vec![],
2474 is_opaque: false,
2475 is_clone: true,
2476 is_copy: false,
2477 doc: String::new(),
2478 cfg: None,
2479 is_trait: false,
2480 has_default: true,
2481 has_stripped_cfg_fields: false,
2482 is_return_type: false,
2483 serde_rename_all: None,
2484 has_serde: false,
2485 super_traits: vec![],
2486 binding_excluded: false,
2487 binding_exclusion_reason: None,
2488 };
2489 let output = gen_magnus_positional_constructor(&typ, &simple_type_mapper);
2490 assert!(
2493 !output.contains("Option<Option<"),
2494 "positional constructor must not emit double Option: {output}"
2495 );
2496 assert!(
2497 output.contains("Option<i64>"),
2498 "positional constructor should emit Option<inner> for optional Optional(T): {output}"
2499 );
2500 }
2501}