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}
138
139impl<'a> ConversionConfig<'a> {
140 pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
145 where
146 'a: 'b,
147 {
148 let _ = type_name;
153 field_name
154 }
155
156 pub fn trait_bridge_field_is_arc_wrapper(&self, field_name: &str) -> bool {
160 self.trait_bridge_arc_wrapper_field_names
161 .iter()
162 .any(|n| n == field_name)
163 }
164
165 pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
168 if let Some(map) = self.binding_field_renames {
169 let key = format!("{type_name}.{field_name}");
170 if let Some(renamed) = map.get(&key) {
171 return renamed.clone();
172 }
173 }
174 field_name.to_string()
175 }
176}
177
178pub use binding_to_core::{
180 apply_core_wrapper_to_core, field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core,
181 gen_from_binding_to_core_cfg,
182};
183pub use core_to_binding::{
184 field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
185};
186pub use enums::{
187 gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
188 gen_enum_from_core_to_binding_cfg,
189};
190pub use helpers::{
191 apply_crate_remaps, binding_to_core_match_arm, build_type_path_map, can_generate_conversion,
192 can_generate_enum_conversion, can_generate_enum_conversion_from_core, convertible_types, core_enum_path,
193 core_enum_path_remapped, core_to_binding_convertible_types, core_to_binding_match_arm, core_type_path,
194 core_type_path_remapped, field_references_excluded_type, has_sanitized_fields, input_type_names, is_tuple_variant,
195 resolve_named_path,
196};
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use alef_core::ir::*;
202
203 fn simple_type() -> TypeDef {
204 TypeDef {
205 name: "Config".to_string(),
206 rust_path: "my_crate::Config".to_string(),
207 original_rust_path: String::new(),
208 fields: vec![
209 FieldDef {
210 name: "name".into(),
211 ty: TypeRef::String,
212 optional: false,
213 default: None,
214 doc: String::new(),
215 sanitized: false,
216 is_boxed: false,
217 type_rust_path: None,
218 cfg: None,
219 typed_default: None,
220 core_wrapper: CoreWrapper::None,
221 vec_inner_core_wrapper: CoreWrapper::None,
222 newtype_wrapper: None,
223 serde_rename: None,
224 serde_flatten: false,
225 binding_excluded: false,
226 binding_exclusion_reason: None,
227 },
228 FieldDef {
229 name: "timeout".into(),
230 ty: TypeRef::Primitive(PrimitiveType::U64),
231 optional: true,
232 default: None,
233 doc: String::new(),
234 sanitized: false,
235 is_boxed: false,
236 type_rust_path: None,
237 cfg: None,
238 typed_default: None,
239 core_wrapper: CoreWrapper::None,
240 vec_inner_core_wrapper: CoreWrapper::None,
241 newtype_wrapper: None,
242 serde_rename: None,
243 serde_flatten: false,
244 binding_excluded: false,
245 binding_exclusion_reason: None,
246 },
247 FieldDef {
248 name: "backend".into(),
249 ty: TypeRef::Named("Backend".into()),
250 optional: true,
251 default: None,
252 doc: String::new(),
253 sanitized: false,
254 is_boxed: false,
255 type_rust_path: None,
256 cfg: None,
257 typed_default: None,
258 core_wrapper: CoreWrapper::None,
259 vec_inner_core_wrapper: CoreWrapper::None,
260 newtype_wrapper: None,
261 serde_rename: None,
262 serde_flatten: false,
263 binding_excluded: false,
264 binding_exclusion_reason: None,
265 },
266 ],
267 methods: vec![],
268 is_opaque: false,
269 is_clone: true,
270 is_copy: false,
271 is_trait: false,
272 has_default: false,
273 has_stripped_cfg_fields: false,
274 is_return_type: false,
275 serde_rename_all: None,
276 has_serde: false,
277 super_traits: vec![],
278 doc: String::new(),
279 cfg: None,
280 binding_excluded: false,
281 binding_exclusion_reason: None,
282 }
283 }
284
285 fn simple_enum() -> EnumDef {
286 EnumDef {
287 name: "Backend".to_string(),
288 rust_path: "my_crate::Backend".to_string(),
289 original_rust_path: String::new(),
290 variants: vec![
291 EnumVariant {
292 name: "Cpu".into(),
293 fields: vec![],
294 is_tuple: false,
295 doc: String::new(),
296 is_default: false,
297 serde_rename: None,
298 },
299 EnumVariant {
300 name: "Gpu".into(),
301 fields: vec![],
302 is_tuple: false,
303 doc: String::new(),
304 is_default: false,
305 serde_rename: None,
306 },
307 ],
308 doc: String::new(),
309 cfg: None,
310 is_copy: false,
311 has_serde: false,
312 serde_tag: None,
313 serde_untagged: false,
314 serde_rename_all: None,
315 binding_excluded: false,
316 binding_exclusion_reason: None,
317 }
318 }
319
320 #[test]
321 fn test_from_binding_to_core() {
322 let typ = simple_type();
323 let result = gen_from_binding_to_core(&typ, "my_crate");
324 assert!(result.contains("impl From<Config> for my_crate::Config"));
325 assert!(result.contains("name: val.name"));
326 assert!(result.contains("timeout: val.timeout"));
327 assert!(result.contains("backend: val.backend.map(Into::into)"));
328 }
329
330 #[test]
331 fn test_from_core_to_binding() {
332 let typ = simple_type();
333 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
334 assert!(result.contains("impl From<my_crate::Config> for Config"));
335 }
336
337 #[test]
338 fn test_enum_from_binding_to_core() {
339 let enum_def = simple_enum();
340 let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
341 assert!(result.contains("impl From<Backend> for my_crate::Backend"));
342 assert!(result.contains("Backend::Cpu => Self::Cpu"));
343 assert!(result.contains("Backend::Gpu => Self::Gpu"));
344 }
345
346 #[test]
347 fn test_enum_from_core_to_binding() {
348 let enum_def = simple_enum();
349 let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
350 assert!(result.contains("impl From<my_crate::Backend> for Backend"));
351 assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
352 assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
353 }
354
355 #[test]
356 fn test_from_binding_to_core_with_cfg_gated_field() {
357 let mut typ = simple_type();
359 typ.has_stripped_cfg_fields = true;
360 typ.fields.push(FieldDef {
361 name: "layout".into(),
362 ty: TypeRef::String,
363 optional: false,
364 default: None,
365 doc: String::new(),
366 sanitized: false,
367 is_boxed: false,
368 type_rust_path: None,
369 cfg: Some("feature = \"layout-detection\"".into()),
370 typed_default: None,
371 core_wrapper: CoreWrapper::None,
372 vec_inner_core_wrapper: CoreWrapper::None,
373 newtype_wrapper: None,
374 serde_rename: None,
375 serde_flatten: false,
376 binding_excluded: false,
377 binding_exclusion_reason: None,
378 });
379
380 let result = gen_from_binding_to_core(&typ, "my_crate");
381
382 assert!(result.contains("impl From<Config> for my_crate::Config"));
384 assert!(result.contains("name: val.name"));
386 assert!(result.contains("timeout: val.timeout"));
387 assert!(result.contains("layout: val.layout"));
390 }
391
392 #[test]
393 fn test_from_core_to_binding_with_cfg_gated_field() {
394 let mut typ = simple_type();
396 typ.fields.push(FieldDef {
397 name: "layout".into(),
398 ty: TypeRef::String,
399 optional: false,
400 default: None,
401 doc: String::new(),
402 sanitized: false,
403 is_boxed: false,
404 type_rust_path: None,
405 cfg: Some("feature = \"layout-detection\"".into()),
406 typed_default: None,
407 core_wrapper: CoreWrapper::None,
408 vec_inner_core_wrapper: CoreWrapper::None,
409 newtype_wrapper: None,
410 serde_rename: None,
411 serde_flatten: false,
412 binding_excluded: false,
413 binding_exclusion_reason: None,
414 });
415
416 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
417
418 assert!(result.contains("impl From<my_crate::Config> for Config"));
420 assert!(result.contains("name: val.name"));
422 assert!(result.contains("layout: val.layout"));
424 }
425
426 #[test]
427 fn test_field_conversion_from_core_map_named_non_optional() {
428 let result = field_conversion_from_core(
430 "tags",
431 &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
432 false,
433 false,
434 &AHashSet::new(),
435 );
436 assert_eq!(
437 result,
438 "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
439 );
440 }
441
442 #[test]
443 fn test_field_conversion_from_core_option_map_named() {
444 let result = field_conversion_from_core(
446 "tags",
447 &TypeRef::Optional(Box::new(TypeRef::Map(
448 Box::new(TypeRef::String),
449 Box::new(TypeRef::Named("Tag".into())),
450 ))),
451 false,
452 false,
453 &AHashSet::new(),
454 );
455 assert_eq!(
456 result,
457 "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
458 );
459 }
460
461 #[test]
462 fn test_field_conversion_from_core_vec_named_non_optional() {
463 let result = field_conversion_from_core(
465 "items",
466 &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
467 false,
468 false,
469 &AHashSet::new(),
470 );
471 assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
472 }
473
474 #[test]
475 fn test_field_conversion_from_core_option_vec_named() {
476 let result = field_conversion_from_core(
478 "items",
479 &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
480 false,
481 false,
482 &AHashSet::new(),
483 );
484 assert_eq!(
485 result,
486 "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
487 );
488 }
489
490 #[test]
491 fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
492 let result = field_conversion_to_core(
495 "patterns",
496 &TypeRef::Map(
497 Box::new(TypeRef::String),
498 Box::new(TypeRef::Named("ExtractionPattern".into())),
499 ),
500 true,
501 );
502 assert!(
503 result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
504 "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
505 );
506 assert_eq!(
507 result,
508 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
509 );
510 }
511
512 #[test]
513 fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
514 use super::binding_to_core::gen_optionalized_field_to_core;
517 let config = ConversionConfig::default();
518 let result = gen_optionalized_field_to_core(
519 "patterns",
520 &TypeRef::Map(
521 Box::new(TypeRef::String),
522 Box::new(TypeRef::Named("ExtractionPattern".into())),
523 ),
524 &config,
525 true,
526 );
527 assert!(
528 result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
529 "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
530 );
531 assert_eq!(
532 result,
533 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
534 );
535 }
536
537 #[test]
538 fn test_optionalized_defaultable_struct_uses_core_default_as_base() {
539 let mut typ = simple_type();
540 typ.has_default = true;
541 typ.fields = vec![
542 FieldDef {
543 name: "language".into(),
544 ty: TypeRef::String,
545 optional: false,
546 default: None,
547 doc: String::new(),
548 sanitized: false,
549 is_boxed: false,
550 type_rust_path: None,
551 cfg: None,
552 typed_default: None,
553 core_wrapper: CoreWrapper::Cow,
554 vec_inner_core_wrapper: CoreWrapper::None,
555 newtype_wrapper: None,
556 serde_rename: None,
557 serde_flatten: false,
558 binding_excluded: false,
559 binding_exclusion_reason: None,
560 },
561 FieldDef {
562 name: "structure".into(),
563 ty: TypeRef::Primitive(PrimitiveType::Bool),
564 optional: false,
565 default: None,
566 doc: String::new(),
567 sanitized: false,
568 is_boxed: false,
569 type_rust_path: None,
570 cfg: None,
571 typed_default: None,
572 core_wrapper: CoreWrapper::None,
573 vec_inner_core_wrapper: CoreWrapper::None,
574 newtype_wrapper: None,
575 serde_rename: None,
576 serde_flatten: false,
577 binding_excluded: false,
578 binding_exclusion_reason: None,
579 },
580 ];
581 let config = ConversionConfig {
582 type_name_prefix: "Js",
583 optionalize_defaults: true,
584 ..ConversionConfig::default()
585 };
586
587 let result = gen_from_binding_to_core_cfg(&typ, "my_crate", &config);
588
589 assert!(result.contains("let mut __result = my_crate::Config::default();"));
590 assert!(result.contains("if let Some(__v) = val.language { __result.language = __v.into(); }"));
591 assert!(result.contains("if let Some(__v) = val.structure { __result.structure = __v; }"));
592 assert!(!result.contains("unwrap_or_default()"));
593 }
594
595 fn arc_field_type(field: FieldDef) -> TypeDef {
596 TypeDef {
597 name: "State".to_string(),
598 rust_path: "my_crate::State".to_string(),
599 original_rust_path: String::new(),
600 fields: vec![field],
601 methods: vec![],
602 is_opaque: false,
603 is_clone: true,
604 is_copy: false,
605 is_trait: false,
606 has_default: false,
607 has_stripped_cfg_fields: false,
608 is_return_type: false,
609 serde_rename_all: None,
610 has_serde: false,
611 super_traits: vec![],
612 doc: String::new(),
613 cfg: None,
614 binding_excluded: false,
615 binding_exclusion_reason: None,
616 }
617 }
618
619 fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
620 FieldDef {
621 name: name.into(),
622 ty,
623 optional,
624 default: None,
625 doc: String::new(),
626 sanitized: false,
627 is_boxed: false,
628 type_rust_path: None,
629 cfg: None,
630 typed_default: None,
631 core_wrapper: CoreWrapper::Arc,
632 vec_inner_core_wrapper: CoreWrapper::None,
633 newtype_wrapper: None,
634 serde_rename: None,
635 serde_flatten: false,
636 binding_excluded: false,
637 binding_exclusion_reason: None,
638 }
639 }
640
641 #[test]
645 fn test_arc_json_option_field_no_double_chain() {
646 let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
647 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
648 assert!(
649 result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
650 "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
651 );
652 assert!(
653 !result.contains("map(ToString::to_string).map("),
654 "must not chain a second map() on top of ToString::to_string, got: {result}"
655 );
656 }
657
658 #[test]
660 fn test_arc_json_non_optional_field() {
661 let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
662 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
663 assert!(
664 result.contains("(*val.spec).clone().to_string()"),
665 "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
666 );
667 }
668
669 #[test]
672 fn test_arc_string_option_field_passthrough() {
673 let typ = arc_field_type(arc_field("label", TypeRef::String, true));
674 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
675 assert!(
676 result.contains("val.label.map(|v| (*v).clone().into())"),
677 "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
678 );
679 }
680}