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