1use std::collections::BTreeSet;
25
26use syn::{FnArg, Item, Pat, ReturnType, TraitItem, Type};
27
28use crate::{
29 enum_to_wit, return_to_wit_with, struct_to_record, to_kebab_case, wit_type_with, WitMethod,
30};
31
32pub struct Generated {
34 pub interface_name: String,
36 pub iface_kebab: String,
38 pub user_types: Vec<String>,
40 pub wit: String,
42 pub conversions: String,
45}
46
47pub fn generate(src: &str) -> Result<Generated, String> {
51 let file = syn::parse_file(src).map_err(|e| format!("parse error: {e}"))?;
52 let mut acc = Collected::default();
53 collect(&file.items, &[], None, &mut acc)?;
54 assemble(acc)
55}
56
57pub fn generate_from_path(lib_rs: &std::path::Path) -> Result<Generated, String> {
61 let src = std::fs::read_to_string(lib_rs)
62 .map_err(|e| format!("reading {}: {e}", lib_rs.display()))?;
63 let file = syn::parse_file(&src).map_err(|e| format!("parse {}: {e}", lib_rs.display()))?;
64 let dir = lib_rs.parent().unwrap_or_else(|| std::path::Path::new("."));
65 let mut acc = Collected::default();
66 collect(&file.items, &[], Some(dir), &mut acc)?;
67 assemble(acc)
68}
69
70#[derive(Default)]
73struct Collected {
74 structs: Vec<(Vec<String>, syn::ItemStruct)>,
75 enums: Vec<(Vec<String>, syn::ItemEnum)>,
76 the_trait: Option<syn::ItemTrait>,
77}
78
79fn collect(
82 items: &[Item],
83 mod_path: &[String],
84 dir: Option<&std::path::Path>,
85 acc: &mut Collected,
86) -> Result<(), String> {
87 for item in items {
88 match item {
89 Item::Struct(s) if has_derive(&s.attrs, "WitType") => {
90 acc.structs.push((mod_path.to_vec(), s.clone()));
91 }
92 Item::Enum(e) if has_derive(&e.attrs, "WitType") => {
93 acc.enums.push((mod_path.to_vec(), e.clone()));
94 }
95 Item::Trait(t) if has_attr(&t.attrs, "plugin_interface") => {
96 if acc.the_trait.is_some() {
97 return Err("multiple #[plugin_interface] traits found".into());
98 }
99 acc.the_trait = Some(t.clone());
100 }
101 Item::Mod(m) => {
102 let mut child = mod_path.to_vec();
103 child.push(m.ident.to_string());
104 if let Some((_, items)) = &m.content {
105 let sub = dir.map(|d| d.join(m.ident.to_string()));
106 collect(items, &child, sub.as_deref(), acc)?;
107 } else if let Some(d) = dir {
108 let name = m.ident.to_string();
109 let candidates = [d.join(format!("{name}.rs")), d.join(&name).join("mod.rs")];
110 let file = candidates.iter().find(|p| p.exists()).ok_or_else(|| {
111 format!(
112 "cannot find module file for `mod {name};` near {}",
113 d.display()
114 )
115 })?;
116 let src = std::fs::read_to_string(file)
117 .map_err(|e| format!("reading {}: {e}", file.display()))?;
118 let parsed = syn::parse_file(&src)
119 .map_err(|e| format!("parse {}: {e}", file.display()))?;
120 collect(&parsed.items, &child, Some(&d.join(&name)), acc)?;
121 }
122 }
123 _ => {}
124 }
125 }
126 Ok(())
127}
128
129fn assemble(acc: Collected) -> Result<Generated, String> {
131 let the_trait = acc
132 .the_trait
133 .ok_or("no #[plugin_interface] trait found in source")?;
134 let interface_name = the_trait.ident.to_string();
135 let iface_kebab = to_kebab_case(&interface_name);
136
137 let mut user_types: Vec<String> = Vec::new();
138 user_types.extend(acc.structs.iter().map(|(_, s)| s.ident.to_string()));
139 user_types.extend(acc.enums.iter().map(|(_, e)| e.ident.to_string()));
140 let known: BTreeSet<String> = user_types.iter().cloned().collect();
141
142 let mut type_defs: Vec<String> = Vec::new();
145 let mut variant_defs: Vec<String> = Vec::new();
146 for (_, s) in &acc.structs {
147 type_defs.push(struct_to_record(s, &known)?);
148 }
149 for (_, e) in &acc.enums {
150 let (synthetic, variant) = enum_to_wit(e, &known)?;
151 type_defs.extend(synthetic);
152 variant_defs.push(variant);
153 }
154 type_defs.extend(variant_defs);
155
156 let mut methods: Vec<WitMethod> = Vec::new();
157 for item in &the_trait.items {
158 let TraitItem::Fn(f) = item else { continue };
159 let mut params = Vec::new();
160 for arg in &f.sig.inputs {
161 let FnArg::Typed(pt) = arg else { continue }; if crate::stream_item_type(&pt.ty).is_some() {
167 continue;
168 }
169 let name = match pt.pat.as_ref() {
170 Pat::Ident(id) => to_kebab_case(&id.ident.to_string()),
171 _ => "arg".to_string(),
172 };
173 let wt = wit_type_with(&pt.ty, &known)
174 .map_err(|e| format!("method `{}` arg `{name}`: {e}", f.sig.ident))?;
175 params.push((name, wt));
176 }
177 let ret_ty: Option<&Type> = match &f.sig.output {
178 ReturnType::Type(_, t) => Some(t.as_ref()),
179 ReturnType::Default => None,
180 };
181 let stream_item = ret_ty.and_then(crate::stream_item_type);
184 let (ret, stream_item) = match stream_item {
185 Some(item_ty) => {
186 let item_wit = wit_type_with(item_ty, &known)
187 .map_err(|e| format!("method `{}` stream item: {e}", f.sig.ident))?;
188 (None, Some(item_wit))
189 }
190 None => {
191 let ret = return_to_wit_with(ret_ty, &known)
192 .map_err(|e| format!("method `{}` return: {e}", f.sig.ident))?;
193 (ret, None)
194 }
195 };
196 methods.push(WitMethod {
197 name: to_kebab_case(&f.sig.ident.to_string()),
198 params,
199 ret,
200 stream_item,
201 });
202 }
203
204 let wit = crate::render_wit_full(&iface_kebab, &type_defs, &methods);
205 let conversions = render_conversions(&iface_kebab, &acc.structs, &acc.enums, &known);
206
207 Ok(Generated {
208 interface_name,
209 iface_kebab,
210 user_types,
211 wit,
212 conversions,
213 })
214}
215
216fn author_path(mod_path: &[String], name: &str) -> String {
218 if mod_path.is_empty() {
219 format!("crate::{name}")
220 } else {
221 format!("crate::{}::{name}", mod_path.join("::"))
222 }
223}
224
225const RUST_KEYWORDS: &[&str] = &[
232 "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
233 "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
234 "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
235 "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
236 "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
237];
238
239fn field_idents(fl: &syn::Field) -> (String, String) {
245 let author = fl.ident.as_ref().unwrap().to_string();
246 let bare = author.strip_prefix("r#").unwrap_or(&author);
247 let generated = if RUST_KEYWORDS.contains(&bare) {
248 format!("{bare}_")
249 } else {
250 author.clone()
251 };
252 (author, generated)
253}
254
255fn render_conversions(
260 iface_kebab: &str,
261 structs: &[(Vec<String>, syn::ItemStruct)],
262 enums: &[(Vec<String>, syn::ItemEnum)],
263 known: &BTreeSet<String>,
264) -> String {
265 if structs.is_empty() && enums.is_empty() {
266 return String::new();
267 }
268 let snake = iface_kebab.replace('-', "_");
269 let gen_path = format!("exports::fidius::{snake}::{snake}");
270 let mut out = String::new();
271 out.push_str("// Generated by fidius-wit: author <-> wit-bindgen conversions.\n");
272
273 for (path, s) in structs {
274 let name = s.ident.to_string();
275 let g = format!("{gen_path}::{name}");
276 let a = author_path(path, &name);
277 let fields: Vec<(String, String, &Type)> = match &s.fields {
280 syn::Fields::Named(f) => f
281 .named
282 .iter()
283 .map(|fl| {
284 let (au, gen) = field_idents(fl);
285 (au, gen, &fl.ty)
286 })
287 .collect(),
288 _ => Vec::new(),
289 };
290 let to_author: Vec<String> = fields
292 .iter()
293 .map(|(au, gen, ty)| format!("{au}: {}", conv_expr(&format!("v.{gen}"), ty, known)))
294 .collect();
295 out.push_str(&format!(
296 "impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ {a} {{ {} }} }} }}\n",
297 to_author.join(", ")
298 ));
299 let to_gen: Vec<String> = fields
301 .iter()
302 .map(|(au, gen, ty)| format!("{gen}: {}", conv_expr(&format!("v.{au}"), ty, known)))
303 .collect();
304 out.push_str(&format!(
305 "impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ {g} {{ {} }} }} }}\n",
306 to_gen.join(", ")
307 ));
308 }
309
310 for (path, e) in enums {
311 let ename = e.ident.to_string();
312 let g = format!("{gen_path}::{ename}");
313 let a = author_path(path, &ename);
314
315 let mut g2a = Vec::new(); let mut a2g = Vec::new(); for v in &e.variants {
318 let case = v.ident.to_string();
319 match &v.fields {
320 syn::Fields::Unit => {
321 g2a.push(format!("{g}::{case} => {a}::{case}"));
322 a2g.push(format!("{a}::{case} => {g}::{case}"));
323 }
324 syn::Fields::Unnamed(u) if u.unnamed.len() == 1 => {
325 let ty = &u.unnamed[0].ty;
326 g2a.push(format!(
327 "{g}::{case}(x) => {a}::{case}({})",
328 conv_expr("x", ty, known)
329 ));
330 a2g.push(format!(
331 "{a}::{case}(x) => {g}::{case}({})",
332 conv_expr("x", ty, known)
333 ));
334 }
335 syn::Fields::Named(f) => {
336 let gen_rec = format!("{gen_path}::{ename}{case}");
340 let fnames: Vec<(String, String, &Type)> = f
342 .named
343 .iter()
344 .map(|fl| {
345 let (au, gen) = field_idents(fl);
346 (au, gen, &fl.ty)
347 })
348 .collect();
349 let g2a_inits = fnames
352 .iter()
353 .map(|(au, gen, ty)| {
354 format!("{au}: {}", conv_expr(&format!("__r.{gen}"), ty, known))
355 })
356 .collect::<Vec<_>>()
357 .join(", ");
358 g2a.push(format!("{g}::{case}(__r) => {a}::{case} {{ {g2a_inits} }}"));
359
360 let binds = fnames
363 .iter()
364 .map(|(au, ..)| au.clone())
365 .collect::<Vec<_>>()
366 .join(", ");
367 let a2g_inits = fnames
368 .iter()
369 .map(|(au, gen, ty)| format!("{gen}: {}", conv_expr(au, ty, known)))
370 .collect::<Vec<_>>()
371 .join(", ");
372 a2g.push(format!(
373 "{a}::{case} {{ {binds} }} => {g}::{case}({gen_rec} {{ {a2g_inits} }})"
374 ));
375 }
376 syn::Fields::Unnamed(_) => unreachable!("rejected by enum_to_wit"),
378 }
379 }
380 out.push_str(&format!(
381 "impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ match v {{ {} }} }} }}\n",
382 g2a.join(", ")
383 ));
384 out.push_str(&format!(
385 "impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ match v {{ {} }} }} }}\n",
386 a2g.join(", ")
387 ));
388 }
389 out
390}
391
392pub fn conv_expr(access: &str, ty: &Type, known: &BTreeSet<String>) -> String {
399 if let Type::Path(p) = ty {
404 if let Some(seg) = p.path.segments.last() {
405 if matches!(seg.ident.to_string().as_str(), "HashMap" | "BTreeMap") {
406 if let Some((k, v)) = two_generics(seg) {
407 let kc = conv_expr("k", k, known);
408 let vc = conv_expr("v", v, known);
409 if kc == "k" && vc == "v" {
410 return format!("{access}.into_iter().collect()");
411 }
412 return format!("{access}.into_iter().map(|(k, v)| ({kc}, {vc})).collect()");
413 }
414 }
415 }
416 }
417 if !contains_user_type(ty, known) {
418 return access.to_string();
419 }
420 if let Type::Path(p) = ty {
421 if let Some(seg) = p.path.segments.last() {
422 let ident = seg.ident.to_string();
423 if let Some(inner) = single_generic(seg) {
424 match ident.as_str() {
425 "Vec" => {
426 return format!(
427 "{access}.into_iter().map(|w| {}).collect()",
428 conv_expr("w", inner, known)
429 );
430 }
431 "Option" => {
432 return format!("{access}.map(|w| {})", conv_expr("w", inner, known));
433 }
434 _ => {}
435 }
436 }
437 if known.contains(&ident) {
438 return format!("{access}.into()");
439 }
440 }
441 }
442 access.to_string()
443}
444
445pub fn contains_user_type(ty: &Type, known: &BTreeSet<String>) -> bool {
448 if let Type::Path(p) = ty {
449 if let Some(seg) = p.path.segments.last() {
450 let ident = seg.ident.to_string();
451 if known.contains(&ident) {
452 return true;
453 }
454 if matches!(ident.as_str(), "Vec" | "Option" | "Box") {
455 if let Some(inner) = single_generic(seg) {
456 return contains_user_type(inner, known);
457 }
458 }
459 if matches!(ident.as_str(), "HashMap" | "BTreeMap") {
460 if let Some((k, v)) = two_generics(seg) {
461 return contains_user_type(k, known) || contains_user_type(v, known);
462 }
463 }
464 }
465 }
466 false
467}
468
469fn single_generic(seg: &syn::PathSegment) -> Option<&Type> {
470 if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
471 for a in &ab.args {
472 if let syn::GenericArgument::Type(t) = a {
473 return Some(t);
474 }
475 }
476 }
477 None
478}
479
480fn two_generics(seg: &syn::PathSegment) -> Option<(&Type, &Type)> {
481 if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
482 let types: Vec<&Type> = ab
483 .args
484 .iter()
485 .filter_map(|a| match a {
486 syn::GenericArgument::Type(t) => Some(t),
487 _ => None,
488 })
489 .collect();
490 if types.len() >= 2 {
491 return Some((types[0], types[1]));
492 }
493 }
494 None
495}
496
497fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
499 attrs.iter().any(|a| {
500 a.path()
501 .segments
502 .last()
503 .map(|s| s.ident == name)
504 .unwrap_or(false)
505 })
506}
507
508fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
510 for a in attrs {
511 if !a.path().is_ident("derive") {
512 continue;
513 }
514 let mut found = false;
515 let _ = a.parse_nested_meta(|m| {
516 if m.path
517 .segments
518 .last()
519 .map(|s| s.ident == name)
520 .unwrap_or(false)
521 {
522 found = true;
523 }
524 Ok(())
525 });
526 if found {
527 return true;
528 }
529 }
530 false
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 const SRC: &str = r#"
538 #[derive(WitType)]
539 pub struct Point { pub x: i32, pub y: i32 }
540
541 #[derive(WitType)]
542 pub enum Shape { Circle(u32), Rect(Point), Dot }
543
544 #[plugin_interface(version = 1, crate = "fidius_guest")]
545 pub trait Geo: Send + Sync {
546 fn midpoint(&self, a: Point, b: Point) -> Point;
547 fn classify(&self, pts: Vec<Point>) -> Shape;
548 fn name(&self, s: Shape) -> String;
549 }
550 "#;
551
552 #[test]
553 fn generates_wit_with_records_variants_and_funcs() {
554 let g = generate(SRC).unwrap();
555 assert_eq!(g.interface_name, "Geo");
556 assert_eq!(g.iface_kebab, "geo");
557 assert!(g.user_types.contains(&"Point".to_string()));
558 assert!(g.user_types.contains(&"Shape".to_string()));
559
560 assert!(g.wit.contains("record point {"));
561 assert!(g.wit.contains("variant shape {"));
562 assert!(g.wit.contains("rect(point),"));
563 assert!(g
564 .wit
565 .contains("midpoint: func(a: point, b: point) -> point;"));
566 assert!(g.wit.contains("classify: func(pts: list<point>) -> shape;"));
567 assert!(g.wit.contains("name: func(s: shape) -> string;"));
568 assert!(g.wit.contains("fidius-interface-hash: func() -> u64;"));
569 }
570
571 #[test]
572 fn generates_conversions_both_ways() {
573 let g = generate(SRC).unwrap();
574 let c = &g.conversions;
575 assert!(c.contains("From<exports::fidius::geo::geo::Point> for crate::Point"));
577 assert!(c.contains("From<crate::Point> for exports::fidius::geo::geo::Point"));
578 assert!(c.contains("From<exports::fidius::geo::geo::Shape> for crate::Shape"));
580 assert!(
581 c.contains("Rect(x) => crate :: Shape :: Rect")
582 || c.contains("Rect(x) => crate::Shape::Rect")
583 );
584 assert!(c.contains(".into()"));
585 }
586
587 #[test]
588 fn primitive_only_interface_has_no_conversions() {
589 let src = r#"
590 #[plugin_interface(version = 1)]
591 pub trait Greeter { fn greet(&self, name: String) -> String; }
592 "#;
593 let g = generate(src).unwrap();
594 assert!(g.user_types.is_empty());
595 assert!(g.conversions.is_empty());
596 assert!(g.wit.contains("greet: func(name: string) -> string;"));
597 }
598
599 #[test]
600 fn unsupported_type_errors() {
601 let src = r#"
604 #[plugin_interface(version = 1)]
605 pub trait T { fn f(&self, x: Box<String>) -> u32; }
606 "#;
607 assert!(generate(src).is_err());
608 }
609
610 const KEYWORD_SRC: &str = r#"
616 #[derive(WitType)]
617 pub struct DeadLetter { pub record: String, pub stream: u64, pub from: bool, pub r#type: u8 }
618
619 #[derive(WitType)]
620 pub enum Variant { Stream, Record(u32), List { from: u8 } }
621
622 #[plugin_interface(version = 1, crate = "fidius_guest")]
623 pub trait Stream: Send + Sync {
624 fn record(&self, list: DeadLetter, option: Variant) -> DeadLetter;
625 }
626 "#;
627
628 #[test]
629 fn keyword_heavy_interface_escapes_every_identifier() {
630 let g = generate(KEYWORD_SRC).unwrap();
631 assert!(g.wit.contains("interface %stream {"), "{}", g.wit);
633 assert!(g.wit.contains("package fidius:%stream@0.1.0;"));
634 assert!(g.wit.contains("export %stream;"));
635 assert!(g.wit.contains("record dead-letter {"));
637 assert!(g.wit.contains("%record: string,"));
638 assert!(g.wit.contains("%stream: u64,"));
639 assert!(g.wit.contains("%from: bool,"));
640 assert!(g.wit.contains("%type: u8,"));
641 assert!(g.wit.contains("variant %variant {"));
644 assert!(g.wit.contains("%stream,"));
645 assert!(g.wit.contains("%record(u32),"));
646 assert!(g.wit.contains("%list(variant-list),"));
647 assert!(g.wit.contains("record variant-list {"));
648 assert!(g
650 .wit
651 .contains("%record: func(%list: dead-letter, %option: %variant) -> dead-letter;"));
652 }
653
654 #[test]
655 fn keyword_field_conversions_use_wit_bindgen_mangling() {
656 let c = &generate(KEYWORD_SRC).unwrap().conversions;
660 assert!(c.contains("r#type: v.type_"), "{c}");
662 assert!(c.contains("type_: v.r#type"), "{c}");
664 assert!(c.contains("record: v.record") && c.contains("stream: v.stream"));
666 }
667
668 #[test]
669 fn keyword_heavy_interface_parses() {
670 let g = generate(KEYWORD_SRC).unwrap();
671 wit_parser::UnresolvedPackageGroup::parse("keyword.wit", &g.wit)
674 .unwrap_or_else(|e| panic!("generated WIT must parse: {e}\n---\n{}", g.wit));
675 }
676}