use std::collections::BTreeSet;
use syn::{FnArg, Item, Pat, ReturnType, TraitItem, Type};
use crate::{
enum_to_wit, return_to_wit_with, struct_to_record, to_kebab_case, wit_type_with, WitMethod,
};
pub struct Generated {
pub interface_name: String,
pub iface_kebab: String,
pub user_types: Vec<String>,
pub wit: String,
pub conversions: String,
}
pub fn generate(src: &str) -> Result<Generated, String> {
let file = syn::parse_file(src).map_err(|e| format!("parse error: {e}"))?;
let mut acc = Collected::default();
collect(&file.items, &[], None, &mut acc)?;
assemble(acc)
}
pub fn generate_from_path(lib_rs: &std::path::Path) -> Result<Generated, String> {
let src = std::fs::read_to_string(lib_rs)
.map_err(|e| format!("reading {}: {e}", lib_rs.display()))?;
let file = syn::parse_file(&src).map_err(|e| format!("parse {}: {e}", lib_rs.display()))?;
let dir = lib_rs.parent().unwrap_or_else(|| std::path::Path::new("."));
let mut acc = Collected::default();
collect(&file.items, &[], Some(dir), &mut acc)?;
assemble(acc)
}
#[derive(Default)]
struct Collected {
structs: Vec<(Vec<String>, syn::ItemStruct)>,
enums: Vec<(Vec<String>, syn::ItemEnum)>,
the_trait: Option<syn::ItemTrait>,
}
fn collect(
items: &[Item],
mod_path: &[String],
dir: Option<&std::path::Path>,
acc: &mut Collected,
) -> Result<(), String> {
for item in items {
match item {
Item::Struct(s) if has_derive(&s.attrs, "WitType") => {
acc.structs.push((mod_path.to_vec(), s.clone()));
}
Item::Enum(e) if has_derive(&e.attrs, "WitType") => {
acc.enums.push((mod_path.to_vec(), e.clone()));
}
Item::Trait(t) if has_attr(&t.attrs, "plugin_interface") => {
if acc.the_trait.is_some() {
return Err("multiple #[plugin_interface] traits found".into());
}
acc.the_trait = Some(t.clone());
}
Item::Mod(m) => {
let mut child = mod_path.to_vec();
child.push(m.ident.to_string());
if let Some((_, items)) = &m.content {
let sub = dir.map(|d| d.join(m.ident.to_string()));
collect(items, &child, sub.as_deref(), acc)?;
} else if let Some(d) = dir {
let name = m.ident.to_string();
let candidates = [d.join(format!("{name}.rs")), d.join(&name).join("mod.rs")];
let file = candidates.iter().find(|p| p.exists()).ok_or_else(|| {
format!(
"cannot find module file for `mod {name};` near {}",
d.display()
)
})?;
let src = std::fs::read_to_string(file)
.map_err(|e| format!("reading {}: {e}", file.display()))?;
let parsed = syn::parse_file(&src)
.map_err(|e| format!("parse {}: {e}", file.display()))?;
collect(&parsed.items, &child, Some(&d.join(&name)), acc)?;
}
}
_ => {}
}
}
Ok(())
}
fn assemble(acc: Collected) -> Result<Generated, String> {
let the_trait = acc
.the_trait
.ok_or("no #[plugin_interface] trait found in source")?;
let interface_name = the_trait.ident.to_string();
let iface_kebab = to_kebab_case(&interface_name);
let mut user_types: Vec<String> = Vec::new();
user_types.extend(acc.structs.iter().map(|(_, s)| s.ident.to_string()));
user_types.extend(acc.enums.iter().map(|(_, e)| e.ident.to_string()));
let known: BTreeSet<String> = user_types.iter().cloned().collect();
let mut type_defs: Vec<String> = Vec::new();
let mut variant_defs: Vec<String> = Vec::new();
for (_, s) in &acc.structs {
type_defs.push(struct_to_record(s, &known)?);
}
for (_, e) in &acc.enums {
let (synthetic, variant) = enum_to_wit(e, &known)?;
type_defs.extend(synthetic);
variant_defs.push(variant);
}
type_defs.extend(variant_defs);
let mut methods: Vec<WitMethod> = Vec::new();
for item in &the_trait.items {
let TraitItem::Fn(f) = item else { continue };
let mut params = Vec::new();
for arg in &f.sig.inputs {
let FnArg::Typed(pt) = arg else { continue };
if crate::stream_item_type(&pt.ty).is_some() {
continue;
}
let name = match pt.pat.as_ref() {
Pat::Ident(id) => to_kebab_case(&id.ident.to_string()),
_ => "arg".to_string(),
};
let wt = wit_type_with(&pt.ty, &known)
.map_err(|e| format!("method `{}` arg `{name}`: {e}", f.sig.ident))?;
params.push((name, wt));
}
let ret_ty: Option<&Type> = match &f.sig.output {
ReturnType::Type(_, t) => Some(t.as_ref()),
ReturnType::Default => None,
};
let stream_item = ret_ty.and_then(crate::stream_item_type);
let (ret, stream_item) = match stream_item {
Some(item_ty) => {
let item_wit = wit_type_with(item_ty, &known)
.map_err(|e| format!("method `{}` stream item: {e}", f.sig.ident))?;
(None, Some(item_wit))
}
None => {
let ret = return_to_wit_with(ret_ty, &known)
.map_err(|e| format!("method `{}` return: {e}", f.sig.ident))?;
(ret, None)
}
};
methods.push(WitMethod {
name: to_kebab_case(&f.sig.ident.to_string()),
params,
ret,
stream_item,
});
}
let wit = crate::render_wit_full(&iface_kebab, &type_defs, &methods);
let conversions = render_conversions(&iface_kebab, &acc.structs, &acc.enums, &known);
Ok(Generated {
interface_name,
iface_kebab,
user_types,
wit,
conversions,
})
}
fn author_path(mod_path: &[String], name: &str) -> String {
if mod_path.is_empty() {
format!("crate::{name}")
} else {
format!("crate::{}::{name}", mod_path.join("::"))
}
}
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
"if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
"self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
"while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
"override", "priv", "typeof", "unsized", "virtual", "yield", "try",
];
fn field_idents(fl: &syn::Field) -> (String, String) {
let author = fl.ident.as_ref().unwrap().to_string();
let bare = author.strip_prefix("r#").unwrap_or(&author);
let generated = if RUST_KEYWORDS.contains(&bare) {
format!("{bare}_")
} else {
author.clone()
};
(author, generated)
}
fn render_conversions(
iface_kebab: &str,
structs: &[(Vec<String>, syn::ItemStruct)],
enums: &[(Vec<String>, syn::ItemEnum)],
known: &BTreeSet<String>,
) -> String {
if structs.is_empty() && enums.is_empty() {
return String::new();
}
let snake = iface_kebab.replace('-', "_");
let gen_path = format!("exports::fidius::{snake}::{snake}");
let mut out = String::new();
out.push_str("// Generated by fidius-wit: author <-> wit-bindgen conversions.\n");
for (path, s) in structs {
let name = s.ident.to_string();
let g = format!("{gen_path}::{name}");
let a = author_path(path, &name);
let fields: Vec<(String, String, &Type)> = match &s.fields {
syn::Fields::Named(f) => f
.named
.iter()
.map(|fl| {
let (au, gen) = field_idents(fl);
(au, gen, &fl.ty)
})
.collect(),
_ => Vec::new(),
};
let to_author: Vec<String> = fields
.iter()
.map(|(au, gen, ty)| format!("{au}: {}", conv_expr(&format!("v.{gen}"), ty, known)))
.collect();
out.push_str(&format!(
"impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ {a} {{ {} }} }} }}\n",
to_author.join(", ")
));
let to_gen: Vec<String> = fields
.iter()
.map(|(au, gen, ty)| format!("{gen}: {}", conv_expr(&format!("v.{au}"), ty, known)))
.collect();
out.push_str(&format!(
"impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ {g} {{ {} }} }} }}\n",
to_gen.join(", ")
));
}
for (path, e) in enums {
let ename = e.ident.to_string();
let g = format!("{gen_path}::{ename}");
let a = author_path(path, &ename);
let mut g2a = Vec::new(); let mut a2g = Vec::new(); for v in &e.variants {
let case = v.ident.to_string();
match &v.fields {
syn::Fields::Unit => {
g2a.push(format!("{g}::{case} => {a}::{case}"));
a2g.push(format!("{a}::{case} => {g}::{case}"));
}
syn::Fields::Unnamed(u) if u.unnamed.len() == 1 => {
let ty = &u.unnamed[0].ty;
g2a.push(format!(
"{g}::{case}(x) => {a}::{case}({})",
conv_expr("x", ty, known)
));
a2g.push(format!(
"{a}::{case}(x) => {g}::{case}({})",
conv_expr("x", ty, known)
));
}
syn::Fields::Named(f) => {
let gen_rec = format!("{gen_path}::{ename}{case}");
let fnames: Vec<(String, String, &Type)> = f
.named
.iter()
.map(|fl| {
let (au, gen) = field_idents(fl);
(au, gen, &fl.ty)
})
.collect();
let g2a_inits = fnames
.iter()
.map(|(au, gen, ty)| {
format!("{au}: {}", conv_expr(&format!("__r.{gen}"), ty, known))
})
.collect::<Vec<_>>()
.join(", ");
g2a.push(format!("{g}::{case}(__r) => {a}::{case} {{ {g2a_inits} }}"));
let binds = fnames
.iter()
.map(|(au, ..)| au.clone())
.collect::<Vec<_>>()
.join(", ");
let a2g_inits = fnames
.iter()
.map(|(au, gen, ty)| format!("{gen}: {}", conv_expr(au, ty, known)))
.collect::<Vec<_>>()
.join(", ");
a2g.push(format!(
"{a}::{case} {{ {binds} }} => {g}::{case}({gen_rec} {{ {a2g_inits} }})"
));
}
syn::Fields::Unnamed(_) => unreachable!("rejected by enum_to_wit"),
}
}
out.push_str(&format!(
"impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ match v {{ {} }} }} }}\n",
g2a.join(", ")
));
out.push_str(&format!(
"impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ match v {{ {} }} }} }}\n",
a2g.join(", ")
));
}
out
}
pub fn conv_expr(access: &str, ty: &Type, known: &BTreeSet<String>) -> String {
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
if matches!(seg.ident.to_string().as_str(), "HashMap" | "BTreeMap") {
if let Some((k, v)) = two_generics(seg) {
let kc = conv_expr("k", k, known);
let vc = conv_expr("v", v, known);
if kc == "k" && vc == "v" {
return format!("{access}.into_iter().collect()");
}
return format!("{access}.into_iter().map(|(k, v)| ({kc}, {vc})).collect()");
}
}
}
}
if !contains_user_type(ty, known) {
return access.to_string();
}
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
let ident = seg.ident.to_string();
if let Some(inner) = single_generic(seg) {
match ident.as_str() {
"Vec" => {
return format!(
"{access}.into_iter().map(|w| {}).collect()",
conv_expr("w", inner, known)
);
}
"Option" => {
return format!("{access}.map(|w| {})", conv_expr("w", inner, known));
}
_ => {}
}
}
if known.contains(&ident) {
return format!("{access}.into()");
}
}
}
access.to_string()
}
pub fn contains_user_type(ty: &Type, known: &BTreeSet<String>) -> bool {
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
let ident = seg.ident.to_string();
if known.contains(&ident) {
return true;
}
if matches!(ident.as_str(), "Vec" | "Option" | "Box") {
if let Some(inner) = single_generic(seg) {
return contains_user_type(inner, known);
}
}
if matches!(ident.as_str(), "HashMap" | "BTreeMap") {
if let Some((k, v)) = two_generics(seg) {
return contains_user_type(k, known) || contains_user_type(v, known);
}
}
}
}
false
}
fn single_generic(seg: &syn::PathSegment) -> Option<&Type> {
if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
for a in &ab.args {
if let syn::GenericArgument::Type(t) = a {
return Some(t);
}
}
}
None
}
fn two_generics(seg: &syn::PathSegment) -> Option<(&Type, &Type)> {
if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
let types: Vec<&Type> = ab
.args
.iter()
.filter_map(|a| match a {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
})
.collect();
if types.len() >= 2 {
return Some((types[0], types[1]));
}
}
None
}
fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
attrs.iter().any(|a| {
a.path()
.segments
.last()
.map(|s| s.ident == name)
.unwrap_or(false)
})
}
fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
for a in attrs {
if !a.path().is_ident("derive") {
continue;
}
let mut found = false;
let _ = a.parse_nested_meta(|m| {
if m.path
.segments
.last()
.map(|s| s.ident == name)
.unwrap_or(false)
{
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
const SRC: &str = r#"
#[derive(WitType)]
pub struct Point { pub x: i32, pub y: i32 }
#[derive(WitType)]
pub enum Shape { Circle(u32), Rect(Point), Dot }
#[plugin_interface(version = 1, crate = "fidius_guest")]
pub trait Geo: Send + Sync {
fn midpoint(&self, a: Point, b: Point) -> Point;
fn classify(&self, pts: Vec<Point>) -> Shape;
fn name(&self, s: Shape) -> String;
}
"#;
#[test]
fn generates_wit_with_records_variants_and_funcs() {
let g = generate(SRC).unwrap();
assert_eq!(g.interface_name, "Geo");
assert_eq!(g.iface_kebab, "geo");
assert!(g.user_types.contains(&"Point".to_string()));
assert!(g.user_types.contains(&"Shape".to_string()));
assert!(g.wit.contains("record point {"));
assert!(g.wit.contains("variant shape {"));
assert!(g.wit.contains("rect(point),"));
assert!(g
.wit
.contains("midpoint: func(a: point, b: point) -> point;"));
assert!(g.wit.contains("classify: func(pts: list<point>) -> shape;"));
assert!(g.wit.contains("name: func(s: shape) -> string;"));
assert!(g.wit.contains("fidius-interface-hash: func() -> u64;"));
}
#[test]
fn generates_conversions_both_ways() {
let g = generate(SRC).unwrap();
let c = &g.conversions;
assert!(c.contains("From<exports::fidius::geo::geo::Point> for crate::Point"));
assert!(c.contains("From<crate::Point> for exports::fidius::geo::geo::Point"));
assert!(c.contains("From<exports::fidius::geo::geo::Shape> for crate::Shape"));
assert!(
c.contains("Rect(x) => crate :: Shape :: Rect")
|| c.contains("Rect(x) => crate::Shape::Rect")
);
assert!(c.contains(".into()"));
}
#[test]
fn primitive_only_interface_has_no_conversions() {
let src = r#"
#[plugin_interface(version = 1)]
pub trait Greeter { fn greet(&self, name: String) -> String; }
"#;
let g = generate(src).unwrap();
assert!(g.user_types.is_empty());
assert!(g.conversions.is_empty());
assert!(g.wit.contains("greet: func(name: string) -> string;"));
}
#[test]
fn unsupported_type_errors() {
let src = r#"
#[plugin_interface(version = 1)]
pub trait T { fn f(&self, x: Box<String>) -> u32; }
"#;
assert!(generate(src).is_err());
}
const KEYWORD_SRC: &str = r#"
#[derive(WitType)]
pub struct DeadLetter { pub record: String, pub stream: u64, pub from: bool, pub r#type: u8 }
#[derive(WitType)]
pub enum Variant { Stream, Record(u32), List { from: u8 } }
#[plugin_interface(version = 1, crate = "fidius_guest")]
pub trait Stream: Send + Sync {
fn record(&self, list: DeadLetter, option: Variant) -> DeadLetter;
}
"#;
#[test]
fn keyword_heavy_interface_escapes_every_identifier() {
let g = generate(KEYWORD_SRC).unwrap();
assert!(g.wit.contains("interface %stream {"), "{}", g.wit);
assert!(g.wit.contains("package fidius:%stream@0.1.0;"));
assert!(g.wit.contains("export %stream;"));
assert!(g.wit.contains("record dead-letter {"));
assert!(g.wit.contains("%record: string,"));
assert!(g.wit.contains("%stream: u64,"));
assert!(g.wit.contains("%from: bool,"));
assert!(g.wit.contains("%type: u8,"));
assert!(g.wit.contains("variant %variant {"));
assert!(g.wit.contains("%stream,"));
assert!(g.wit.contains("%record(u32),"));
assert!(g.wit.contains("%list(variant-list),"));
assert!(g.wit.contains("record variant-list {"));
assert!(g
.wit
.contains("%record: func(%list: dead-letter, %option: %variant) -> dead-letter;"));
}
#[test]
fn keyword_field_conversions_use_wit_bindgen_mangling() {
let c = &generate(KEYWORD_SRC).unwrap().conversions;
assert!(c.contains("r#type: v.type_"), "{c}");
assert!(c.contains("type_: v.r#type"), "{c}");
assert!(c.contains("record: v.record") && c.contains("stream: v.stream"));
}
#[test]
fn keyword_heavy_interface_parses() {
let g = generate(KEYWORD_SRC).unwrap();
wit_parser::UnresolvedPackageGroup::parse("keyword.wit", &g.wit)
.unwrap_or_else(|e| panic!("generated WIT must parse: {e}\n---\n{}", g.wit));
}
}