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