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 original_type: None,
228 },
229 FieldDef {
230 name: "timeout".into(),
231 ty: TypeRef::Primitive(PrimitiveType::U64),
232 optional: true,
233 default: None,
234 doc: String::new(),
235 sanitized: false,
236 is_boxed: false,
237 type_rust_path: None,
238 cfg: None,
239 typed_default: None,
240 core_wrapper: CoreWrapper::None,
241 vec_inner_core_wrapper: CoreWrapper::None,
242 newtype_wrapper: None,
243 serde_rename: None,
244 serde_flatten: false,
245 binding_excluded: false,
246 binding_exclusion_reason: None,
247 original_type: None,
248 },
249 FieldDef {
250 name: "backend".into(),
251 ty: TypeRef::Named("Backend".into()),
252 optional: true,
253 default: None,
254 doc: String::new(),
255 sanitized: false,
256 is_boxed: false,
257 type_rust_path: None,
258 cfg: None,
259 typed_default: None,
260 core_wrapper: CoreWrapper::None,
261 vec_inner_core_wrapper: CoreWrapper::None,
262 newtype_wrapper: None,
263 serde_rename: None,
264 serde_flatten: false,
265 binding_excluded: false,
266 binding_exclusion_reason: None,
267 original_type: None,
268 },
269 ],
270 methods: vec![],
271 is_opaque: false,
272 is_clone: true,
273 is_copy: false,
274 is_trait: false,
275 has_default: false,
276 has_stripped_cfg_fields: false,
277 is_return_type: false,
278 serde_rename_all: None,
279 has_serde: false,
280 super_traits: vec![],
281 doc: String::new(),
282 cfg: None,
283 binding_excluded: false,
284 binding_exclusion_reason: None,
285 }
286 }
287
288 fn simple_enum() -> EnumDef {
289 EnumDef {
290 name: "Backend".to_string(),
291 rust_path: "my_crate::Backend".to_string(),
292 original_rust_path: String::new(),
293 variants: vec![
294 EnumVariant {
295 name: "Cpu".into(),
296 fields: vec![],
297 is_tuple: false,
298 doc: String::new(),
299 is_default: false,
300 serde_rename: None,
301 },
302 EnumVariant {
303 name: "Gpu".into(),
304 fields: vec![],
305 is_tuple: false,
306 doc: String::new(),
307 is_default: false,
308 serde_rename: None,
309 },
310 ],
311 doc: String::new(),
312 cfg: None,
313 is_copy: false,
314 has_serde: false,
315 serde_tag: None,
316 serde_untagged: false,
317 serde_rename_all: None,
318 binding_excluded: false,
319 binding_exclusion_reason: None,
320 }
321 }
322
323 #[test]
324 fn test_from_binding_to_core() {
325 let typ = simple_type();
326 let result = gen_from_binding_to_core(&typ, "my_crate");
327 assert!(result.contains("impl From<Config> for my_crate::Config"));
328 assert!(result.contains("name: val.name"));
329 assert!(result.contains("timeout: val.timeout"));
330 assert!(result.contains("backend: val.backend.map(Into::into)"));
331 }
332
333 #[test]
334 fn test_from_core_to_binding() {
335 let typ = simple_type();
336 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
337 assert!(result.contains("impl From<my_crate::Config> for Config"));
338 }
339
340 #[test]
341 fn test_enum_from_binding_to_core() {
342 let enum_def = simple_enum();
343 let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
344 assert!(result.contains("impl From<Backend> for my_crate::Backend"));
345 assert!(result.contains("Backend::Cpu => Self::Cpu"));
346 assert!(result.contains("Backend::Gpu => Self::Gpu"));
347 }
348
349 #[test]
350 fn test_enum_from_core_to_binding() {
351 let enum_def = simple_enum();
352 let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
353 assert!(result.contains("impl From<my_crate::Backend> for Backend"));
354 assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
355 assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
356 }
357
358 #[test]
359 fn test_from_binding_to_core_with_cfg_gated_field() {
360 let mut typ = simple_type();
362 typ.has_stripped_cfg_fields = true;
363 typ.fields.push(FieldDef {
364 name: "layout".into(),
365 ty: TypeRef::String,
366 optional: false,
367 default: None,
368 doc: String::new(),
369 sanitized: false,
370 is_boxed: false,
371 type_rust_path: None,
372 cfg: Some("feature = \"layout-detection\"".into()),
373 typed_default: None,
374 core_wrapper: CoreWrapper::None,
375 vec_inner_core_wrapper: CoreWrapper::None,
376 newtype_wrapper: None,
377 serde_rename: None,
378 serde_flatten: false,
379 binding_excluded: false,
380 binding_exclusion_reason: None,
381 original_type: None,
382 });
383
384 let result = gen_from_binding_to_core(&typ, "my_crate");
385
386 assert!(result.contains("impl From<Config> for my_crate::Config"));
388 assert!(result.contains("name: val.name"));
390 assert!(result.contains("timeout: val.timeout"));
391 assert!(result.contains("layout: val.layout"));
394 }
395
396 #[test]
397 fn test_from_core_to_binding_with_cfg_gated_field() {
398 let mut typ = simple_type();
400 typ.fields.push(FieldDef {
401 name: "layout".into(),
402 ty: TypeRef::String,
403 optional: false,
404 default: None,
405 doc: String::new(),
406 sanitized: false,
407 is_boxed: false,
408 type_rust_path: None,
409 cfg: Some("feature = \"layout-detection\"".into()),
410 typed_default: None,
411 core_wrapper: CoreWrapper::None,
412 vec_inner_core_wrapper: CoreWrapper::None,
413 newtype_wrapper: None,
414 serde_rename: None,
415 serde_flatten: false,
416 binding_excluded: false,
417 binding_exclusion_reason: None,
418 original_type: None,
419 });
420
421 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
422
423 assert!(result.contains("impl From<my_crate::Config> for Config"));
425 assert!(result.contains("name: val.name"));
427 assert!(result.contains("layout: val.layout"));
429 }
430
431 #[test]
432 fn test_field_conversion_from_core_map_named_non_optional() {
433 let result = field_conversion_from_core(
435 "tags",
436 &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
437 false,
438 false,
439 &AHashSet::new(),
440 );
441 assert_eq!(
442 result,
443 "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
444 );
445 }
446
447 #[test]
448 fn test_field_conversion_from_core_option_map_named() {
449 let result = field_conversion_from_core(
451 "tags",
452 &TypeRef::Optional(Box::new(TypeRef::Map(
453 Box::new(TypeRef::String),
454 Box::new(TypeRef::Named("Tag".into())),
455 ))),
456 false,
457 false,
458 &AHashSet::new(),
459 );
460 assert_eq!(
461 result,
462 "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
463 );
464 }
465
466 #[test]
467 fn test_field_conversion_from_core_vec_named_non_optional() {
468 let result = field_conversion_from_core(
470 "items",
471 &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
472 false,
473 false,
474 &AHashSet::new(),
475 );
476 assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
477 }
478
479 #[test]
480 fn test_field_conversion_from_core_option_vec_named() {
481 let result = field_conversion_from_core(
483 "items",
484 &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
485 false,
486 false,
487 &AHashSet::new(),
488 );
489 assert_eq!(
490 result,
491 "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
492 );
493 }
494
495 #[test]
496 fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
497 let result = field_conversion_to_core(
500 "patterns",
501 &TypeRef::Map(
502 Box::new(TypeRef::String),
503 Box::new(TypeRef::Named("ExtractionPattern".into())),
504 ),
505 true,
506 );
507 assert!(
508 result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
509 "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
510 );
511 assert_eq!(
512 result,
513 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
514 );
515 }
516
517 #[test]
518 fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
519 use super::binding_to_core::gen_optionalized_field_to_core;
522 let config = ConversionConfig::default();
523 let result = gen_optionalized_field_to_core(
524 "patterns",
525 &TypeRef::Map(
526 Box::new(TypeRef::String),
527 Box::new(TypeRef::Named("ExtractionPattern".into())),
528 ),
529 &config,
530 true,
531 );
532 assert!(
533 result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
534 "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
535 );
536 assert_eq!(
537 result,
538 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
539 );
540 }
541
542 #[test]
543 fn test_optionalized_defaultable_struct_uses_core_default_as_base() {
544 let mut typ = simple_type();
545 typ.has_default = true;
546 typ.fields = vec![
547 FieldDef {
548 name: "language".into(),
549 ty: TypeRef::String,
550 optional: false,
551 default: None,
552 doc: String::new(),
553 sanitized: false,
554 is_boxed: false,
555 type_rust_path: None,
556 cfg: None,
557 typed_default: None,
558 core_wrapper: CoreWrapper::Cow,
559 vec_inner_core_wrapper: CoreWrapper::None,
560 newtype_wrapper: None,
561 serde_rename: None,
562 serde_flatten: false,
563 binding_excluded: false,
564 binding_exclusion_reason: None,
565 original_type: None,
566 },
567 FieldDef {
568 name: "structure".into(),
569 ty: TypeRef::Primitive(PrimitiveType::Bool),
570 optional: false,
571 default: None,
572 doc: String::new(),
573 sanitized: false,
574 is_boxed: false,
575 type_rust_path: None,
576 cfg: None,
577 typed_default: None,
578 core_wrapper: CoreWrapper::None,
579 vec_inner_core_wrapper: CoreWrapper::None,
580 newtype_wrapper: None,
581 serde_rename: None,
582 serde_flatten: false,
583 binding_excluded: false,
584 binding_exclusion_reason: None,
585 original_type: None,
586 },
587 ];
588 let config = ConversionConfig {
589 type_name_prefix: "Js",
590 optionalize_defaults: true,
591 ..ConversionConfig::default()
592 };
593
594 let result = gen_from_binding_to_core_cfg(&typ, "my_crate", &config);
595
596 assert!(result.contains("let mut __result = my_crate::Config::default();"));
597 assert!(result.contains("if let Some(__v) = val.language { __result.language = __v.into(); }"));
598 assert!(result.contains("if let Some(__v) = val.structure { __result.structure = __v; }"));
599 assert!(!result.contains("unwrap_or_default()"));
600 }
601
602 fn arc_field_type(field: FieldDef) -> TypeDef {
603 TypeDef {
604 name: "State".to_string(),
605 rust_path: "my_crate::State".to_string(),
606 original_rust_path: String::new(),
607 fields: vec![field],
608 methods: vec![],
609 is_opaque: false,
610 is_clone: true,
611 is_copy: false,
612 is_trait: false,
613 has_default: false,
614 has_stripped_cfg_fields: false,
615 is_return_type: false,
616 serde_rename_all: None,
617 has_serde: false,
618 super_traits: vec![],
619 doc: String::new(),
620 cfg: None,
621 binding_excluded: false,
622 binding_exclusion_reason: None,
623 }
624 }
625
626 fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
627 FieldDef {
628 name: name.into(),
629 ty,
630 optional,
631 default: None,
632 doc: String::new(),
633 sanitized: false,
634 is_boxed: false,
635 type_rust_path: None,
636 cfg: None,
637 typed_default: None,
638 core_wrapper: CoreWrapper::Arc,
639 vec_inner_core_wrapper: CoreWrapper::None,
640 newtype_wrapper: None,
641 serde_rename: None,
642 serde_flatten: false,
643 binding_excluded: false,
644 binding_exclusion_reason: None,
645 original_type: None,
646 }
647 }
648
649 #[test]
653 fn test_arc_json_option_field_no_double_chain() {
654 let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
655 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
656 assert!(
657 result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
658 "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
659 );
660 assert!(
661 !result.contains("map(ToString::to_string).map("),
662 "must not chain a second map() on top of ToString::to_string, got: {result}"
663 );
664 }
665
666 #[test]
668 fn test_arc_json_non_optional_field() {
669 let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
670 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
671 assert!(
672 result.contains("(*val.spec).clone().to_string()"),
673 "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
674 );
675 }
676
677 #[test]
680 fn test_arc_string_option_field_passthrough() {
681 let typ = arc_field_type(arc_field("label", TypeRef::String, true));
682 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
683 assert!(
684 result.contains("val.label.map(|v| (*v).clone().into())"),
685 "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
686 );
687 }
688}