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 },
185 FieldDef {
186 name: "timeout".into(),
187 ty: TypeRef::Primitive(PrimitiveType::U64),
188 optional: true,
189 default: None,
190 doc: String::new(),
191 sanitized: false,
192 is_boxed: false,
193 type_rust_path: None,
194 cfg: None,
195 typed_default: None,
196 core_wrapper: CoreWrapper::None,
197 vec_inner_core_wrapper: CoreWrapper::None,
198 newtype_wrapper: None,
199 serde_rename: None,
200 },
201 FieldDef {
202 name: "backend".into(),
203 ty: TypeRef::Named("Backend".into()),
204 optional: true,
205 default: None,
206 doc: String::new(),
207 sanitized: false,
208 is_boxed: false,
209 type_rust_path: None,
210 cfg: None,
211 typed_default: None,
212 core_wrapper: CoreWrapper::None,
213 vec_inner_core_wrapper: CoreWrapper::None,
214 newtype_wrapper: None,
215 serde_rename: None,
216 },
217 ],
218 methods: vec![],
219 is_opaque: false,
220 is_clone: true,
221 is_copy: false,
222 is_trait: false,
223 has_default: false,
224 has_stripped_cfg_fields: false,
225 is_return_type: false,
226 serde_rename_all: None,
227 has_serde: false,
228 super_traits: vec![],
229 doc: String::new(),
230 cfg: None,
231 }
232 }
233
234 fn simple_enum() -> EnumDef {
235 EnumDef {
236 name: "Backend".to_string(),
237 rust_path: "my_crate::Backend".to_string(),
238 original_rust_path: String::new(),
239 variants: vec![
240 EnumVariant {
241 name: "Cpu".into(),
242 fields: vec![],
243 is_tuple: false,
244 doc: String::new(),
245 is_default: false,
246 serde_rename: None,
247 },
248 EnumVariant {
249 name: "Gpu".into(),
250 fields: vec![],
251 is_tuple: false,
252 doc: String::new(),
253 is_default: false,
254 serde_rename: None,
255 },
256 ],
257 doc: String::new(),
258 cfg: None,
259 is_copy: false,
260 has_serde: false,
261 serde_tag: None,
262 serde_untagged: false,
263 serde_rename_all: None,
264 }
265 }
266
267 #[test]
268 fn test_from_binding_to_core() {
269 let typ = simple_type();
270 let result = gen_from_binding_to_core(&typ, "my_crate");
271 assert!(result.contains("impl From<Config> for my_crate::Config"));
272 assert!(result.contains("name: val.name"));
273 assert!(result.contains("timeout: val.timeout"));
274 assert!(result.contains("backend: val.backend.map(Into::into)"));
275 }
276
277 #[test]
278 fn test_from_core_to_binding() {
279 let typ = simple_type();
280 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
281 assert!(result.contains("impl From<my_crate::Config> for Config"));
282 }
283
284 #[test]
285 fn test_enum_from_binding_to_core() {
286 let enum_def = simple_enum();
287 let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
288 assert!(result.contains("impl From<Backend> for my_crate::Backend"));
289 assert!(result.contains("Backend::Cpu => Self::Cpu"));
290 assert!(result.contains("Backend::Gpu => Self::Gpu"));
291 }
292
293 #[test]
294 fn test_enum_from_core_to_binding() {
295 let enum_def = simple_enum();
296 let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
297 assert!(result.contains("impl From<my_crate::Backend> for Backend"));
298 assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
299 assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
300 }
301
302 #[test]
303 fn test_from_binding_to_core_with_cfg_gated_field() {
304 let mut typ = simple_type();
306 typ.has_stripped_cfg_fields = true;
307 typ.fields.push(FieldDef {
308 name: "layout".into(),
309 ty: TypeRef::String,
310 optional: false,
311 default: None,
312 doc: String::new(),
313 sanitized: false,
314 is_boxed: false,
315 type_rust_path: None,
316 cfg: Some("feature = \"layout-detection\"".into()),
317 typed_default: None,
318 core_wrapper: CoreWrapper::None,
319 vec_inner_core_wrapper: CoreWrapper::None,
320 newtype_wrapper: None,
321 serde_rename: None,
322 });
323
324 let result = gen_from_binding_to_core(&typ, "my_crate");
325
326 assert!(result.contains("impl From<Config> for my_crate::Config"));
328 assert!(result.contains("name: val.name"));
330 assert!(result.contains("timeout: val.timeout"));
331 assert!(!result.contains("layout: val.layout"));
333 assert!(result.contains("..Default::default()"));
335 }
336
337 #[test]
338 fn test_from_core_to_binding_with_cfg_gated_field() {
339 let mut typ = simple_type();
341 typ.fields.push(FieldDef {
342 name: "layout".into(),
343 ty: TypeRef::String,
344 optional: false,
345 default: None,
346 doc: String::new(),
347 sanitized: false,
348 is_boxed: false,
349 type_rust_path: None,
350 cfg: Some("feature = \"layout-detection\"".into()),
351 typed_default: None,
352 core_wrapper: CoreWrapper::None,
353 vec_inner_core_wrapper: CoreWrapper::None,
354 newtype_wrapper: None,
355 serde_rename: None,
356 });
357
358 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
359
360 assert!(result.contains("impl From<my_crate::Config> for Config"));
362 assert!(result.contains("name: val.name"));
364 assert!(!result.contains("layout:"));
366 }
367
368 #[test]
369 fn test_field_conversion_from_core_map_named_non_optional() {
370 let result = field_conversion_from_core(
372 "tags",
373 &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
374 false,
375 false,
376 &AHashSet::new(),
377 );
378 assert_eq!(
379 result,
380 "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
381 );
382 }
383
384 #[test]
385 fn test_field_conversion_from_core_option_map_named() {
386 let result = field_conversion_from_core(
388 "tags",
389 &TypeRef::Optional(Box::new(TypeRef::Map(
390 Box::new(TypeRef::String),
391 Box::new(TypeRef::Named("Tag".into())),
392 ))),
393 false,
394 false,
395 &AHashSet::new(),
396 );
397 assert_eq!(
398 result,
399 "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
400 );
401 }
402
403 #[test]
404 fn test_field_conversion_from_core_vec_named_non_optional() {
405 let result = field_conversion_from_core(
407 "items",
408 &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
409 false,
410 false,
411 &AHashSet::new(),
412 );
413 assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
414 }
415
416 #[test]
417 fn test_field_conversion_from_core_option_vec_named() {
418 let result = field_conversion_from_core(
420 "items",
421 &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
422 false,
423 false,
424 &AHashSet::new(),
425 );
426 assert_eq!(
427 result,
428 "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
429 );
430 }
431
432 #[test]
433 fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
434 let result = field_conversion_to_core(
437 "patterns",
438 &TypeRef::Map(
439 Box::new(TypeRef::String),
440 Box::new(TypeRef::Named("ExtractionPattern".into())),
441 ),
442 true,
443 );
444 assert!(
445 result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
446 "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
447 );
448 assert_eq!(
449 result,
450 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
451 );
452 }
453
454 #[test]
455 fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
456 use super::binding_to_core::gen_optionalized_field_to_core;
459 let config = ConversionConfig::default();
460 let result = gen_optionalized_field_to_core(
461 "patterns",
462 &TypeRef::Map(
463 Box::new(TypeRef::String),
464 Box::new(TypeRef::Named("ExtractionPattern".into())),
465 ),
466 &config,
467 true,
468 );
469 assert!(
470 result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
471 "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
472 );
473 assert_eq!(
474 result,
475 "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
476 );
477 }
478
479 #[test]
480 fn test_optionalized_defaultable_struct_uses_core_default_as_base() {
481 let mut typ = simple_type();
482 typ.has_default = true;
483 typ.fields = vec![
484 FieldDef {
485 name: "language".into(),
486 ty: TypeRef::String,
487 optional: false,
488 default: None,
489 doc: String::new(),
490 sanitized: false,
491 is_boxed: false,
492 type_rust_path: None,
493 cfg: None,
494 typed_default: None,
495 core_wrapper: CoreWrapper::Cow,
496 vec_inner_core_wrapper: CoreWrapper::None,
497 newtype_wrapper: None,
498 serde_rename: None,
499 },
500 FieldDef {
501 name: "structure".into(),
502 ty: TypeRef::Primitive(PrimitiveType::Bool),
503 optional: false,
504 default: None,
505 doc: String::new(),
506 sanitized: false,
507 is_boxed: false,
508 type_rust_path: None,
509 cfg: None,
510 typed_default: None,
511 core_wrapper: CoreWrapper::None,
512 vec_inner_core_wrapper: CoreWrapper::None,
513 newtype_wrapper: None,
514 serde_rename: None,
515 },
516 ];
517 let config = ConversionConfig {
518 type_name_prefix: "Js",
519 optionalize_defaults: true,
520 ..ConversionConfig::default()
521 };
522
523 let result = gen_from_binding_to_core_cfg(&typ, "my_crate", &config);
524
525 assert!(result.contains("let mut __result = my_crate::Config::default();"));
526 assert!(result.contains("if let Some(__v) = val.language { __result.language = __v.into(); }"));
527 assert!(result.contains("if let Some(__v) = val.structure { __result.structure = __v; }"));
528 assert!(!result.contains("unwrap_or_default()"));
529 }
530
531 fn arc_field_type(field: FieldDef) -> TypeDef {
532 TypeDef {
533 name: "State".to_string(),
534 rust_path: "my_crate::State".to_string(),
535 original_rust_path: String::new(),
536 fields: vec![field],
537 methods: vec![],
538 is_opaque: false,
539 is_clone: true,
540 is_copy: false,
541 is_trait: false,
542 has_default: false,
543 has_stripped_cfg_fields: false,
544 is_return_type: false,
545 serde_rename_all: None,
546 has_serde: false,
547 super_traits: vec![],
548 doc: String::new(),
549 cfg: None,
550 }
551 }
552
553 fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
554 FieldDef {
555 name: name.into(),
556 ty,
557 optional,
558 default: None,
559 doc: String::new(),
560 sanitized: false,
561 is_boxed: false,
562 type_rust_path: None,
563 cfg: None,
564 typed_default: None,
565 core_wrapper: CoreWrapper::Arc,
566 vec_inner_core_wrapper: CoreWrapper::None,
567 newtype_wrapper: None,
568 serde_rename: None,
569 }
570 }
571
572 #[test]
576 fn test_arc_json_option_field_no_double_chain() {
577 let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
578 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
579 assert!(
580 result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
581 "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
582 );
583 assert!(
584 !result.contains("map(ToString::to_string).map("),
585 "must not chain a second map() on top of ToString::to_string, got: {result}"
586 );
587 }
588
589 #[test]
591 fn test_arc_json_non_optional_field() {
592 let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
593 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
594 assert!(
595 result.contains("(*val.spec).clone().to_string()"),
596 "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
597 );
598 }
599
600 #[test]
603 fn test_arc_string_option_field_passthrough() {
604 let typ = arc_field_type(arc_field("label", TypeRef::String, true));
605 let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
606 assert!(
607 result.contains("val.label.map(|v| (*v).clone().into())"),
608 "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
609 );
610 }
611}