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