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