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