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