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