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