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 {
43 let s = s.strip_prefix("r#").unwrap_or(s);
44 let mut out = String::new();
45 for (i, ch) in s.chars().enumerate() {
46 if ch == '_' {
47 out.push('-');
48 } else if ch.is_uppercase() {
49 if i != 0 {
50 out.push('-');
51 }
52 out.extend(ch.to_lowercase());
53 } else {
54 out.push(ch);
55 }
56 }
57 out
58}
59
60const WIT_KEYWORDS: &[&str] = &[
70 "use",
71 "type",
72 "func",
73 "u8",
74 "u16",
75 "u32",
76 "u64",
77 "s8",
78 "s16",
79 "s32",
80 "s64",
81 "f32",
82 "f64",
83 "char",
84 "resource",
85 "own",
86 "borrow",
87 "record",
88 "flags",
89 "variant",
90 "enum",
91 "bool",
92 "string",
93 "option",
94 "result",
95 "future",
96 "stream",
97 "error-context",
98 "list",
99 "as",
100 "from",
101 "static",
102 "interface",
103 "tuple",
104 "world",
105 "import",
106 "export",
107 "package",
108 "constructor",
109 "include",
110 "with",
111 "async",
112];
113
114pub fn is_wit_keyword(ident: &str) -> bool {
117 WIT_KEYWORDS.contains(&ident)
118}
119
120pub fn wit_ident(ident: &str) -> String {
130 if is_wit_keyword(ident) {
131 format!("%{ident}")
132 } else {
133 ident.to_string()
134 }
135}
136
137pub fn result_ok_type(ty: &Type) -> Option<&Type> {
139 if let Type::Path(p) = ty {
140 if let Some(seg) = p.path.segments.last() {
141 if seg.ident == "Result" {
142 return first_generic(seg);
143 }
144 }
145 }
146 None
147}
148
149pub struct WitMethod {
151 pub name: String,
153 pub params: Vec<(String, String)>,
155 pub ret: Option<String>,
157 pub stream_item: Option<String>,
163}
164
165pub fn stream_item_type(ty: &Type) -> Option<&Type> {
169 if let Type::Path(p) = ty {
170 if let Some(seg) = p.path.segments.last() {
171 if seg.ident == "Stream" {
172 return first_generic(seg);
173 }
174 }
175 }
176 None
177}
178
179pub fn wit_type_with(ty: &Type, known: &BTreeSet<String>) -> Result<String, String> {
183 match ty {
184 Type::Reference(r) => {
186 if let Type::Path(p) = r.elem.as_ref() {
187 if path_is(p, "str") {
188 return Ok("string".to_string());
189 }
190 }
191 if let Type::Slice(s) = r.elem.as_ref() {
192 let inner = wit_type_with(&s.elem, known)?;
193 return Ok(format!("list<{inner}>"));
194 }
195 wit_type_with(&r.elem, known)
196 }
197 Type::Path(p) => {
198 let seg = p
199 .path
200 .segments
201 .last()
202 .ok_or_else(|| "empty type path".to_string())?;
203 let ident = seg.ident.to_string();
204 match ident.as_str() {
205 "bool" => Ok("bool".into()),
206 "i8" => Ok("s8".into()),
207 "i16" => Ok("s16".into()),
208 "i32" => Ok("s32".into()),
209 "i64" => Ok("s64".into()),
210 "u8" => Ok("u8".into()),
211 "u16" => Ok("u16".into()),
212 "u32" => Ok("u32".into()),
213 "u64" => Ok("u64".into()),
214 "f32" => Ok("f32".into()),
215 "f64" => Ok("f64".into()),
216 "char" => Ok("char".into()),
217 "String" => Ok("string".into()),
218 "Vec" => {
219 let inner = single_generic(seg, "Vec")?;
220 Ok(format!("list<{}>", wit_type_with(inner, known)?))
221 }
222 "Option" => {
223 let inner = single_generic(seg, "Option")?;
224 Ok(format!("option<{}>", wit_type_with(inner, known)?))
225 }
226 "HashMap" | "BTreeMap" => {
230 let (k, v) = two_generics(seg, &ident)?;
231 Ok(format!(
232 "list<tuple<{}, {}>>",
233 wit_type_with(k, known)?,
234 wit_type_with(v, known)?
235 ))
236 }
237 other if known.contains(other) => Ok(wit_ident(&to_kebab_case(other))),
240 other => Err(format!(
241 "type `{other}` is not supported in a WASM fidius interface \
242 (supported: bool, i8..i64, u8..u64, f32/f64, char, String, Vec<T>, \
243 Option<T>, HashMap/BTreeMap<K, V>, tuples, Result<T, PluginError>, \
244 and #[derive(WitType)] structs/enums)"
245 )),
246 }
247 }
248 Type::Tuple(t) if t.elems.is_empty() => {
249 Err("unit `()` is not a valid argument type".to_string())
250 }
251 Type::Tuple(t) => {
253 let elems = t
254 .elems
255 .iter()
256 .map(|e| wit_type_with(e, known))
257 .collect::<Result<Vec<_>, _>>()?;
258 Ok(format!("tuple<{}>", elems.join(", ")))
259 }
260 _ => Err("unsupported type in a WASM fidius interface".to_string()),
261 }
262}
263
264pub fn rust_type_to_wit(ty: &Type) -> Result<String, String> {
267 wit_type_with(ty, &BTreeSet::new())
268}
269
270pub fn return_to_wit_with(
274 ret: Option<&Type>,
275 known: &BTreeSet<String>,
276) -> Result<Option<String>, String> {
277 let Some(ty) = ret else { return Ok(None) };
278 if is_unit(ty) {
279 return Ok(None);
280 }
281 if let Type::Path(p) = ty {
282 if let Some(seg) = p.path.segments.last() {
283 if seg.ident == "Result" {
284 let ok = first_generic(seg).ok_or_else(|| "Result needs type args".to_string())?;
285 let ok_wit = if is_unit(ok) {
286 "_".to_string()
287 } else {
288 wit_type_with(ok, known)?
289 };
290 return Ok(Some(format!("result<{ok_wit}, plugin-error>")));
291 }
292 }
293 }
294 Ok(Some(wit_type_with(ty, known)?))
295}
296
297pub fn return_to_wit(ret: Option<&Type>) -> Result<Option<String>, String> {
299 return_to_wit_with(ret, &BTreeSet::new())
300}
301
302pub fn struct_to_record(item: &ItemStruct, known: &BTreeSet<String>) -> Result<String, String> {
306 let name = to_kebab_case(&item.ident.to_string());
307 let Fields::Named(fields) = &item.fields else {
308 return Err(format!(
309 "WitType struct `{}` must have named fields (tuple/unit structs are not supported)",
310 item.ident
311 ));
312 };
313 let mut out = format!(" record {} {{\n", wit_ident(&name));
314 for f in &fields.named {
315 let fname = to_kebab_case(&f.ident.as_ref().unwrap().to_string());
316 let fty = wit_type_with(&f.ty, known)
317 .map_err(|e| format!("field `{}` of `{}`: {e}", fname, item.ident))?;
318 out.push_str(&format!(" {}: {fty},\n", wit_ident(&fname)));
319 }
320 out.push_str(" }\n");
321 Ok(out)
322}
323
324pub fn enum_to_wit(
333 item: &ItemEnum,
334 known: &BTreeSet<String>,
335) -> Result<(Vec<String>, String), String> {
336 let ename = item.ident.to_string();
337 let name = to_kebab_case(&ename);
338 let mut records = Vec::new();
339 let mut out = format!(" variant {} {{\n", wit_ident(&name));
340 for v in &item.variants {
341 let case = to_kebab_case(&v.ident.to_string());
342 match &v.fields {
343 Fields::Unit => out.push_str(&format!(" {},\n", wit_ident(&case))),
344 Fields::Unnamed(u) if u.unnamed.len() == 1 => {
345 let payload = wit_type_with(&u.unnamed[0].ty, known)
346 .map_err(|e| format!("variant `{ename}::{}`: {e}", v.ident))?;
347 out.push_str(&format!(" {}({payload}),\n", wit_ident(&case)));
348 }
349 Fields::Named(f) => {
350 let rec_name = format!("{name}-{case}");
355 let mut rec = format!(" record {rec_name} {{\n");
356 for fl in &f.named {
357 let fname = to_kebab_case(&fl.ident.as_ref().unwrap().to_string());
358 let fty = wit_type_with(&fl.ty, known)
359 .map_err(|e| format!("field `{fname}` of `{ename}::{}`: {e}", v.ident))?;
360 rec.push_str(&format!(" {}: {fty},\n", wit_ident(&fname)));
361 }
362 rec.push_str(" }\n");
363 records.push(rec);
364 out.push_str(&format!(" {}({rec_name}),\n", wit_ident(&case)));
365 }
366 Fields::Unnamed(_) => {
367 return Err(format!(
368 "WitType enum `{ename}` variant `{}` has multiple tuple fields; \
369 a WIT variant case takes one payload — use a struct variant \
370 `{} {{ .. }}` (or a single field)",
371 v.ident, v.ident
372 ));
373 }
374 }
375 }
376 out.push_str(" }\n");
377 Ok((records, out))
378}
379
380pub fn render_wit_full(iface_kebab: &str, type_defs: &[String], methods: &[WitMethod]) -> String {
385 let iface = wit_ident(iface_kebab);
390 let mut s = String::new();
391 s.push_str(&format!("package fidius:{iface}@0.1.0;\n\n"));
392 s.push_str(&format!("interface {iface} {{\n"));
393 s.push_str(" record plugin-error {\n");
394 s.push_str(" code: string,\n");
395 s.push_str(" message: string,\n");
396 s.push_str(" details: option<string>,\n");
397 s.push_str(" }\n");
398 for def in type_defs {
399 s.push_str(def);
400 }
401 for m in methods {
404 if let Some(item) = &m.stream_item {
405 s.push_str(&format!(" resource {}-stream {{\n", m.name));
406 s.push_str(&format!(
407 " next: func() -> result<option<{item}>, plugin-error>;\n"
408 ));
409 s.push_str(" }\n");
410 }
411 }
412 for m in methods {
413 let params = m
414 .params
415 .iter()
416 .map(|(n, t)| format!("{}: {t}", wit_ident(n)))
417 .collect::<Vec<_>>()
418 .join(", ");
419 let fname = wit_ident(&m.name);
422 if m.stream_item.is_some() {
423 s.push_str(&format!(
425 " {fname}: func({params}) -> {}-stream;\n",
426 m.name
427 ));
428 } else {
429 match &m.ret {
430 Some(r) => s.push_str(&format!(" {fname}: func({params}) -> {r};\n")),
431 None => s.push_str(&format!(" {fname}: func({params});\n")),
432 }
433 }
434 }
435 s.push_str(" fidius-interface-hash: func() -> u64;\n");
436 s.push_str(" fidius-configure: func(config: list<u8>);\n");
441 s.push_str("}\n\n");
442 s.push_str(&format!(
443 "world {iface_kebab}-plugin {{\n export {iface};\n}}\n"
444 ));
445 s
446}
447
448pub fn render_wit(iface_kebab: &str, methods: &[WitMethod]) -> String {
451 render_wit_full(iface_kebab, &[], methods)
452}
453
454fn is_unit(ty: &Type) -> bool {
457 matches!(ty, Type::Tuple(t) if t.elems.is_empty())
458}
459
460fn path_is(p: &syn::TypePath, name: &str) -> bool {
461 p.path
462 .segments
463 .last()
464 .map(|s| s.ident == name)
465 .unwrap_or(false)
466}
467
468fn single_generic<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<&'a Type, String> {
469 first_generic(seg).ok_or_else(|| format!("`{what}` needs one type argument"))
470}
471
472fn first_generic(seg: &syn::PathSegment) -> Option<&Type> {
473 if let PathArguments::AngleBracketed(ab) = &seg.arguments {
474 for a in &ab.args {
475 if let GenericArgument::Type(t) = a {
476 return Some(t);
477 }
478 }
479 }
480 None
481}
482
483fn two_generics<'a>(seg: &'a syn::PathSegment, what: &str) -> Result<(&'a Type, &'a Type), String> {
485 if let PathArguments::AngleBracketed(ab) = &seg.arguments {
486 let types: Vec<&Type> = ab
487 .args
488 .iter()
489 .filter_map(|a| match a {
490 GenericArgument::Type(t) => Some(t),
491 _ => None,
492 })
493 .collect();
494 if types.len() >= 2 {
495 return Ok((types[0], types[1]));
496 }
497 }
498 Err(format!("`{what}` needs two type arguments (key, value)"))
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 fn known(names: &[&str]) -> BTreeSet<String> {
506 names.iter().map(|s| s.to_string()).collect()
507 }
508 fn wit(s: &str) -> String {
509 rust_type_to_wit(&syn::parse_str::<Type>(s).unwrap()).unwrap()
510 }
511
512 #[test]
513 fn primitives_strings_containers() {
514 assert_eq!(wit("i64"), "s64");
515 assert_eq!(wit("u32"), "u32");
516 assert_eq!(wit("String"), "string");
517 assert_eq!(wit("& str"), "string");
518 assert_eq!(wit("Vec<u8>"), "list<u8>");
519 assert_eq!(wit("Option<i32>"), "option<s32>");
520 assert_eq!(wit("&[u8]"), "list<u8>");
521 }
522
523 #[test]
524 fn maps_tuples_and_nesting() {
525 assert_eq!(wit("HashMap<String, i64>"), "list<tuple<string, s64>>");
527 assert_eq!(wit("BTreeMap<u32, String>"), "list<tuple<u32, string>>");
528 assert_eq!(wit("(i32, String)"), "tuple<s32, string>");
530 assert_eq!(wit("(u8, u8, bool)"), "tuple<u8, u8, bool>");
531 assert_eq!(wit("Vec<Option<i32>>"), "list<option<s32>>");
533 assert_eq!(wit("Option<Vec<u8>>"), "option<list<u8>>");
534 assert_eq!(
535 wit("HashMap<String, Vec<i64>>"),
536 "list<tuple<string, list<s64>>>"
537 );
538 let k = known(&["Row"]);
540 let wk = |s: &str| wit_type_with(&syn::parse_str::<Type>(s).unwrap(), &k).unwrap();
541 assert_eq!(wk("Vec<Row>"), "list<row>");
542 assert_eq!(wk("HashMap<String, Row>"), "list<tuple<string, row>>");
543 assert_eq!(wk("(Row, i32)"), "tuple<row, s32>");
544 }
545
546 #[test]
547 fn returns() {
548 let ret = |s: &str| return_to_wit(Some(&syn::parse_str::<Type>(s).unwrap())).unwrap();
549 assert_eq!(ret("()"), None);
550 assert_eq!(
551 ret("Result<i64, PluginError>"),
552 Some("result<s64, plugin-error>".into())
553 );
554 assert_eq!(
555 ret("Result<(), PluginError>"),
556 Some("result<_, plugin-error>".into())
557 );
558 }
559
560 #[test]
561 fn user_types_need_the_known_set() {
562 let ty: Type = syn::parse_str("Point").unwrap();
563 assert!(rust_type_to_wit(&ty).is_err()); let k = known(&["Point"]);
565 assert_eq!(wit_type_with(&ty, &k).unwrap(), "point");
566 let v: Type = syn::parse_str("Vec<Point>").unwrap();
568 assert_eq!(wit_type_with(&v, &k).unwrap(), "list<point>");
569 let o: Type = syn::parse_str("Option<BytePipe>").unwrap();
570 assert_eq!(
571 wit_type_with(&o, &known(&["BytePipe"])).unwrap(),
572 "option<byte-pipe>"
573 );
574 }
575
576 #[test]
577 fn struct_renders_to_record() {
578 let item: ItemStruct = syn::parse_str("struct Point { x: i32, y_pos: u64 }").unwrap();
579 let rec = struct_to_record(&item, &BTreeSet::new()).unwrap();
580 assert!(rec.contains("record point {"));
581 assert!(rec.contains("x: s32,"));
582 assert!(rec.contains("y-pos: u64,"));
583 }
584
585 #[test]
586 fn struct_with_nested_user_type() {
587 let item: ItemStruct = syn::parse_str("struct Line { from: Point, to: Point }").unwrap();
588 let rec = struct_to_record(&item, &known(&["Point"])).unwrap();
589 assert!(rec.contains("from: point,"));
590 assert!(rec.contains("to: point,"));
591 }
592
593 #[test]
594 fn enum_renders_to_variant() {
595 let item: ItemEnum =
596 syn::parse_str("enum Shape { Circle(u32), Rect(Point), Dot }").unwrap();
597 let (records, var) = enum_to_wit(&item, &known(&["Point"])).unwrap();
598 assert!(records.is_empty());
599 assert!(var.contains("variant shape {"));
600 assert!(var.contains("circle(u32),"));
601 assert!(var.contains("rect(point),"));
602 assert!(var.contains("dot,"));
603 }
604
605 #[test]
606 fn struct_variant_synthesizes_a_record() {
607 let item: ItemEnum = syn::parse_str("enum Shape { Rect { w: u32, h: u32 }, Dot }").unwrap();
608 let (records, var) = enum_to_wit(&item, &BTreeSet::new()).unwrap();
609 assert_eq!(records.len(), 1);
610 assert!(records[0].contains("record shape-rect {"));
611 assert!(records[0].contains("w: u32,"));
612 assert!(records[0].contains("h: u32,"));
613 assert!(var.contains("rect(shape-rect),"));
614 assert!(var.contains("dot,"));
615 }
616
617 #[test]
618 fn multifield_tuple_variant_is_rejected() {
619 let item: ItemEnum = syn::parse_str("enum E { Pair(u32, u32) }").unwrap();
620 assert!(enum_to_wit(&item, &BTreeSet::new()).is_err());
621 }
622
623 #[test]
624 fn full_document_places_type_defs_before_funcs() {
625 let recs = vec![struct_to_record(
626 &syn::parse_str("struct Point { x: i32, y: i32 }").unwrap(),
627 &BTreeSet::new(),
628 )
629 .unwrap()];
630 let methods = vec![WitMethod {
631 name: "midpoint".into(),
632 params: vec![("a".into(), "point".into()), ("b".into(), "point".into())],
633 ret: Some("point".into()),
634 stream_item: None,
635 }];
636 let doc = render_wit_full("geo", &recs, &methods);
637 assert!(doc.contains("package fidius:geo@0.1.0;"));
638 let rec_at = doc.find("record point {").unwrap();
639 let fn_at = doc.find("midpoint: func").unwrap();
640 assert!(
641 rec_at < fn_at,
642 "records must precede funcs in the interface"
643 );
644 assert!(doc.contains("midpoint: func(a: point, b: point) -> point;"));
645 assert!(doc.contains("fidius-interface-hash: func() -> u64;"));
646 assert!(doc.contains("world geo-plugin {"));
647 }
648
649 #[test]
650 fn streaming_method_renders_a_resource() {
651 let methods = vec![WitMethod {
652 name: "tick".into(),
653 params: vec![("count".into(), "u32".into())],
654 ret: None,
655 stream_item: Some("u64".into()),
656 }];
657 let doc = render_wit("ticker", &methods);
658 assert!(doc.contains("resource tick-stream {"));
660 assert!(doc.contains("next: func() -> result<option<u64>, plugin-error>;"));
661 assert!(doc.contains("tick: func(count: u32) -> tick-stream;"));
663 assert!(doc.find("resource tick-stream").unwrap() < doc.find("tick: func").unwrap());
665 }
666
667 #[test]
668 fn keyword_idents_are_escaped_others_untouched() {
669 assert_eq!(wit_ident("stream"), "%stream");
670 assert_eq!(wit_ident("record"), "%record");
671 assert_eq!(wit_ident("from"), "%from");
672 assert_eq!(wit_ident("error-context"), "%error-context");
673 assert_eq!(wit_ident("row"), "row");
674 assert_eq!(wit_ident("dead-letter"), "dead-letter");
675 assert!(is_wit_keyword("result") && !is_wit_keyword("results"));
676 }
677
678 #[test]
679 fn raw_idents_lose_their_prefix_then_escape() {
680 assert_eq!(to_kebab_case("r#type"), "type");
682 assert_eq!(wit_ident(&to_kebab_case("r#type")), "%type");
683 assert_eq!(wit_ident(&to_kebab_case("r#async")), "%async");
684 assert_eq!(to_kebab_case("r#match"), "match");
686 }
687
688 #[test]
689 fn record_with_keyword_fields_escapes_field_and_record_names() {
690 let item: ItemStruct =
692 syn::parse_str("struct Record { record: String, stream: u64, from: bool, r#type: u8 }")
693 .unwrap();
694 let rec = struct_to_record(&item, &BTreeSet::new()).unwrap();
695 assert!(rec.contains("record %record {"), "got: {rec}");
696 assert!(rec.contains("%record: string,"));
697 assert!(rec.contains("%stream: u64,"));
698 assert!(rec.contains("%from: bool,"));
699 assert!(rec.contains("%type: u8,"));
700 }
701
702 #[test]
703 fn variant_with_keyword_cases_escapes_them() {
704 let item: ItemEnum =
705 syn::parse_str("enum Variant { Stream, Record(u32), List { from: u8 } }").unwrap();
706 let (records, var) = enum_to_wit(&item, &BTreeSet::new()).unwrap();
707 assert!(var.contains("variant %variant {"), "got: {var}");
708 assert!(var.contains("%stream,"));
709 assert!(var.contains("%record(u32),"));
710 assert!(var.contains("%list(variant-list),"));
713 assert!(records[0].contains("record variant-list {"));
714 assert!(records[0].contains("%from: u8,"));
715 }
716
717 #[test]
718 fn keyword_user_type_reference_matches_its_declaration() {
719 let k = known(&["Record"]);
722 let ty: Type = syn::parse_str("Vec<Record>").unwrap();
723 assert_eq!(wit_type_with(&ty, &k).unwrap(), "list<%record>");
724 }
725
726 #[test]
727 fn stream_item_type_detects_marker() {
728 let ty: Type = syn::parse_str("fidius::Stream<u64>").unwrap();
729 let item = stream_item_type(&ty).unwrap();
730 assert_eq!(rust_type_to_wit(item).unwrap(), "u64");
731 let bare: Type = syn::parse_str("Stream<String>").unwrap();
732 assert!(stream_item_type(&bare).is_some());
733 let not: Type = syn::parse_str("Vec<u64>").unwrap();
734 assert!(stream_item_type(¬).is_none());
735 }
736}