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
225fn render_conversions(
230 iface_kebab: &str,
231 structs: &[(Vec<String>, syn::ItemStruct)],
232 enums: &[(Vec<String>, syn::ItemEnum)],
233 known: &BTreeSet<String>,
234) -> String {
235 if structs.is_empty() && enums.is_empty() {
236 return String::new();
237 }
238 let snake = iface_kebab.replace('-', "_");
239 let gen_path = format!("exports::fidius::{snake}::{snake}");
240 let mut out = String::new();
241 out.push_str("// Generated by fidius-wit: author <-> wit-bindgen conversions.\n");
242
243 for (path, s) in structs {
244 let name = s.ident.to_string();
245 let g = format!("{gen_path}::{name}");
246 let a = author_path(path, &name);
247 let fields: Vec<String> = match &s.fields {
248 syn::Fields::Named(f) => f
249 .named
250 .iter()
251 .map(|fl| fl.ident.as_ref().unwrap().to_string())
252 .collect(),
253 _ => Vec::new(),
254 };
255 let field_types: Vec<&Type> = match &s.fields {
256 syn::Fields::Named(f) => f.named.iter().map(|fl| &fl.ty).collect(),
257 _ => Vec::new(),
258 };
259 let to_author: Vec<String> = fields
261 .iter()
262 .zip(&field_types)
263 .map(|(f, ty)| format!("{f}: {}", conv_expr(&format!("v.{f}"), ty, known)))
264 .collect();
265 out.push_str(&format!(
266 "impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ {a} {{ {} }} }} }}\n",
267 to_author.join(", ")
268 ));
269 let to_gen: Vec<String> = fields
271 .iter()
272 .zip(&field_types)
273 .map(|(f, ty)| format!("{f}: {}", conv_expr(&format!("v.{f}"), ty, known)))
274 .collect();
275 out.push_str(&format!(
276 "impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ {g} {{ {} }} }} }}\n",
277 to_gen.join(", ")
278 ));
279 }
280
281 for (path, e) in enums {
282 let ename = e.ident.to_string();
283 let g = format!("{gen_path}::{ename}");
284 let a = author_path(path, &ename);
285
286 let mut g2a = Vec::new(); let mut a2g = Vec::new(); for v in &e.variants {
289 let case = v.ident.to_string();
290 match &v.fields {
291 syn::Fields::Unit => {
292 g2a.push(format!("{g}::{case} => {a}::{case}"));
293 a2g.push(format!("{a}::{case} => {g}::{case}"));
294 }
295 syn::Fields::Unnamed(u) if u.unnamed.len() == 1 => {
296 let ty = &u.unnamed[0].ty;
297 g2a.push(format!(
298 "{g}::{case}(x) => {a}::{case}({})",
299 conv_expr("x", ty, known)
300 ));
301 a2g.push(format!(
302 "{a}::{case}(x) => {g}::{case}({})",
303 conv_expr("x", ty, known)
304 ));
305 }
306 syn::Fields::Named(f) => {
307 let gen_rec = format!("{gen_path}::{ename}{case}");
311 let fnames: Vec<String> = f
312 .named
313 .iter()
314 .map(|fl| fl.ident.as_ref().unwrap().to_string())
315 .collect();
316 let g2a_inits = fnames
317 .iter()
318 .zip(f.named.iter())
319 .map(|(n, fl)| {
320 format!("{n}: {}", conv_expr(&format!("__r.{n}"), &fl.ty, known))
321 })
322 .collect::<Vec<_>>()
323 .join(", ");
324 g2a.push(format!("{g}::{case}(__r) => {a}::{case} {{ {g2a_inits} }}"));
325
326 let binds = fnames.join(", ");
327 let a2g_inits = fnames
328 .iter()
329 .zip(f.named.iter())
330 .map(|(n, fl)| format!("{n}: {}", conv_expr(n, &fl.ty, known)))
331 .collect::<Vec<_>>()
332 .join(", ");
333 a2g.push(format!(
334 "{a}::{case} {{ {binds} }} => {g}::{case}({gen_rec} {{ {a2g_inits} }})"
335 ));
336 }
337 syn::Fields::Unnamed(_) => unreachable!("rejected by enum_to_wit"),
339 }
340 }
341 out.push_str(&format!(
342 "impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ match v {{ {} }} }} }}\n",
343 g2a.join(", ")
344 ));
345 out.push_str(&format!(
346 "impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ match v {{ {} }} }} }}\n",
347 a2g.join(", ")
348 ));
349 }
350 out
351}
352
353pub fn conv_expr(access: &str, ty: &Type, known: &BTreeSet<String>) -> String {
360 if let Type::Path(p) = ty {
365 if let Some(seg) = p.path.segments.last() {
366 if matches!(seg.ident.to_string().as_str(), "HashMap" | "BTreeMap") {
367 if let Some((k, v)) = two_generics(seg) {
368 let kc = conv_expr("k", k, known);
369 let vc = conv_expr("v", v, known);
370 if kc == "k" && vc == "v" {
371 return format!("{access}.into_iter().collect()");
372 }
373 return format!("{access}.into_iter().map(|(k, v)| ({kc}, {vc})).collect()");
374 }
375 }
376 }
377 }
378 if !contains_user_type(ty, known) {
379 return access.to_string();
380 }
381 if let Type::Path(p) = ty {
382 if let Some(seg) = p.path.segments.last() {
383 let ident = seg.ident.to_string();
384 if let Some(inner) = single_generic(seg) {
385 match ident.as_str() {
386 "Vec" => {
387 return format!(
388 "{access}.into_iter().map(|w| {}).collect()",
389 conv_expr("w", inner, known)
390 );
391 }
392 "Option" => {
393 return format!("{access}.map(|w| {})", conv_expr("w", inner, known));
394 }
395 _ => {}
396 }
397 }
398 if known.contains(&ident) {
399 return format!("{access}.into()");
400 }
401 }
402 }
403 access.to_string()
404}
405
406pub fn contains_user_type(ty: &Type, known: &BTreeSet<String>) -> bool {
409 if let Type::Path(p) = ty {
410 if let Some(seg) = p.path.segments.last() {
411 let ident = seg.ident.to_string();
412 if known.contains(&ident) {
413 return true;
414 }
415 if matches!(ident.as_str(), "Vec" | "Option" | "Box") {
416 if let Some(inner) = single_generic(seg) {
417 return contains_user_type(inner, known);
418 }
419 }
420 if matches!(ident.as_str(), "HashMap" | "BTreeMap") {
421 if let Some((k, v)) = two_generics(seg) {
422 return contains_user_type(k, known) || contains_user_type(v, known);
423 }
424 }
425 }
426 }
427 false
428}
429
430fn single_generic(seg: &syn::PathSegment) -> Option<&Type> {
431 if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
432 for a in &ab.args {
433 if let syn::GenericArgument::Type(t) = a {
434 return Some(t);
435 }
436 }
437 }
438 None
439}
440
441fn two_generics(seg: &syn::PathSegment) -> Option<(&Type, &Type)> {
442 if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
443 let types: Vec<&Type> = ab
444 .args
445 .iter()
446 .filter_map(|a| match a {
447 syn::GenericArgument::Type(t) => Some(t),
448 _ => None,
449 })
450 .collect();
451 if types.len() >= 2 {
452 return Some((types[0], types[1]));
453 }
454 }
455 None
456}
457
458fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
460 attrs.iter().any(|a| {
461 a.path()
462 .segments
463 .last()
464 .map(|s| s.ident == name)
465 .unwrap_or(false)
466 })
467}
468
469fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
471 for a in attrs {
472 if !a.path().is_ident("derive") {
473 continue;
474 }
475 let mut found = false;
476 let _ = a.parse_nested_meta(|m| {
477 if m.path
478 .segments
479 .last()
480 .map(|s| s.ident == name)
481 .unwrap_or(false)
482 {
483 found = true;
484 }
485 Ok(())
486 });
487 if found {
488 return true;
489 }
490 }
491 false
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 const SRC: &str = r#"
499 #[derive(WitType)]
500 pub struct Point { pub x: i32, pub y: i32 }
501
502 #[derive(WitType)]
503 pub enum Shape { Circle(u32), Rect(Point), Dot }
504
505 #[plugin_interface(version = 1, crate = "fidius_guest")]
506 pub trait Geo: Send + Sync {
507 fn midpoint(&self, a: Point, b: Point) -> Point;
508 fn classify(&self, pts: Vec<Point>) -> Shape;
509 fn name(&self, s: Shape) -> String;
510 }
511 "#;
512
513 #[test]
514 fn generates_wit_with_records_variants_and_funcs() {
515 let g = generate(SRC).unwrap();
516 assert_eq!(g.interface_name, "Geo");
517 assert_eq!(g.iface_kebab, "geo");
518 assert!(g.user_types.contains(&"Point".to_string()));
519 assert!(g.user_types.contains(&"Shape".to_string()));
520
521 assert!(g.wit.contains("record point {"));
522 assert!(g.wit.contains("variant shape {"));
523 assert!(g.wit.contains("rect(point),"));
524 assert!(g
525 .wit
526 .contains("midpoint: func(a: point, b: point) -> point;"));
527 assert!(g.wit.contains("classify: func(pts: list<point>) -> shape;"));
528 assert!(g.wit.contains("name: func(s: shape) -> string;"));
529 assert!(g.wit.contains("fidius-interface-hash: func() -> u64;"));
530 }
531
532 #[test]
533 fn generates_conversions_both_ways() {
534 let g = generate(SRC).unwrap();
535 let c = &g.conversions;
536 assert!(c.contains("From<exports::fidius::geo::geo::Point> for crate::Point"));
538 assert!(c.contains("From<crate::Point> for exports::fidius::geo::geo::Point"));
539 assert!(c.contains("From<exports::fidius::geo::geo::Shape> for crate::Shape"));
541 assert!(
542 c.contains("Rect(x) => crate :: Shape :: Rect")
543 || c.contains("Rect(x) => crate::Shape::Rect")
544 );
545 assert!(c.contains(".into()"));
546 }
547
548 #[test]
549 fn primitive_only_interface_has_no_conversions() {
550 let src = r#"
551 #[plugin_interface(version = 1)]
552 pub trait Greeter { fn greet(&self, name: String) -> String; }
553 "#;
554 let g = generate(src).unwrap();
555 assert!(g.user_types.is_empty());
556 assert!(g.conversions.is_empty());
557 assert!(g.wit.contains("greet: func(name: string) -> string;"));
558 }
559
560 #[test]
561 fn unsupported_type_errors() {
562 let src = r#"
565 #[plugin_interface(version = 1)]
566 pub trait T { fn f(&self, x: Box<String>) -> u32; }
567 "#;
568 assert!(generate(src).is_err());
569 }
570}