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