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