use std::collections::BTreeSet;
use syn::{Fields, GenericArgument, ItemEnum, ItemStruct, PathArguments, Type};
mod generate;
pub use generate::{contains_user_type, conv_expr, generate, generate_from_path, Generated};
pub fn to_kebab_case(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch == '_' {
out.push('-');
} else if ch.is_uppercase() {
if i != 0 {
out.push('-');
}
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
pub fn result_ok_type(ty: &Type) -> Option<&Type> {
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
if seg.ident == "Result" {
return first_generic(seg);
}
}
}
None
}
pub struct WitMethod {
pub name: String,
pub params: Vec<(String, String)>,
pub ret: Option<String>,
pub stream_item: Option<String>,
}
pub fn stream_item_type(ty: &Type) -> Option<&Type> {
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
if seg.ident == "Stream" {
return first_generic(seg);
}
}
}
None
}
pub fn wit_type_with(ty: &Type, known: &BTreeSet<String>) -> Result<String, String> {
match ty {
Type::Reference(r) => {
if let Type::Path(p) = r.elem.as_ref() {
if path_is(p, "str") {
return Ok("string".to_string());
}
}
if let Type::Slice(s) = r.elem.as_ref() {
let inner = wit_type_with(&s.elem, known)?;
return Ok(format!("list<{inner}>"));
}
wit_type_with(&r.elem, known)
}
Type::Path(p) => {
let seg = p
.path
.segments
.last()
.ok_or_else(|| "empty type path".to_string())?;
let ident = seg.ident.to_string();
match ident.as_str() {
"bool" => Ok("bool".into()),
"i8" => Ok("s8".into()),
"i16" => Ok("s16".into()),
"i32" => Ok("s32".into()),
"i64" => Ok("s64".into()),
"u8" => Ok("u8".into()),
"u16" => Ok("u16".into()),
"u32" => Ok("u32".into()),
"u64" => Ok("u64".into()),
"f32" => Ok("f32".into()),
"f64" => Ok("f64".into()),
"char" => Ok("char".into()),
"String" => Ok("string".into()),
"Vec" => {
let inner = single_generic(seg, "Vec")?;
Ok(format!("list<{}>", wit_type_with(inner, known)?))
}
"Option" => {
let inner = single_generic(seg, "Option")?;
Ok(format!("option<{}>", wit_type_with(inner, known)?))
}
"HashMap" | "BTreeMap" => {
let (k, v) = two_generics(seg, &ident)?;
Ok(format!(
"list<tuple<{}, {}>>",
wit_type_with(k, known)?,
wit_type_with(v, known)?
))
}
other if known.contains(other) => Ok(to_kebab_case(other)),
other => Err(format!(
"type `{other}` is not supported in a WASM fidius interface \
(supported: bool, i8..i64, u8..u64, f32/f64, char, String, Vec<T>, \
Option<T>, HashMap/BTreeMap<K, V>, tuples, Result<T, PluginError>, \
and #[derive(WitType)] structs/enums)"
)),
}
}
Type::Tuple(t) if t.elems.is_empty() => {
Err("unit `()` is not a valid argument type".to_string())
}
Type::Tuple(t) => {
let elems = t
.elems
.iter()
.map(|e| wit_type_with(e, known))
.collect::<Result<Vec<_>, _>>()?;
Ok(format!("tuple<{}>", elems.join(", ")))
}
_ => Err("unsupported type in a WASM fidius interface".to_string()),
}
}
pub fn rust_type_to_wit(ty: &Type) -> Result<String, String> {
wit_type_with(ty, &BTreeSet::new())
}
pub fn return_to_wit_with(
ret: Option<&Type>,
known: &BTreeSet<String>,
) -> Result<Option<String>, String> {
let Some(ty) = ret else { return Ok(None) };
if is_unit(ty) {
return Ok(None);
}
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
if seg.ident == "Result" {
let ok = first_generic(seg).ok_or_else(|| "Result needs type args".to_string())?;
let ok_wit = if is_unit(ok) {
"_".to_string()
} else {
wit_type_with(ok, known)?
};
return Ok(Some(format!("result<{ok_wit}, plugin-error>")));
}
}
}
Ok(Some(wit_type_with(ty, known)?))
}
pub fn return_to_wit(ret: Option<&Type>) -> Result<Option<String>, String> {
return_to_wit_with(ret, &BTreeSet::new())
}
pub fn struct_to_record(item: &ItemStruct, known: &BTreeSet<String>) -> Result<String, String> {
let name = to_kebab_case(&item.ident.to_string());
let Fields::Named(fields) = &item.fields else {
return Err(format!(
"WitType struct `{}` must have named fields (tuple/unit structs are not supported)",
item.ident
));
};
let mut out = format!(" record {name} {{\n");
for f in &fields.named {
let fname = to_kebab_case(&f.ident.as_ref().unwrap().to_string());
let fty = wit_type_with(&f.ty, known)
.map_err(|e| format!("field `{}` of `{}`: {e}", fname, item.ident))?;
out.push_str(&format!(" {fname}: {fty},\n"));
}
out.push_str(" }\n");
Ok(out)
}
pub fn enum_to_wit(
item: &ItemEnum,
known: &BTreeSet<String>,
) -> Result<(Vec<String>, String), String> {
let ename = item.ident.to_string();
let name = to_kebab_case(&ename);
let mut records = Vec::new();
let mut out = format!(" variant {name} {{\n");
for v in &item.variants {
let case = to_kebab_case(&v.ident.to_string());
match &v.fields {
Fields::Unit => out.push_str(&format!(" {case},\n")),
Fields::Unnamed(u) if u.unnamed.len() == 1 => {
let payload = wit_type_with(&u.unnamed[0].ty, known)
.map_err(|e| format!("variant `{ename}::{}`: {e}", v.ident))?;
out.push_str(&format!(" {case}({payload}),\n"));
}
Fields::Named(f) => {
let rec_name = format!("{name}-{case}");
let mut rec = format!(" record {rec_name} {{\n");
for fl in &f.named {
let fname = to_kebab_case(&fl.ident.as_ref().unwrap().to_string());
let fty = wit_type_with(&fl.ty, known)
.map_err(|e| format!("field `{fname}` of `{ename}::{}`: {e}", v.ident))?;
rec.push_str(&format!(" {fname}: {fty},\n"));
}
rec.push_str(" }\n");
records.push(rec);
out.push_str(&format!(" {case}({rec_name}),\n"));
}
Fields::Unnamed(_) => {
return Err(format!(
"WitType enum `{ename}` variant `{}` has multiple tuple fields; \
a WIT variant case takes one payload — use a struct variant \
`{} {{ .. }}` (or a single field)",
v.ident, v.ident
));
}
}
}
out.push_str(" }\n");
Ok((records, out))
}
pub fn render_wit_full(iface_kebab: &str, type_defs: &[String], methods: &[WitMethod]) -> String {
let mut s = String::new();
s.push_str(&format!("package fidius:{iface_kebab}@0.1.0;\n\n"));
s.push_str(&format!("interface {iface_kebab} {{\n"));
s.push_str(" record plugin-error {\n");
s.push_str(" code: string,\n");
s.push_str(" message: string,\n");
s.push_str(" details: option<string>,\n");
s.push_str(" }\n");
for def in type_defs {
s.push_str(def);
}
for m in methods {
if let Some(item) = &m.stream_item {
s.push_str(&format!(" resource {}-stream {{\n", m.name));
s.push_str(&format!(
" next: func() -> result<option<{item}>, plugin-error>;\n"
));
s.push_str(" }\n");
}
}
for m in methods {
let params = m
.params
.iter()
.map(|(n, t)| format!("{n}: {t}"))
.collect::<Vec<_>>()
.join(", ");
if m.stream_item.is_some() {
s.push_str(&format!(
" {}: func({params}) -> {}-stream;\n",
m.name, m.name
));
} else {
match &m.ret {
Some(r) => s.push_str(&format!(" {}: func({params}) -> {r};\n", m.name)),
None => s.push_str(&format!(" {}: func({params});\n", m.name)),
}
}
}
s.push_str(" fidius-interface-hash: func() -> u64;\n");
s.push_str(" fidius-configure: func(config: list<u8>);\n");
s.push_str("}\n\n");
s.push_str(&format!(
"world {iface_kebab}-plugin {{\n export {iface_kebab};\n}}\n"
));
s
}
pub fn render_wit(iface_kebab: &str, methods: &[WitMethod]) -> String {
render_wit_full(iface_kebab, &[], methods)
}
fn is_unit(ty: &Type) -> bool {
matches!(ty, Type::Tuple(t) if t.elems.is_empty())
}
fn path_is(p: &syn::TypePath, name: &str) -> bool {
p.path
.segments
.last()
.map(|s| s.ident == name)
.unwrap_or(false)
}
fn single_generic<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<&'a Type, String> {
first_generic(seg).ok_or_else(|| format!("`{what}` needs one type argument"))
}
fn first_generic(seg: &syn::PathSegment) -> Option<&Type> {
if let PathArguments::AngleBracketed(ab) = &seg.arguments {
for a in &ab.args {
if let GenericArgument::Type(t) = a {
return Some(t);
}
}
}
None
}
fn two_generics<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<(&'a Type, &'a Type), String> {
if let PathArguments::AngleBracketed(ab) = &seg.arguments {
let types: Vec<&Type> = ab
.args
.iter()
.filter_map(|a| match a {
GenericArgument::Type(t) => Some(t),
_ => None,
})
.collect();
if types.len() >= 2 {
return Ok((types[0], types[1]));
}
}
Err(format!("`{what}` needs two type arguments (key, value)"))
}
#[cfg(test)]
mod tests {
use super::*;
fn known(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
fn wit(s: &str) -> String {
rust_type_to_wit(&syn::parse_str::<Type>(s).unwrap()).unwrap()
}
#[test]
fn primitives_strings_containers() {
assert_eq!(wit("i64"), "s64");
assert_eq!(wit("u32"), "u32");
assert_eq!(wit("String"), "string");
assert_eq!(wit("& str"), "string");
assert_eq!(wit("Vec<u8>"), "list<u8>");
assert_eq!(wit("Option<i32>"), "option<s32>");
assert_eq!(wit("&[u8]"), "list<u8>");
}
#[test]
fn maps_tuples_and_nesting() {
assert_eq!(wit("HashMap<String, i64>"), "list<tuple<string, s64>>");
assert_eq!(wit("BTreeMap<u32, String>"), "list<tuple<u32, string>>");
assert_eq!(wit("(i32, String)"), "tuple<s32, string>");
assert_eq!(wit("(u8, u8, bool)"), "tuple<u8, u8, bool>");
assert_eq!(wit("Vec<Option<i32>>"), "list<option<s32>>");
assert_eq!(wit("Option<Vec<u8>>"), "option<list<u8>>");
assert_eq!(
wit("HashMap<String, Vec<i64>>"),
"list<tuple<string, list<s64>>>"
);
let k = known(&["Row"]);
let wk = |s: &str| wit_type_with(&syn::parse_str::<Type>(s).unwrap(), &k).unwrap();
assert_eq!(wk("Vec<Row>"), "list<row>");
assert_eq!(wk("HashMap<String, Row>"), "list<tuple<string, row>>");
assert_eq!(wk("(Row, i32)"), "tuple<row, s32>");
}
#[test]
fn returns() {
let ret = |s: &str| return_to_wit(Some(&syn::parse_str::<Type>(s).unwrap())).unwrap();
assert_eq!(ret("()"), None);
assert_eq!(
ret("Result<i64, PluginError>"),
Some("result<s64, plugin-error>".into())
);
assert_eq!(
ret("Result<(), PluginError>"),
Some("result<_, plugin-error>".into())
);
}
#[test]
fn user_types_need_the_known_set() {
let ty: Type = syn::parse_str("Point").unwrap();
assert!(rust_type_to_wit(&ty).is_err()); let k = known(&["Point"]);
assert_eq!(wit_type_with(&ty, &k).unwrap(), "point");
let v: Type = syn::parse_str("Vec<Point>").unwrap();
assert_eq!(wit_type_with(&v, &k).unwrap(), "list<point>");
let o: Type = syn::parse_str("Option<BytePipe>").unwrap();
assert_eq!(
wit_type_with(&o, &known(&["BytePipe"])).unwrap(),
"option<byte-pipe>"
);
}
#[test]
fn struct_renders_to_record() {
let item: ItemStruct = syn::parse_str("struct Point { x: i32, y_pos: u64 }").unwrap();
let rec = struct_to_record(&item, &BTreeSet::new()).unwrap();
assert!(rec.contains("record point {"));
assert!(rec.contains("x: s32,"));
assert!(rec.contains("y-pos: u64,"));
}
#[test]
fn struct_with_nested_user_type() {
let item: ItemStruct = syn::parse_str("struct Line { from: Point, to: Point }").unwrap();
let rec = struct_to_record(&item, &known(&["Point"])).unwrap();
assert!(rec.contains("from: point,"));
assert!(rec.contains("to: point,"));
}
#[test]
fn enum_renders_to_variant() {
let item: ItemEnum =
syn::parse_str("enum Shape { Circle(u32), Rect(Point), Dot }").unwrap();
let (records, var) = enum_to_wit(&item, &known(&["Point"])).unwrap();
assert!(records.is_empty());
assert!(var.contains("variant shape {"));
assert!(var.contains("circle(u32),"));
assert!(var.contains("rect(point),"));
assert!(var.contains("dot,"));
}
#[test]
fn struct_variant_synthesizes_a_record() {
let item: ItemEnum = syn::parse_str("enum Shape { Rect { w: u32, h: u32 }, Dot }").unwrap();
let (records, var) = enum_to_wit(&item, &BTreeSet::new()).unwrap();
assert_eq!(records.len(), 1);
assert!(records[0].contains("record shape-rect {"));
assert!(records[0].contains("w: u32,"));
assert!(records[0].contains("h: u32,"));
assert!(var.contains("rect(shape-rect),"));
assert!(var.contains("dot,"));
}
#[test]
fn multifield_tuple_variant_is_rejected() {
let item: ItemEnum = syn::parse_str("enum E { Pair(u32, u32) }").unwrap();
assert!(enum_to_wit(&item, &BTreeSet::new()).is_err());
}
#[test]
fn full_document_places_type_defs_before_funcs() {
let recs = vec![struct_to_record(
&syn::parse_str("struct Point { x: i32, y: i32 }").unwrap(),
&BTreeSet::new(),
)
.unwrap()];
let methods = vec![WitMethod {
name: "midpoint".into(),
params: vec![("a".into(), "point".into()), ("b".into(), "point".into())],
ret: Some("point".into()),
stream_item: None,
}];
let doc = render_wit_full("geo", &recs, &methods);
assert!(doc.contains("package fidius:geo@0.1.0;"));
let rec_at = doc.find("record point {").unwrap();
let fn_at = doc.find("midpoint: func").unwrap();
assert!(
rec_at < fn_at,
"records must precede funcs in the interface"
);
assert!(doc.contains("midpoint: func(a: point, b: point) -> point;"));
assert!(doc.contains("fidius-interface-hash: func() -> u64;"));
assert!(doc.contains("world geo-plugin {"));
}
#[test]
fn streaming_method_renders_a_resource() {
let methods = vec![WitMethod {
name: "tick".into(),
params: vec![("count".into(), "u32".into())],
ret: None,
stream_item: Some("u64".into()),
}];
let doc = render_wit("ticker", &methods);
assert!(doc.contains("resource tick-stream {"));
assert!(doc.contains("next: func() -> result<option<u64>, plugin-error>;"));
assert!(doc.contains("tick: func(count: u32) -> tick-stream;"));
assert!(doc.find("resource tick-stream").unwrap() < doc.find("tick: func").unwrap());
}
#[test]
fn stream_item_type_detects_marker() {
let ty: Type = syn::parse_str("fidius::Stream<u64>").unwrap();
let item = stream_item_type(&ty).unwrap();
assert_eq!(rust_type_to_wit(item).unwrap(), "u64");
let bare: Type = syn::parse_str("Stream<String>").unwrap();
assert!(stream_item_type(&bare).is_some());
let not: Type = syn::parse_str("Vec<u64>").unwrap();
assert!(stream_item_type(¬).is_none());
}
}