1mod binding_to_core;
2mod core_to_binding;
3mod enums;
4pub(crate) mod helpers;
5
6use ahash::AHashSet;
7
8#[derive(Default, Clone)]
11pub struct ConversionConfig<'a> {
12 pub type_name_prefix: &'a str,
14 pub cast_large_ints_to_i64: bool,
16 pub enum_string_names: Option<&'a AHashSet<String>>,
19 pub map_uses_jsvalue: bool,
23 pub cast_f32_to_f64: bool,
25 pub optionalize_defaults: bool,
29 pub json_to_string: bool,
33 pub json_as_value: bool,
38 pub include_cfg_metadata: bool,
41 pub option_duration_on_defaults: bool,
47 pub binding_enums_have_data: bool,
50 pub exclude_types: &'a [String],
54 pub vec_named_to_string: bool,
59 pub map_as_string: bool,
63 pub opaque_types: Option<&'a AHashSet<String>>,
68 pub from_binding_skip_types: &'a [String],
72 pub source_crate_remaps: &'a [(&'a str, &'a str)],
80 pub binding_field_renames: Option<&'a std::collections::HashMap<String, String>>,
88 pub cast_uints_to_i32: bool,
92 pub cast_large_ints_to_f64: bool,
96 pub untagged_data_enum_names: Option<&'a AHashSet<String>>,
106 pub tagged_data_enum_names: Option<&'a AHashSet<String>>,
117 pub never_skip_cfg_field_names: &'a [String],
121 pub trait_bridge_arc_wrapper_field_names: &'a [String],
127 pub strip_cfg_fields_from_binding_struct: bool,
137 pub binding_tuple_form_for_untagged_variants: bool,
146}
147
148impl<'a> ConversionConfig<'a> {
149 pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
154 where
155 'a: 'b,
156 {
157 let _ = type_name;
162 field_name
163 }
164
165 pub fn trait_bridge_field_is_arc_wrapper(&self, field_name: &str) -> bool {
169 self.trait_bridge_arc_wrapper_field_names
170 .iter()
171 .any(|n| n == field_name)
172 }
173
174 pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
177 if let Some(map) = self.binding_field_renames {
178 let key = format!("{type_name}.{field_name}");
179 if let Some(renamed) = map.get(&key) {
180 return renamed.clone();
181 }
182 }
183 field_name.to_string()
184 }
185}
186
187pub use binding_to_core::{
189 apply_core_wrapper_to_core, field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core,
190 gen_from_binding_to_core_cfg,
191};
192pub use core_to_binding::{
193 field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
194};
195pub use enums::{
196 gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
197 gen_enum_from_core_to_binding_cfg,
198};
199pub use helpers::{
200 apply_crate_remaps, binding_to_core_match_arm, build_type_path_map, can_generate_conversion,
201 can_generate_enum_conversion, can_generate_enum_conversion_from_core, convertible_types, core_enum_path,
202 core_enum_path_remapped, core_to_binding_convertible_types, core_to_binding_match_arm, core_type_path,
203 core_type_path_remapped, field_references_excluded_type, has_sanitized_fields, input_type_names, is_tuple_variant,
204 resolve_named_path,
205};
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use alef_core::ir::*;
211
212 fn simple_type() -> TypeDef {
213 TypeDef {
214 name: "Config".to_string(),
215 rust_path: "my_crate::Config".to_string(),
216 original_rust_path: String::new(),
217 fields: vec![
218 FieldDef {
219 name: "name".into(),
220 ty: TypeRef::String,
221 optional: false,
222 default: None,
223 doc: String::new(),
224 sanitized: false,
225 is_boxed: false,
226 type_rust_path: None,
227 cfg: None,
228 typed_default: None,
229 core_wrapper: CoreWrapper::None,
230 vec_inner_core_wrapper: CoreWrapper::None,
231 newtype_wrapper: None,
232 serde_rename: None,
233 serde_flatten: false,
234 binding_excluded: false,
235 binding_exclusion_reason: None,
236 original_type: None,
237 },
238 FieldDef {
239 name: "timeout".into(),
240 ty: TypeRef::Primitive(PrimitiveType::U64),
241 optional: true,
242 default: None,
243 doc: String::new(),
244 sanitized: false,
245 is_boxed: false,
246 type_rust_path: None,
247 cfg: None,
248 typed_default: None,
249 core_wrapper: CoreWrapper::None,
250 vec_inner_core_wrapper: CoreWrapper::None,
251 newtype_wrapper: None,
252 serde_rename: None,
253 serde_flatten: false,
254 binding_excluded: false,
255 binding_exclusion_reason: None,
256 original_type: None,
257 },
258 FieldDef {
259 name: "backend".into(),
260 ty: TypeRef::Named("Backend".into()),
261 optional: true,
262 default: None,
263 doc: String::new(),
264 sanitized: false,
265 is_boxed: false,
266 type_rust_path: None,
267 cfg: None,
268 typed_default: None,
269 core_wrapper: CoreWrapper::None,
270 vec_inner_core_wrapper: CoreWrapper::None,
271 newtype_wrapper: None,
272 serde_rename: None,
273 serde_flatten: false,
274 binding_excluded: false,
275 binding_exclusion_reason: None,
276 original_type: None,
277 },
278 ],
279 methods: vec![],
280 is_opaque: false,
281 is_clone: true,
282 is_copy: false,
283 is_trait: false,
284 has_default: false,
285 has_stripped_cfg_fields: false,
286 is_return_type: false,
287 serde_rename_all: None,
288 has_serde: false,
289 super_traits: vec![],
290 doc: String::new(),
291 cfg: None,
292 binding_excluded: false,
293 binding_exclusion_reason: None,
294 }
295 }
296
297 fn simple_enum() -> EnumDef {
298 EnumDef {
299 name: "Backend".to_string(),
300 rust_path: "my_crate::Backend".to_string(),
301 original_rust_path: String::new(),
302 variants: vec![
303 EnumVariant {
304 name: "Cpu".into(),
305 fields: vec![],
306 is_tuple: false,
307 doc: String::new(),
308 is_default: false,
309 serde_rename: None,
310 },
311 EnumVariant {
312 name: "Gpu".into(),
313 fields: vec![],
314 is_tuple: false,
315 doc: String::new(),
316 is_default: false,
317 serde_rename: None,
318 },
319 ],
320 doc: String::new(),
321 cfg: None,
322 is_copy: false,
323 has_serde: false,
324 serde_tag: None,
325 serde_untagged: false,
326 serde_rename_all: None,
327 binding_excluded: false,
328 binding_exclusion_reason: None,
329 }
330 }
331
332 #[test]
333 fn test_from_binding_to_core() {
334 let typ = simple_type();
335 let result = gen_from_binding_to_core(&typ, "my_crate");
336 assert!(result.contains("impl From<Config> for my_crate::Config"));
337 assert!(result.contains("name: val.name"));
338 assert!(result.contains("timeout: val.timeout"));
339 assert!(result.contains("backend: val.backend.map(Into::into)"));
340 }
341
342 #[test]
343 fn test_from_core_to_binding() {
344 let typ = simple_type();
345 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
346 assert!(result.contains("impl From<my_crate::Config> for Config"));
347 }
348
349 #[test]
350 fn test_enum_from_binding_to_core() {
351 let enum_def = simple_enum();
352 let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
353 assert!(result.contains("impl From<Backend> for my_crate::Backend"));
354 assert!(result.contains("Backend::Cpu => Self::Cpu"));
355 assert!(result.contains("Backend::Gpu => Self::Gpu"));
356 }
357
358 #[test]
359 fn test_enum_from_core_to_binding() {
360 let enum_def = simple_enum();
361 let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
362 assert!(result.contains("impl From<my_crate::Backend> for Backend"));
363 assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
364 assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
365 }
366
367 fn untagged_tuple_enum() -> EnumDef {
368 EnumDef {
369 name: "UserContent".to_string(),
370 rust_path: "my_crate::UserContent".to_string(),
371 original_rust_path: String::new(),
372 variants: vec![
373 EnumVariant {
374 name: "Text".into(),
375 fields: vec![FieldDef {
376 name: "_0".into(),
377 ty: TypeRef::String,
378 optional: false,
379 default: None,
380 doc: String::new(),
381 sanitized: false,
382 is_boxed: false,
383 type_rust_path: None,
384 cfg: None,
385 typed_default: None,
386 core_wrapper: CoreWrapper::None,
387 vec_inner_core_wrapper: CoreWrapper::None,
388 newtype_wrapper: None,
389 serde_rename: None,
390 serde_flatten: false,
391 binding_excluded: false,
392 binding_exclusion_reason: None,
393 original_type: None,
394 }],
395 is_tuple: true,
396 doc: String::new(),
397 is_default: false,
398 serde_rename: None,
399 },
400 EnumVariant {
401 name: "Parts".into(),
402 fields: vec![FieldDef {
403 name: "_0".into(),
404 ty: TypeRef::Vec(Box::new(TypeRef::String)),
405 optional: false,
406 default: None,
407 doc: String::new(),
408 sanitized: false,
409 is_boxed: false,
410 type_rust_path: None,
411 cfg: None,
412 typed_default: None,
413 core_wrapper: CoreWrapper::None,
414 vec_inner_core_wrapper: CoreWrapper::None,
415 newtype_wrapper: None,
416 serde_rename: None,
417 serde_flatten: false,
418 binding_excluded: false,
419 binding_exclusion_reason: None,
420 original_type: None,
421 }],
422 is_tuple: true,
423 doc: String::new(),
424 is_default: false,
425 serde_rename: None,
426 },
427 ],
428 doc: String::new(),
429 cfg: None,
430 is_copy: false,
431 has_serde: true,
432 serde_tag: None,
433 serde_untagged: true,
434 serde_rename_all: None,
435 binding_excluded: false,
436 binding_exclusion_reason: None,
437 }
438 }
439
440 #[test]
441 fn test_enum_from_binding_to_core_untagged_tuple_emits_tuple_pattern() {
442 let enum_def = untagged_tuple_enum();
446 let config = ConversionConfig {
447 binding_enums_have_data: true,
448 binding_tuple_form_for_untagged_variants: true,
449 ..ConversionConfig::default()
450 };
451 let result = gen_enum_from_binding_to_core_cfg(&enum_def, "my_crate", &config);
452 assert!(
454 result.contains("UserContent::Text(_0)"),
455 "expected tuple-form binding pattern, got: {result}"
456 );
457 assert!(
458 !result.contains("UserContent::Text { _0 }"),
459 "must NOT use struct-form for untagged enums, got: {result}"
460 );
461 assert!(result.contains("Self::Text("));
463 }
464
465 #[test]
466 fn test_enum_from_core_to_binding_untagged_tuple_emits_tuple_constructor() {
467 let enum_def = untagged_tuple_enum();
470 let config = ConversionConfig {
471 binding_enums_have_data: true,
472 binding_tuple_form_for_untagged_variants: true,
473 ..ConversionConfig::default()
474 };
475 let result = gen_enum_from_core_to_binding_cfg(&enum_def, "my_crate", &config);
476 assert!(
478 result.contains("Self::Text(_0)"),
479 "expected tuple-form binding constructor, got: {result}"
480 );
481 assert!(
482 !result.contains("Self::Text { _0 }"),
483 "must NOT use struct-form constructor for untagged enums, got: {result}"
484 );
485 }
486
487 #[test]
488 fn test_enum_tagged_data_keeps_struct_form_pattern() {
489 let mut enum_def = untagged_tuple_enum();
492 enum_def.serde_untagged = false;
493 enum_def.serde_tag = Some("type".to_string());
494 let config = ConversionConfig {
495 binding_enums_have_data: true,
496 binding_tuple_form_for_untagged_variants: true,
497 ..ConversionConfig::default()
498 };
499 let result = gen_enum_from_binding_to_core_cfg(&enum_def, "my_crate", &config);
500 assert!(
501 result.contains("UserContent::Text { _0 }"),
502 "tagged enums must keep struct-form, got: {result}"
503 );
504 }
505
506 #[test]
507 fn test_enum_untagged_keeps_struct_form_when_backend_does_not_opt_in() {
508 let enum_def = untagged_tuple_enum();
513 let config = ConversionConfig {
514 binding_enums_have_data: true,
515 binding_tuple_form_for_untagged_variants: false,
516 ..ConversionConfig::default()
517 };
518 let result = gen_enum_from_binding_to_core_cfg(&enum_def, "my_crate", &config);
519 assert!(
520 result.contains("UserContent::Text { _0 }"),
521 "backends without the opt-in must keep struct-form, got: {result}"
522 );
523 let result2 = gen_enum_from_core_to_binding_cfg(&enum_def, "my_crate", &config);
524 assert!(
525 result2.contains("Self::Text { _0:"),
526 "backends without the opt-in must construct struct-form, got: {result2}"
527 );
528 }
529
530 #[test]
531 fn test_from_binding_to_core_with_cfg_gated_field() {
532 let mut typ = simple_type();
534 typ.has_stripped_cfg_fields = true;
535 typ.fields.push(FieldDef {
536 name: "layout".into(),
537 ty: TypeRef::String,
538 optional: false,
539 default: None,
540 doc: String::new(),
541 sanitized: false,
542 is_boxed: false,
543 type_rust_path: None,
544 cfg: Some("feature = \"layout-detection\"".into()),
545 typed_default: None,
546 core_wrapper: CoreWrapper::None,
547 vec_inner_core_wrapper: CoreWrapper::None,
548 newtype_wrapper: None,
549 serde_rename: None,
550 serde_flatten: false,
551 binding_excluded: false,
552 binding_exclusion_reason: None,
553 original_type: None,
554 });
555
556 let result = gen_from_binding_to_core(&typ, "my_crate");
557
558 assert!(result.contains("impl From<Config> for my_crate::Config"));
560 assert!(result.contains("name: val.name"));
562 assert!(result.contains("timeout: val.timeout"));
563 assert!(result.contains("layout: val.layout"));
566 }
567
568 #[test]
569 fn test_from_core_to_binding_with_cfg_gated_field() {
570 let mut typ = simple_type();
572 typ.fields.push(FieldDef {
573 name: "layout".into(),
574 ty: TypeRef::String,
575 optional: false,
576 default: None,
577 doc: String::new(),
578 sanitized: false,
579 is_boxed: false,
580 type_rust_path: None,
581 cfg: Some("feature = \"layout-detection\"".into()),
582 typed_default: None,
583 core_wrapper: CoreWrapper::None,
584 vec_inner_core_wrapper: CoreWrapper::None,
585 newtype_wrapper: None,
586 serde_rename: None,
587 serde_flatten: false,
588 binding_excluded: false,
589 binding_exclusion_reason: None,
590 original_type: None,
591 });
592
593 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
594
595 assert!(result.contains("impl From<my_crate::Config> for Config"));
597 assert!(result.contains("name: val.name"));
599 assert!(result.contains("layout: val.layout"));
601 }
602
603 #[test]
604 fn test_field_conversion_from_core_map_named_non_optional() {
605 let result = field_conversion_from_core(
607 "tags",
608 &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
609 false,
610 false,
611 &AHashSet::new(),
612 );
613 assert_eq!(
614 result,
615 "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
616 );
617 }
618
619 #[test]
620 fn test_field_conversion_from_core_option_map_named() {
621 let result = field_conversion_from_core(
623 "tags",
624 &TypeRef::Optional(Box::new(TypeRef::Map(
625 Box::new(TypeRef::String),
626 Box::new(TypeRef::Named("Tag".into())),
627 ))),
628 false,
629 false,
630 &AHashSet::new(),
631 );
632 assert_eq!(
633 result,
634 "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
635 );
636 }
637
638 #[test]
639 fn test_field_conversion_from_core_vec_named_non_optional() {
640 let result = field_conversion_from_core(
642 "items",
643 &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
644 false,
645 false,
646 &AHashSet::new(),
647 );
648 assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
649 }
650
651 #[test]
652 fn test_field_conversion_from_core_option_vec_named() {
653 let result = field_conversion_from_core(
655 "items",
656 &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
657 false,
658 false,
659 &AHashSet::new(),
660 );
661 assert_eq!(
662 result,
663 "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
664 );
665 }
666
667 #[test]
668 fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
669 let result = field_conversion_to_core(
672 "patterns",
673 &TypeRef::Map(
674 Box::new(TypeRef::String),
675 Box::new(TypeRef::Named("ExtractionPattern".into())),
676 ),
677 true,
678 );
679 assert!(
680 result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
681 "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
682 );
683 assert_eq!(
684 result,
685 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
686 );
687 }
688
689 #[test]
690 fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
691 use super::binding_to_core::gen_optionalized_field_to_core;
694 let config = ConversionConfig::default();
695 let result = gen_optionalized_field_to_core(
696 "patterns",
697 &TypeRef::Map(
698 Box::new(TypeRef::String),
699 Box::new(TypeRef::Named("ExtractionPattern".into())),
700 ),
701 &config,
702 true,
703 );
704 assert!(
705 result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
706 "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
707 );
708 assert_eq!(
709 result,
710 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
711 );
712 }
713
714 #[test]
715 fn test_optionalized_defaultable_struct_uses_core_default_as_base() {
716 let mut typ = simple_type();
717 typ.has_default = true;
718 typ.fields = vec![
719 FieldDef {
720 name: "language".into(),
721 ty: TypeRef::String,
722 optional: false,
723 default: None,
724 doc: String::new(),
725 sanitized: false,
726 is_boxed: false,
727 type_rust_path: None,
728 cfg: None,
729 typed_default: None,
730 core_wrapper: CoreWrapper::Cow,
731 vec_inner_core_wrapper: CoreWrapper::None,
732 newtype_wrapper: None,
733 serde_rename: None,
734 serde_flatten: false,
735 binding_excluded: false,
736 binding_exclusion_reason: None,
737 original_type: None,
738 },
739 FieldDef {
740 name: "structure".into(),
741 ty: TypeRef::Primitive(PrimitiveType::Bool),
742 optional: false,
743 default: None,
744 doc: String::new(),
745 sanitized: false,
746 is_boxed: false,
747 type_rust_path: None,
748 cfg: None,
749 typed_default: None,
750 core_wrapper: CoreWrapper::None,
751 vec_inner_core_wrapper: CoreWrapper::None,
752 newtype_wrapper: None,
753 serde_rename: None,
754 serde_flatten: false,
755 binding_excluded: false,
756 binding_exclusion_reason: None,
757 original_type: None,
758 },
759 ];
760 let config = ConversionConfig {
761 type_name_prefix: "Js",
762 optionalize_defaults: true,
763 ..ConversionConfig::default()
764 };
765
766 let result = gen_from_binding_to_core_cfg(&typ, "my_crate", &config);
767
768 assert!(result.contains("let mut __result = my_crate::Config::default();"));
769 assert!(result.contains("if let Some(__v) = val.language { __result.language = __v.into(); }"));
770 assert!(result.contains("if let Some(__v) = val.structure { __result.structure = __v; }"));
771 assert!(!result.contains("unwrap_or_default()"));
772 }
773
774 fn arc_field_type(field: FieldDef) -> TypeDef {
775 TypeDef {
776 name: "State".to_string(),
777 rust_path: "my_crate::State".to_string(),
778 original_rust_path: String::new(),
779 fields: vec![field],
780 methods: vec![],
781 is_opaque: false,
782 is_clone: true,
783 is_copy: false,
784 is_trait: false,
785 has_default: false,
786 has_stripped_cfg_fields: false,
787 is_return_type: false,
788 serde_rename_all: None,
789 has_serde: false,
790 super_traits: vec![],
791 doc: String::new(),
792 cfg: None,
793 binding_excluded: false,
794 binding_exclusion_reason: None,
795 }
796 }
797
798 fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
799 FieldDef {
800 name: name.into(),
801 ty,
802 optional,
803 default: None,
804 doc: String::new(),
805 sanitized: false,
806 is_boxed: false,
807 type_rust_path: None,
808 cfg: None,
809 typed_default: None,
810 core_wrapper: CoreWrapper::Arc,
811 vec_inner_core_wrapper: CoreWrapper::None,
812 newtype_wrapper: None,
813 serde_rename: None,
814 serde_flatten: false,
815 binding_excluded: false,
816 binding_exclusion_reason: None,
817 original_type: None,
818 }
819 }
820
821 #[test]
825 fn test_arc_json_option_field_no_double_chain() {
826 let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
827 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
828 assert!(
829 result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
830 "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
831 );
832 assert!(
833 !result.contains("map(ToString::to_string).map("),
834 "must not chain a second map() on top of ToString::to_string, got: {result}"
835 );
836 }
837
838 #[test]
840 fn test_arc_json_non_optional_field() {
841 let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
842 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
843 assert!(
844 result.contains("(*val.spec).clone().to_string()"),
845 "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
846 );
847 }
848
849 #[test]
852 fn test_arc_string_option_field_passthrough() {
853 let typ = arc_field_type(arc_field("label", TypeRef::String, true));
854 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
855 assert!(
856 result.contains("val.label.map(|v| (*v).clone().into())"),
857 "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
858 );
859 }
860}