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