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 pub stream_item: Option<String>,
78}
79
80pub fn stream_item_type(ty: &Type) -> Option<&Type> {
84 if let Type::Path(p) = ty {
85 if let Some(seg) = p.path.segments.last() {
86 if seg.ident == "Stream" {
87 return first_generic(seg);
88 }
89 }
90 }
91 None
92}
93
94pub fn wit_type_with(ty: &Type, known: &BTreeSet<String>) -> Result<String, String> {
98 match ty {
99 Type::Reference(r) => {
101 if let Type::Path(p) = r.elem.as_ref() {
102 if path_is(p, "str") {
103 return Ok("string".to_string());
104 }
105 }
106 if let Type::Slice(s) = r.elem.as_ref() {
107 let inner = wit_type_with(&s.elem, known)?;
108 return Ok(format!("list<{inner}>"));
109 }
110 wit_type_with(&r.elem, known)
111 }
112 Type::Path(p) => {
113 let seg = p
114 .path
115 .segments
116 .last()
117 .ok_or_else(|| "empty type path".to_string())?;
118 let ident = seg.ident.to_string();
119 match ident.as_str() {
120 "bool" => Ok("bool".into()),
121 "i8" => Ok("s8".into()),
122 "i16" => Ok("s16".into()),
123 "i32" => Ok("s32".into()),
124 "i64" => Ok("s64".into()),
125 "u8" => Ok("u8".into()),
126 "u16" => Ok("u16".into()),
127 "u32" => Ok("u32".into()),
128 "u64" => Ok("u64".into()),
129 "f32" => Ok("f32".into()),
130 "f64" => Ok("f64".into()),
131 "char" => Ok("char".into()),
132 "String" => Ok("string".into()),
133 "Vec" => {
134 let inner = single_generic(seg, "Vec")?;
135 Ok(format!("list<{}>", wit_type_with(inner, known)?))
136 }
137 "Option" => {
138 let inner = single_generic(seg, "Option")?;
139 Ok(format!("option<{}>", wit_type_with(inner, known)?))
140 }
141 "HashMap" | "BTreeMap" => {
145 let (k, v) = two_generics(seg, &ident)?;
146 Ok(format!(
147 "list<tuple<{}, {}>>",
148 wit_type_with(k, known)?,
149 wit_type_with(v, known)?
150 ))
151 }
152 other if known.contains(other) => Ok(to_kebab_case(other)),
154 other => Err(format!(
155 "type `{other}` is not supported in a WASM fidius interface \
156 (supported: bool, i8..i64, u8..u64, f32/f64, char, String, Vec<T>, \
157 Option<T>, HashMap/BTreeMap<K, V>, tuples, Result<T, PluginError>, \
158 and #[derive(WitType)] structs/enums)"
159 )),
160 }
161 }
162 Type::Tuple(t) if t.elems.is_empty() => {
163 Err("unit `()` is not a valid argument type".to_string())
164 }
165 Type::Tuple(t) => {
167 let elems = t
168 .elems
169 .iter()
170 .map(|e| wit_type_with(e, known))
171 .collect::<Result<Vec<_>, _>>()?;
172 Ok(format!("tuple<{}>", elems.join(", ")))
173 }
174 _ => Err("unsupported type in a WASM fidius interface".to_string()),
175 }
176}
177
178pub fn rust_type_to_wit(ty: &Type) -> Result<String, String> {
181 wit_type_with(ty, &BTreeSet::new())
182}
183
184pub fn return_to_wit_with(
188 ret: Option<&Type>,
189 known: &BTreeSet<String>,
190) -> Result<Option<String>, String> {
191 let Some(ty) = ret else { return Ok(None) };
192 if is_unit(ty) {
193 return Ok(None);
194 }
195 if let Type::Path(p) = ty {
196 if let Some(seg) = p.path.segments.last() {
197 if seg.ident == "Result" {
198 let ok = first_generic(seg).ok_or_else(|| "Result needs type args".to_string())?;
199 let ok_wit = if is_unit(ok) {
200 "_".to_string()
201 } else {
202 wit_type_with(ok, known)?
203 };
204 return Ok(Some(format!("result<{ok_wit}, plugin-error>")));
205 }
206 }
207 }
208 Ok(Some(wit_type_with(ty, known)?))
209}
210
211pub fn return_to_wit(ret: Option<&Type>) -> Result<Option<String>, String> {
213 return_to_wit_with(ret, &BTreeSet::new())
214}
215
216pub fn struct_to_record(item: &ItemStruct, known: &BTreeSet<String>) -> Result<String, String> {
220 let name = to_kebab_case(&item.ident.to_string());
221 let Fields::Named(fields) = &item.fields else {
222 return Err(format!(
223 "WitType struct `{}` must have named fields (tuple/unit structs are not supported)",
224 item.ident
225 ));
226 };
227 let mut out = format!(" record {name} {{\n");
228 for f in &fields.named {
229 let fname = to_kebab_case(&f.ident.as_ref().unwrap().to_string());
230 let fty = wit_type_with(&f.ty, known)
231 .map_err(|e| format!("field `{}` of `{}`: {e}", fname, item.ident))?;
232 out.push_str(&format!(" {fname}: {fty},\n"));
233 }
234 out.push_str(" }\n");
235 Ok(out)
236}
237
238pub fn enum_to_wit(
247 item: &ItemEnum,
248 known: &BTreeSet<String>,
249) -> Result<(Vec<String>, String), String> {
250 let ename = item.ident.to_string();
251 let name = to_kebab_case(&ename);
252 let mut records = Vec::new();
253 let mut out = format!(" variant {name} {{\n");
254 for v in &item.variants {
255 let case = to_kebab_case(&v.ident.to_string());
256 match &v.fields {
257 Fields::Unit => out.push_str(&format!(" {case},\n")),
258 Fields::Unnamed(u) if u.unnamed.len() == 1 => {
259 let payload = wit_type_with(&u.unnamed[0].ty, known)
260 .map_err(|e| format!("variant `{ename}::{}`: {e}", v.ident))?;
261 out.push_str(&format!(" {case}({payload}),\n"));
262 }
263 Fields::Named(f) => {
264 let rec_name = format!("{name}-{case}");
266 let mut rec = format!(" record {rec_name} {{\n");
267 for fl in &f.named {
268 let fname = to_kebab_case(&fl.ident.as_ref().unwrap().to_string());
269 let fty = wit_type_with(&fl.ty, known)
270 .map_err(|e| format!("field `{fname}` of `{ename}::{}`: {e}", v.ident))?;
271 rec.push_str(&format!(" {fname}: {fty},\n"));
272 }
273 rec.push_str(" }\n");
274 records.push(rec);
275 out.push_str(&format!(" {case}({rec_name}),\n"));
276 }
277 Fields::Unnamed(_) => {
278 return Err(format!(
279 "WitType enum `{ename}` variant `{}` has multiple tuple fields; \
280 a WIT variant case takes one payload — use a struct variant \
281 `{} {{ .. }}` (or a single field)",
282 v.ident, v.ident
283 ));
284 }
285 }
286 }
287 out.push_str(" }\n");
288 Ok((records, out))
289}
290
291pub fn render_wit_full(iface_kebab: &str, type_defs: &[String], methods: &[WitMethod]) -> String {
296 let mut s = String::new();
297 s.push_str(&format!("package fidius:{iface_kebab}@0.1.0;\n\n"));
298 s.push_str(&format!("interface {iface_kebab} {{\n"));
299 s.push_str(" record plugin-error {\n");
300 s.push_str(" code: string,\n");
301 s.push_str(" message: string,\n");
302 s.push_str(" details: option<string>,\n");
303 s.push_str(" }\n");
304 for def in type_defs {
305 s.push_str(def);
306 }
307 for m in methods {
310 if let Some(item) = &m.stream_item {
311 s.push_str(&format!(" resource {}-stream {{\n", m.name));
312 s.push_str(&format!(
313 " next: func() -> result<option<{item}>, plugin-error>;\n"
314 ));
315 s.push_str(" }\n");
316 }
317 }
318 for m in methods {
319 let params = m
320 .params
321 .iter()
322 .map(|(n, t)| format!("{n}: {t}"))
323 .collect::<Vec<_>>()
324 .join(", ");
325 if m.stream_item.is_some() {
326 s.push_str(&format!(
328 " {}: func({params}) -> {}-stream;\n",
329 m.name, m.name
330 ));
331 } else {
332 match &m.ret {
333 Some(r) => s.push_str(&format!(" {}: func({params}) -> {r};\n", m.name)),
334 None => s.push_str(&format!(" {}: func({params});\n", m.name)),
335 }
336 }
337 }
338 s.push_str(" fidius-interface-hash: func() -> u64;\n");
339 s.push_str(" fidius-configure: func(config: list<u8>);\n");
344 s.push_str("}\n\n");
345 s.push_str(&format!(
346 "world {iface_kebab}-plugin {{\n export {iface_kebab};\n}}\n"
347 ));
348 s
349}
350
351pub fn render_wit(iface_kebab: &str, methods: &[WitMethod]) -> String {
354 render_wit_full(iface_kebab, &[], methods)
355}
356
357fn is_unit(ty: &Type) -> bool {
360 matches!(ty, Type::Tuple(t) if t.elems.is_empty())
361}
362
363fn path_is(p: &syn::TypePath, name: &str) -> bool {
364 p.path
365 .segments
366 .last()
367 .map(|s| s.ident == name)
368 .unwrap_or(false)
369}
370
371fn single_generic<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<&'a Type, String> {
372 first_generic(seg).ok_or_else(|| format!("`{what}` needs one type argument"))
373}
374
375fn first_generic(seg: &syn::PathSegment) -> Option<&Type> {
376 if let PathArguments::AngleBracketed(ab) = &seg.arguments {
377 for a in &ab.args {
378 if let GenericArgument::Type(t) = a {
379 return Some(t);
380 }
381 }
382 }
383 None
384}
385
386fn two_generics<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<(&'a Type, &'a Type), String> {
388 if let PathArguments::AngleBracketed(ab) = &seg.arguments {
389 let types: Vec<&Type> = ab
390 .args
391 .iter()
392 .filter_map(|a| match a {
393 GenericArgument::Type(t) => Some(t),
394 _ => None,
395 })
396 .collect();
397 if types.len() >= 2 {
398 return Ok((types[0], types[1]));
399 }
400 }
401 Err(format!("`{what}` needs two type arguments (key, value)"))
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn known(names: &[&str]) -> BTreeSet<String> {
409 names.iter().map(|s| s.to_string()).collect()
410 }
411 fn wit(s: &str) -> String {
412 rust_type_to_wit(&syn::parse_str::<Type>(s).unwrap()).unwrap()
413 }
414
415 #[test]
416 fn primitives_strings_containers() {
417 assert_eq!(wit("i64"), "s64");
418 assert_eq!(wit("u32"), "u32");
419 assert_eq!(wit("String"), "string");
420 assert_eq!(wit("& str"), "string");
421 assert_eq!(wit("Vec<u8>"), "list<u8>");
422 assert_eq!(wit("Option<i32>"), "option<s32>");
423 assert_eq!(wit("&[u8]"), "list<u8>");
424 }
425
426 #[test]
427 fn maps_tuples_and_nesting() {
428 assert_eq!(wit("HashMap<String, i64>"), "list<tuple<string, s64>>");
430 assert_eq!(wit("BTreeMap<u32, String>"), "list<tuple<u32, string>>");
431 assert_eq!(wit("(i32, String)"), "tuple<s32, string>");
433 assert_eq!(wit("(u8, u8, bool)"), "tuple<u8, u8, bool>");
434 assert_eq!(wit("Vec<Option<i32>>"), "list<option<s32>>");
436 assert_eq!(wit("Option<Vec<u8>>"), "option<list<u8>>");
437 assert_eq!(
438 wit("HashMap<String, Vec<i64>>"),
439 "list<tuple<string, list<s64>>>"
440 );
441 let k = known(&["Row"]);
443 let wk = |s: &str| wit_type_with(&syn::parse_str::<Type>(s).unwrap(), &k).unwrap();
444 assert_eq!(wk("Vec<Row>"), "list<row>");
445 assert_eq!(wk("HashMap<String, Row>"), "list<tuple<string, row>>");
446 assert_eq!(wk("(Row, i32)"), "tuple<row, s32>");
447 }
448
449 #[test]
450 fn returns() {
451 let ret = |s: &str| return_to_wit(Some(&syn::parse_str::<Type>(s).unwrap())).unwrap();
452 assert_eq!(ret("()"), None);
453 assert_eq!(
454 ret("Result<i64, PluginError>"),
455 Some("result<s64, plugin-error>".into())
456 );
457 assert_eq!(
458 ret("Result<(), PluginError>"),
459 Some("result<_, plugin-error>".into())
460 );
461 }
462
463 #[test]
464 fn user_types_need_the_known_set() {
465 let ty: Type = syn::parse_str("Point").unwrap();
466 assert!(rust_type_to_wit(&ty).is_err()); let k = known(&["Point"]);
468 assert_eq!(wit_type_with(&ty, &k).unwrap(), "point");
469 let v: Type = syn::parse_str("Vec<Point>").unwrap();
471 assert_eq!(wit_type_with(&v, &k).unwrap(), "list<point>");
472 let o: Type = syn::parse_str("Option<BytePipe>").unwrap();
473 assert_eq!(
474 wit_type_with(&o, &known(&["BytePipe"])).unwrap(),
475 "option<byte-pipe>"
476 );
477 }
478
479 #[test]
480 fn struct_renders_to_record() {
481 let item: ItemStruct = syn::parse_str("struct Point { x: i32, y_pos: u64 }").unwrap();
482 let rec = struct_to_record(&item, &BTreeSet::new()).unwrap();
483 assert!(rec.contains("record point {"));
484 assert!(rec.contains("x: s32,"));
485 assert!(rec.contains("y-pos: u64,"));
486 }
487
488 #[test]
489 fn struct_with_nested_user_type() {
490 let item: ItemStruct = syn::parse_str("struct Line { from: Point, to: Point }").unwrap();
491 let rec = struct_to_record(&item, &known(&["Point"])).unwrap();
492 assert!(rec.contains("from: point,"));
493 assert!(rec.contains("to: point,"));
494 }
495
496 #[test]
497 fn enum_renders_to_variant() {
498 let item: ItemEnum =
499 syn::parse_str("enum Shape { Circle(u32), Rect(Point), Dot }").unwrap();
500 let (records, var) = enum_to_wit(&item, &known(&["Point"])).unwrap();
501 assert!(records.is_empty());
502 assert!(var.contains("variant shape {"));
503 assert!(var.contains("circle(u32),"));
504 assert!(var.contains("rect(point),"));
505 assert!(var.contains("dot,"));
506 }
507
508 #[test]
509 fn struct_variant_synthesizes_a_record() {
510 let item: ItemEnum = syn::parse_str("enum Shape { Rect { w: u32, h: u32 }, Dot }").unwrap();
511 let (records, var) = enum_to_wit(&item, &BTreeSet::new()).unwrap();
512 assert_eq!(records.len(), 1);
513 assert!(records[0].contains("record shape-rect {"));
514 assert!(records[0].contains("w: u32,"));
515 assert!(records[0].contains("h: u32,"));
516 assert!(var.contains("rect(shape-rect),"));
517 assert!(var.contains("dot,"));
518 }
519
520 #[test]
521 fn multifield_tuple_variant_is_rejected() {
522 let item: ItemEnum = syn::parse_str("enum E { Pair(u32, u32) }").unwrap();
523 assert!(enum_to_wit(&item, &BTreeSet::new()).is_err());
524 }
525
526 #[test]
527 fn full_document_places_type_defs_before_funcs() {
528 let recs = vec![struct_to_record(
529 &syn::parse_str("struct Point { x: i32, y: i32 }").unwrap(),
530 &BTreeSet::new(),
531 )
532 .unwrap()];
533 let methods = vec![WitMethod {
534 name: "midpoint".into(),
535 params: vec![("a".into(), "point".into()), ("b".into(), "point".into())],
536 ret: Some("point".into()),
537 stream_item: None,
538 }];
539 let doc = render_wit_full("geo", &recs, &methods);
540 assert!(doc.contains("package fidius:geo@0.1.0;"));
541 let rec_at = doc.find("record point {").unwrap();
542 let fn_at = doc.find("midpoint: func").unwrap();
543 assert!(
544 rec_at < fn_at,
545 "records must precede funcs in the interface"
546 );
547 assert!(doc.contains("midpoint: func(a: point, b: point) -> point;"));
548 assert!(doc.contains("fidius-interface-hash: func() -> u64;"));
549 assert!(doc.contains("world geo-plugin {"));
550 }
551
552 #[test]
553 fn streaming_method_renders_a_resource() {
554 let methods = vec![WitMethod {
555 name: "tick".into(),
556 params: vec![("count".into(), "u32".into())],
557 ret: None,
558 stream_item: Some("u64".into()),
559 }];
560 let doc = render_wit("ticker", &methods);
561 assert!(doc.contains("resource tick-stream {"));
563 assert!(doc.contains("next: func() -> result<option<u64>, plugin-error>;"));
564 assert!(doc.contains("tick: func(count: u32) -> tick-stream;"));
566 assert!(doc.find("resource tick-stream").unwrap() < doc.find("tick: func").unwrap());
568 }
569
570 #[test]
571 fn stream_item_type_detects_marker() {
572 let ty: Type = syn::parse_str("fidius::Stream<u64>").unwrap();
573 let item = stream_item_type(&ty).unwrap();
574 assert_eq!(rust_type_to_wit(item).unwrap(), "u64");
575 let bare: Type = syn::parse_str("Stream<String>").unwrap();
576 assert!(stream_item_type(&bare).is_some());
577 let not: Type = syn::parse_str("Vec<u64>").unwrap();
578 assert!(stream_item_type(¬).is_none());
579 }
580}