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