1use std::collections::BTreeSet;
27
28use syn::{Fields, GenericArgument, ItemEnum, ItemStruct, PathArguments, Type};
29
30mod generate;
31pub use generate::{contains_user_type, conv_expr, generate, generate_from_path, Generated};
32
33pub fn to_kebab_case(s: &str) -> String {
36 let mut out = String::new();
37 for (i, ch) in s.chars().enumerate() {
38 if ch == '_' {
39 out.push('-');
40 } else if ch.is_uppercase() {
41 if i != 0 {
42 out.push('-');
43 }
44 out.extend(ch.to_lowercase());
45 } else {
46 out.push(ch);
47 }
48 }
49 out
50}
51
52pub fn result_ok_type(ty: &Type) -> Option<&Type> {
54 if let Type::Path(p) = ty {
55 if let Some(seg) = p.path.segments.last() {
56 if seg.ident == "Result" {
57 return first_generic(seg);
58 }
59 }
60 }
61 None
62}
63
64pub struct WitMethod {
66 pub name: String,
68 pub params: Vec<(String, String)>,
70 pub ret: Option<String>,
72}
73
74pub fn wit_type_with(ty: &Type, known: &BTreeSet<String>) -> Result<String, String> {
78 match ty {
79 Type::Reference(r) => {
81 if let Type::Path(p) = r.elem.as_ref() {
82 if path_is(p, "str") {
83 return Ok("string".to_string());
84 }
85 }
86 if let Type::Slice(s) = r.elem.as_ref() {
87 let inner = wit_type_with(&s.elem, known)?;
88 return Ok(format!("list<{inner}>"));
89 }
90 wit_type_with(&r.elem, known)
91 }
92 Type::Path(p) => {
93 let seg = p
94 .path
95 .segments
96 .last()
97 .ok_or_else(|| "empty type path".to_string())?;
98 let ident = seg.ident.to_string();
99 match ident.as_str() {
100 "bool" => Ok("bool".into()),
101 "i8" => Ok("s8".into()),
102 "i16" => Ok("s16".into()),
103 "i32" => Ok("s32".into()),
104 "i64" => Ok("s64".into()),
105 "u8" => Ok("u8".into()),
106 "u16" => Ok("u16".into()),
107 "u32" => Ok("u32".into()),
108 "u64" => Ok("u64".into()),
109 "f32" => Ok("f32".into()),
110 "f64" => Ok("f64".into()),
111 "char" => Ok("char".into()),
112 "String" => Ok("string".into()),
113 "Vec" => {
114 let inner = single_generic(seg, "Vec")?;
115 Ok(format!("list<{}>", wit_type_with(inner, known)?))
116 }
117 "Option" => {
118 let inner = single_generic(seg, "Option")?;
119 Ok(format!("option<{}>", wit_type_with(inner, known)?))
120 }
121 other if known.contains(other) => Ok(to_kebab_case(other)),
123 other => Err(format!(
124 "type `{other}` is not supported in a WASM fidius interface \
125 (supported: bool, i8..i64, u8..u64, f32/f64, char, String, Vec<T>, \
126 Option<T>, Result<T, PluginError>, and #[derive(WitType)] structs/enums)"
127 )),
128 }
129 }
130 Type::Tuple(t) if t.elems.is_empty() => {
131 Err("unit `()` is not a valid argument type".to_string())
132 }
133 _ => Err("unsupported type in a WASM fidius interface".to_string()),
134 }
135}
136
137pub fn rust_type_to_wit(ty: &Type) -> Result<String, String> {
140 wit_type_with(ty, &BTreeSet::new())
141}
142
143pub fn return_to_wit_with(
147 ret: Option<&Type>,
148 known: &BTreeSet<String>,
149) -> Result<Option<String>, String> {
150 let Some(ty) = ret else { return Ok(None) };
151 if is_unit(ty) {
152 return Ok(None);
153 }
154 if let Type::Path(p) = ty {
155 if let Some(seg) = p.path.segments.last() {
156 if seg.ident == "Result" {
157 let ok = first_generic(seg).ok_or_else(|| "Result needs type args".to_string())?;
158 let ok_wit = if is_unit(ok) {
159 "_".to_string()
160 } else {
161 wit_type_with(ok, known)?
162 };
163 return Ok(Some(format!("result<{ok_wit}, plugin-error>")));
164 }
165 }
166 }
167 Ok(Some(wit_type_with(ty, known)?))
168}
169
170pub fn return_to_wit(ret: Option<&Type>) -> Result<Option<String>, String> {
172 return_to_wit_with(ret, &BTreeSet::new())
173}
174
175pub fn struct_to_record(item: &ItemStruct, known: &BTreeSet<String>) -> Result<String, String> {
179 let name = to_kebab_case(&item.ident.to_string());
180 let Fields::Named(fields) = &item.fields else {
181 return Err(format!(
182 "WitType struct `{}` must have named fields (tuple/unit structs are not supported)",
183 item.ident
184 ));
185 };
186 let mut out = format!(" record {name} {{\n");
187 for f in &fields.named {
188 let fname = to_kebab_case(&f.ident.as_ref().unwrap().to_string());
189 let fty = wit_type_with(&f.ty, known)
190 .map_err(|e| format!("field `{}` of `{}`: {e}", fname, item.ident))?;
191 out.push_str(&format!(" {fname}: {fty},\n"));
192 }
193 out.push_str(" }\n");
194 Ok(out)
195}
196
197pub fn enum_to_wit(
206 item: &ItemEnum,
207 known: &BTreeSet<String>,
208) -> Result<(Vec<String>, String), String> {
209 let ename = item.ident.to_string();
210 let name = to_kebab_case(&ename);
211 let mut records = Vec::new();
212 let mut out = format!(" variant {name} {{\n");
213 for v in &item.variants {
214 let case = to_kebab_case(&v.ident.to_string());
215 match &v.fields {
216 Fields::Unit => out.push_str(&format!(" {case},\n")),
217 Fields::Unnamed(u) if u.unnamed.len() == 1 => {
218 let payload = wit_type_with(&u.unnamed[0].ty, known)
219 .map_err(|e| format!("variant `{ename}::{}`: {e}", v.ident))?;
220 out.push_str(&format!(" {case}({payload}),\n"));
221 }
222 Fields::Named(f) => {
223 let rec_name = format!("{name}-{case}");
225 let mut rec = format!(" record {rec_name} {{\n");
226 for fl in &f.named {
227 let fname = to_kebab_case(&fl.ident.as_ref().unwrap().to_string());
228 let fty = wit_type_with(&fl.ty, known)
229 .map_err(|e| format!("field `{fname}` of `{ename}::{}`: {e}", v.ident))?;
230 rec.push_str(&format!(" {fname}: {fty},\n"));
231 }
232 rec.push_str(" }\n");
233 records.push(rec);
234 out.push_str(&format!(" {case}({rec_name}),\n"));
235 }
236 Fields::Unnamed(_) => {
237 return Err(format!(
238 "WitType enum `{ename}` variant `{}` has multiple tuple fields; \
239 a WIT variant case takes one payload — use a struct variant \
240 `{} {{ .. }}` (or a single field)",
241 v.ident, v.ident
242 ));
243 }
244 }
245 }
246 out.push_str(" }\n");
247 Ok((records, out))
248}
249
250pub fn render_wit_full(iface_kebab: &str, type_defs: &[String], methods: &[WitMethod]) -> String {
255 let mut s = String::new();
256 s.push_str(&format!("package fidius:{iface_kebab}@0.1.0;\n\n"));
257 s.push_str(&format!("interface {iface_kebab} {{\n"));
258 s.push_str(" record plugin-error {\n");
259 s.push_str(" code: string,\n");
260 s.push_str(" message: string,\n");
261 s.push_str(" details: option<string>,\n");
262 s.push_str(" }\n");
263 for def in type_defs {
264 s.push_str(def);
265 }
266 for m in methods {
267 let params = m
268 .params
269 .iter()
270 .map(|(n, t)| format!("{n}: {t}"))
271 .collect::<Vec<_>>()
272 .join(", ");
273 match &m.ret {
274 Some(r) => s.push_str(&format!(" {}: func({params}) -> {r};\n", m.name)),
275 None => s.push_str(&format!(" {}: func({params});\n", m.name)),
276 }
277 }
278 s.push_str(" fidius-interface-hash: func() -> u64;\n");
279 s.push_str("}\n\n");
280 s.push_str(&format!(
281 "world {iface_kebab}-plugin {{\n export {iface_kebab};\n}}\n"
282 ));
283 s
284}
285
286pub fn render_wit(iface_kebab: &str, methods: &[WitMethod]) -> String {
289 render_wit_full(iface_kebab, &[], methods)
290}
291
292fn is_unit(ty: &Type) -> bool {
295 matches!(ty, Type::Tuple(t) if t.elems.is_empty())
296}
297
298fn path_is(p: &syn::TypePath, name: &str) -> bool {
299 p.path
300 .segments
301 .last()
302 .map(|s| s.ident == name)
303 .unwrap_or(false)
304}
305
306fn single_generic<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<&'a Type, String> {
307 first_generic(seg).ok_or_else(|| format!("`{what}` needs one type argument"))
308}
309
310fn first_generic(seg: &syn::PathSegment) -> Option<&Type> {
311 if let PathArguments::AngleBracketed(ab) = &seg.arguments {
312 for a in &ab.args {
313 if let GenericArgument::Type(t) = a {
314 return Some(t);
315 }
316 }
317 }
318 None
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn known(names: &[&str]) -> BTreeSet<String> {
326 names.iter().map(|s| s.to_string()).collect()
327 }
328 fn wit(s: &str) -> String {
329 rust_type_to_wit(&syn::parse_str::<Type>(s).unwrap()).unwrap()
330 }
331
332 #[test]
333 fn primitives_strings_containers() {
334 assert_eq!(wit("i64"), "s64");
335 assert_eq!(wit("u32"), "u32");
336 assert_eq!(wit("String"), "string");
337 assert_eq!(wit("& str"), "string");
338 assert_eq!(wit("Vec<u8>"), "list<u8>");
339 assert_eq!(wit("Option<i32>"), "option<s32>");
340 assert_eq!(wit("&[u8]"), "list<u8>");
341 }
342
343 #[test]
344 fn returns() {
345 let ret = |s: &str| return_to_wit(Some(&syn::parse_str::<Type>(s).unwrap())).unwrap();
346 assert_eq!(ret("()"), None);
347 assert_eq!(
348 ret("Result<i64, PluginError>"),
349 Some("result<s64, plugin-error>".into())
350 );
351 assert_eq!(
352 ret("Result<(), PluginError>"),
353 Some("result<_, plugin-error>".into())
354 );
355 }
356
357 #[test]
358 fn user_types_need_the_known_set() {
359 let ty: Type = syn::parse_str("Point").unwrap();
360 assert!(rust_type_to_wit(&ty).is_err()); let k = known(&["Point"]);
362 assert_eq!(wit_type_with(&ty, &k).unwrap(), "point");
363 let v: Type = syn::parse_str("Vec<Point>").unwrap();
365 assert_eq!(wit_type_with(&v, &k).unwrap(), "list<point>");
366 let o: Type = syn::parse_str("Option<BytePipe>").unwrap();
367 assert_eq!(
368 wit_type_with(&o, &known(&["BytePipe"])).unwrap(),
369 "option<byte-pipe>"
370 );
371 }
372
373 #[test]
374 fn struct_renders_to_record() {
375 let item: ItemStruct = syn::parse_str("struct Point { x: i32, y_pos: u64 }").unwrap();
376 let rec = struct_to_record(&item, &BTreeSet::new()).unwrap();
377 assert!(rec.contains("record point {"));
378 assert!(rec.contains("x: s32,"));
379 assert!(rec.contains("y-pos: u64,"));
380 }
381
382 #[test]
383 fn struct_with_nested_user_type() {
384 let item: ItemStruct = syn::parse_str("struct Line { from: Point, to: Point }").unwrap();
385 let rec = struct_to_record(&item, &known(&["Point"])).unwrap();
386 assert!(rec.contains("from: point,"));
387 assert!(rec.contains("to: point,"));
388 }
389
390 #[test]
391 fn enum_renders_to_variant() {
392 let item: ItemEnum =
393 syn::parse_str("enum Shape { Circle(u32), Rect(Point), Dot }").unwrap();
394 let (records, var) = enum_to_wit(&item, &known(&["Point"])).unwrap();
395 assert!(records.is_empty());
396 assert!(var.contains("variant shape {"));
397 assert!(var.contains("circle(u32),"));
398 assert!(var.contains("rect(point),"));
399 assert!(var.contains("dot,"));
400 }
401
402 #[test]
403 fn struct_variant_synthesizes_a_record() {
404 let item: ItemEnum = syn::parse_str("enum Shape { Rect { w: u32, h: u32 }, Dot }").unwrap();
405 let (records, var) = enum_to_wit(&item, &BTreeSet::new()).unwrap();
406 assert_eq!(records.len(), 1);
407 assert!(records[0].contains("record shape-rect {"));
408 assert!(records[0].contains("w: u32,"));
409 assert!(records[0].contains("h: u32,"));
410 assert!(var.contains("rect(shape-rect),"));
411 assert!(var.contains("dot,"));
412 }
413
414 #[test]
415 fn multifield_tuple_variant_is_rejected() {
416 let item: ItemEnum = syn::parse_str("enum E { Pair(u32, u32) }").unwrap();
417 assert!(enum_to_wit(&item, &BTreeSet::new()).is_err());
418 }
419
420 #[test]
421 fn full_document_places_type_defs_before_funcs() {
422 let recs = vec![struct_to_record(
423 &syn::parse_str("struct Point { x: i32, y: i32 }").unwrap(),
424 &BTreeSet::new(),
425 )
426 .unwrap()];
427 let methods = vec![WitMethod {
428 name: "midpoint".into(),
429 params: vec![("a".into(), "point".into()), ("b".into(), "point".into())],
430 ret: Some("point".into()),
431 }];
432 let doc = render_wit_full("geo", &recs, &methods);
433 assert!(doc.contains("package fidius:geo@0.1.0;"));
434 let rec_at = doc.find("record point {").unwrap();
435 let fn_at = doc.find("midpoint: func").unwrap();
436 assert!(
437 rec_at < fn_at,
438 "records must precede funcs in the interface"
439 );
440 assert!(doc.contains("midpoint: func(a: point, b: point) -> point;"));
441 assert!(doc.contains("fidius-interface-hash: func() -> u64;"));
442 assert!(doc.contains("world geo-plugin {"));
443 }
444}