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