diff --git a/Cargo.lock b/Cargo.lock index 2b5d90bbf..6511159f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3087,6 +3087,17 @@ dependencies = [ "wrpc-transport", ] +[[package]] +name = "wit-bindgen-wrpc-go" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "heck 0.5.0", + "test-helpers", + "wit-bindgen-core", +] + [[package]] name = "wit-bindgen-wrpc-rust" version = "0.3.3" @@ -3139,6 +3150,7 @@ dependencies = [ "anyhow", "async-nats", "bytes", + "clap", "futures", "http-body 1.0.0", "http-body-util", @@ -3148,7 +3160,10 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "wit-bindgen-core", "wit-bindgen-wrpc", + "wit-bindgen-wrpc-go", + "wit-bindgen-wrpc-rust", "wrpc-interface-blobstore", "wrpc-interface-http", "wrpc-interface-keyvalue", diff --git a/Cargo.toml b/Cargo.toml index 0f142af66..9db2e52c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,20 @@ nats = ["dep:wrpc-transport-nats"] wasmtime = ["dep:wrpc-runtime-wasmtime"] [dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = [ + "color", + "derive", + "error-context", + "help", + "std", + "suggestions", + "usage", +] } +wit-bindgen-core = { workspace = true } wit-bindgen-wrpc = { workspace = true } +wit-bindgen-wrpc-go = { workspace = true, features = ["clap"] } +wit-bindgen-wrpc-rust = { workspace = true, features = ["clap"] } wrpc-interface-blobstore = { workspace = true } wrpc-interface-http = { workspace = true } wrpc-interface-keyvalue = { workspace = true } @@ -96,6 +109,7 @@ wasmtime-wasi-http = { version = "20", default-features = false } webpki-roots = { version = "0.26", default-features = false } wit-bindgen-core = { version = "0.24", default-features = false } wit-bindgen-wrpc = { version = "0.3.6", default-features = false, path = "./crates/wit-bindgen" } +wit-bindgen-wrpc-go = { version = "0.1.0", default-features = false, path = "./crates/wit-bindgen-go" } wit-bindgen-wrpc-rust = { version = "0.3.3", default-features = false, path = "./crates/wit-bindgen-rust" } wit-bindgen-wrpc-rust-macro = { version = "0.3.4", default-features = false, path = "./crates/wit-bindgen-rust-macro" } wit-parser = { version = "0.202", default-features = false } diff --git a/crates/wit-bindgen-go/Cargo.toml b/crates/wit-bindgen-go/Cargo.toml new file mode 100644 index 000000000..d24f128cd --- /dev/null +++ b/crates/wit-bindgen-go/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "wit-bindgen-wrpc-go" +version = "0.1.0" +homepage = 'https://github.com/bytecodealliance/wit-bindgen' +description = """ +Go bindings generator for wRPC +""" +authors = ["Roman Volosatovs "] + +categories.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +test = false +doctest = false + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"], optional = true } +heck = { workspace = true } +wit-bindgen-core = { workspace = true } + +[dev-dependencies] +test-helpers = { workspace = true } diff --git a/crates/wit-bindgen-go/src/interface.rs b/crates/wit-bindgen-go/src/interface.rs new file mode 100644 index 000000000..b1b0ec482 --- /dev/null +++ b/crates/wit-bindgen-go/src/interface.rs @@ -0,0 +1,1892 @@ +use crate::{ + int_repr, to_go_ident, to_package_ident, to_upper_camel_case, Deps, FnSig, GoWrpc, Identifier, + InterfaceName, RustFlagsRepr, +}; +use heck::*; +use std::collections::BTreeMap; +use std::fmt::Write as _; +use std::mem; +use wit_bindgen_core::{dealias, uwrite, uwriteln, wit_parser::*, Source, TypeInfo}; + +pub struct InterfaceGenerator<'a> { + pub src: Source, + pub(super) identifier: Identifier<'a>, + pub in_import: bool, + pub(super) gen: &'a mut GoWrpc, + pub resolve: &'a Resolve, + pub deps: Deps, +} + +impl InterfaceGenerator<'_> { + fn print_encode_ty(&mut self, ty: &Type, name: &str, writer: &str) { + match ty { + Type::Id(t) => self.print_encode_tyid(*t, name, writer), + Type::Bool => uwrite!( + self.src, + r#"func(v bool, w {wrpc}.ByteWriter) error {{ + if !v {{ + {slog}.Debug("writing `false` byte") + return w.WriteByte(0) + }} + {slog}.Debug("writing `true` byte") + return w.WriteByte(1) + }}({name}, {writer})"#, + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::U8 => uwrite!( + self.src, + r#"func(v uint8, w {wrpc}.ByteWriter) error {{ + {slog}.Debug("writing u8 byte") + return w.WriteByte(v) + }}({name}, {writer})"#, + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::U16 => uwrite!( + self.src, + r#"func(v uint16, w {wrpc}.ByteWriter) error {{ + b := make([]byte, {binary}.MaxVarintLen16) + i := {binary}.PutUvarint(b, uint64(v)) + {slog}.Debug("writing u16") + _, err := w.Write(b[:i]) + return err + }}({name}, {writer})"#, + binary = self.deps.binary(), + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::U32 => uwrite!( + self.src, + r#"func(v uint32, w {wrpc}.ByteWriter) error {{ + b := make([]byte, {binary}.MaxVarintLen32) + i := {binary}.PutUvarint(b, uint64(v)) + {slog}.Debug("writing u32") + _, err := w.Write(b[:i]) + return err + }}({name}, {writer})"#, + binary = self.deps.binary(), + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::U64 => uwrite!( + self.src, + r#"func(v uint64, w {wrpc}.ByteWriter) error {{ + b := make([]byte, {binary}.MaxVarintLen64) + i := {binary}.PutUvarint(b, uint64(v)) + {slog}.Debug("writing u64") + _, err := w.Write(b[:i]) + return err + }}({name}, {writer})"#, + binary = self.deps.binary(), + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::S8 => uwrite!( + self.src, + r#"{errors}.New("writing s8 not supported yet")"#, + errors = self.deps.errors(), + ), + Type::S16 => uwrite!( + self.src, + r#"{errors}.New("writing s16 not supported yet")"#, + errors = self.deps.errors(), + ), + Type::S32 => uwrite!( + self.src, + r#"{errors}.New("writing s32 not supported yet")"#, + errors = self.deps.errors(), + ), + Type::S64 => uwrite!( + self.src, + r#"{errors}.New("writing s64 not supported yet")"#, + errors = self.deps.errors(), + ), + Type::F32 => uwrite!( + self.src, + r#"func(v float32, w {wrpc}.ByteWriter) error {{ + b := make([]byte, 4) + {binary}.LittleEndian.PutUint32(b, {math}.Float32bits(x)) + {slog}.Debug("writing f32") + _, err := w.Write(b) + return err + }}({name}, {writer})"#, + binary = self.deps.binary(), + math = self.deps.math(), + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::F64 => uwrite!( + self.src, + r#"func(v float64, w {wrpc}.ByteWriter) error {{ + b := make([]byte, 4) + {binary}.LittleEndian.PutUint64(b, {math}.Float64bits(x)) + {slog}.Debug("writing f64") + _, err := w.Write(b) + return err + }}({name}, {writer})"#, + binary = self.deps.binary(), + math = self.deps.math(), + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::Char => uwrite!( + self.src, + r#"func(v rune, w {wrpc}.ByteWriter) error {{ + b := make([]byte, {binary}.MaxVarintLen32) + i := {binary}.PutUvarint(b, uint64(v)) + {slog}.Debug("writing char") + _, err := w.Write(b[:i]) + return err + }}({name}, {writer})"#, + binary = self.deps.binary(), + slog = self.deps.slog(), + wrpc = self.deps.wrpc(), + ), + Type::String => { + let fmt = self.deps.fmt(); + let math = self.deps.math(); + let slog = self.deps.slog(); + let wrpc = self.deps.wrpc(); + + uwrite!( + self.src, + r#"func(v string, w {wrpc}.ByteWriter) error {{ + n := len(v) + if n > {math}.MaxUint32 {{ + return {fmt}.Errorf("string byte length of %d overflows a 32-bit integer", n) + }} + {slog}.Debug("writing string byte length", "len", n) + if err := "# + ); + self.print_encode_ty(&Type::U32, "uint32(n)", "w"); + uwrite!( + self.src, + r#"; err != nil {{ + return {fmt}.Errorf("failed to write string length of %d: %w", n, err) + }} + {slog}.Debug("writing string bytes") + _, err := w.Write([]byte(v)) + if err != nil {{ + return {fmt}.Errorf("failed to write string bytes: %w", err) + }} + return nil + }}({name}, {writer})"# + ); + } + } + } + + fn print_encode_tyid(&mut self, ty: TypeId, name: &str, writer: &str) { + //let ty = &self.resolve.types[id]; + //if ty.name.is_some() { + // let name = self.type_path(id); + // self.push_str(&name); + // return; + //} + + //match &ty.kind { + // TypeDefKind::List(t) => self.print_list(t), + + // TypeDefKind::Option(t) => { + // self.push_str("*"); + // self.print_ty(t); + // } + + // TypeDefKind::Result(r) => { + // self.push_str("wrpc.Result["); + // self.print_optional_ty(r.ok.as_ref()); + // self.push_str(","); + // self.print_optional_ty(r.err.as_ref()); + // self.push_str("]"); + // } + + // TypeDefKind::Variant(_) => panic!("unsupported anonymous variant"), + + // // Tuple-like records are mapped directly to Rust tuples of + // // types. Note the trailing comma after each member to + // // appropriately handle 1-tuples. + // TypeDefKind::Tuple(t) => { + // self.push_str("wrpc.Tuple"); + // uwrite!(self.src, "{}[", t.types.len()); + // for ty in t.types.iter() { + // self.print_ty(ty); + // self.push_str(","); + // } + // self.push_str("]"); + // } + // TypeDefKind::Resource => { + // panic!("unsupported anonymous type reference: resource") + // } + // TypeDefKind::Record(_) => { + // panic!("unsupported anonymous type reference: record") + // } + // TypeDefKind::Flags(_) => { + // panic!("unsupported anonymous type reference: flags") + // } + // TypeDefKind::Enum(_) => { + // panic!("unsupported anonymous type reference: enum") + // } + // TypeDefKind::Future(ty) => { + // self.push_str("wrpc.Receiver["); + // self.print_optional_ty(ty.as_ref()); + // self.push_str("]"); + // } + // TypeDefKind::Stream(stream) => { + // self.push_str("wrpc.Receiver[[]"); + // self.print_optional_ty(stream.element.as_ref()); + // self.push_str("]"); + // } + + // TypeDefKind::Handle(Handle::Own(ty)) => { + // self.print_ty(&Type::Id(*ty)); + // } + + // TypeDefKind::Handle(Handle::Borrow(ty)) => { + // if self.is_exported_resource(*ty) { + // let camel = self.resolve.types[*ty] + // .name + // .as_deref() + // .unwrap() + // .to_upper_camel_case(); + // let name = self.type_path_with_name(*ty, format!("{camel}Borrow")); + // self.push_str(&name); + // } else { + // let ty = &Type::Id(*ty); + // self.print_ty(ty); + // } + // } + + // TypeDefKind::Type(t) => self.print_ty(t), + + // TypeDefKind::Unknown => unreachable!(), + //} + } + + pub(super) fn generate_exports<'a>( + &mut self, + identifier: Identifier<'a>, + funcs: impl Iterator + Clone + ExactSizeIterator, + ) -> bool { + let mut traits = BTreeMap::new(); + let mut funcs_to_export = Vec::new(); + let mut resources_to_drop = Vec::new(); + + traits.insert(None, ("Handler".to_string(), Vec::new())); + + if let Identifier::Interface(id, ..) = identifier { + for (name, id) in self.resolve.interfaces[id].types.iter() { + match self.resolve.types[*id].kind { + TypeDefKind::Resource => {} + _ => continue, + } + resources_to_drop.push(name); + let camel = name.to_upper_camel_case(); + traits.insert(Some(*id), (format!("Handler{camel}"), Vec::new())); + } + } + + let n = funcs.len(); + for func in funcs { + if self.gen.skip.contains(&func.name) { + continue; + } + + let resource = match func.kind { + FunctionKind::Freestanding => None, + FunctionKind::Method(id) + | FunctionKind::Constructor(id) + | FunctionKind::Static(id) => Some(id), + }; + funcs_to_export.push(func); + let (_, methods) = traits.get_mut(&resource).unwrap(); + + let prev = mem::take(&mut self.src); + let sig = FnSig { + use_item_name: true, + ..Default::default() + }; + self.print_docs_and_params(func, &sig); + if let FunctionKind::Constructor(_) = &func.kind { + uwriteln!(self.src, " ({}, error)", "Self"); // TODO: Use the correct Go name + } else { + self.print_results(&func.results) + } + let trait_method = mem::replace(&mut self.src, prev); + methods.push(trait_method); + } + + let (name, methods) = traits.remove(&None).unwrap(); + if !methods.is_empty() || !traits.is_empty() { + self.generate_interface_trait( + &name, + &methods, + traits.iter().map(|(resource, (trait_name, _methods))| { + (resource.unwrap(), trait_name.as_str()) + }), + ) + } + + for (_, (trait_name, methods)) in traits.iter() { + uwriteln!(self.src, "type {trait_name} interface {{"); + for method in methods { + self.src.push_str(method); + } + uwriteln!(self.src, "}}"); + } + + if !funcs_to_export + .iter() + .any(|Function { kind, .. }| matches!(kind, FunctionKind::Freestanding)) + { + return false; + } + uwriteln!( + &mut self.src, + "func ServeInterface(c {wrpc}.Client, h Handler) (stop func() error, err error) {{", + wrpc = self.deps.wrpc(), + ); + uwriteln!(self.src, r#"stops := make([]func() error, 0, {n})"#); + self.src.push_str( + r#"stop = func() error { + for _, stop := range stops { + if err := stop(); err != nil { + return err + } + } + return nil + } +"#, + ); + let instance = match identifier { + Identifier::Interface(id, name) => { + let interface = &self.resolve.interfaces[id]; + let name = match name { + WorldKey::Name(s) => s.to_string(), + WorldKey::Interface(..) => interface + .name + .as_ref() + .expect("interface name missing") + .to_string(), + }; + if let Some(package) = interface.package { + self.resolve.id_of_name(package, &name) + } else { + name + } + } + Identifier::World(world) => { + let World { + ref name, package, .. + } = self.resolve.worlds[world]; + if let Some(package) = package { + self.resolve.id_of_name(package, name) + } else { + name.to_string() + } + } + }; + for ( + i, + Function { + kind, + name, + params, + results, + .. + }, + ) in funcs_to_export.iter().enumerate() + { + if !matches!(kind, FunctionKind::Freestanding) { + continue; + } + if !params.is_empty() { + eprintln!("skip function with non-empty parameters"); + continue; + } + let bytes = self.deps.bytes(); + let context = self.deps.context(); + let fmt = self.deps.fmt(); + let slog = self.deps.slog(); + let wrpc = self.deps.wrpc(); + uwriteln!( + self.src, + r#"stop{i}, err := c.Serve("{instance}", "{name}", func(ctx {context}.Context, buffer []byte, tx {wrpc}.Transmitter, inv {wrpc}.IncomingInvocation) error {{"#, + ); + uwriteln!( + self.src, + r#"{slog}.DebugContext(ctx, "accepting handshake")"#, + ); + self.src + .push_str("if err := inv.Accept(ctx, nil); err != nil {\n"); + uwriteln!( + self.src, + r#"return {fmt}.Errorf("failed to complete handshake: %w", err)"#, + ); + self.src.push_str("}\n"); + uwriteln!( + self.src, + r#"{slog}.DebugContext(ctx, "calling `{instance}.{name}` handler")"#, + ); + for (i, _) in results.iter_types().enumerate() { + uwrite!(self.src, "r{i}, "); + } + self.push_str("err := h."); + self.push_str(&name.to_upper_camel_case()); + self.push_str("(ctx"); + for (i, _) in params.iter().enumerate() { + uwrite!(self.src, ", p{i}"); + } + self.push_str(")\n"); + self.push_str("if err != nil {\n"); + uwriteln!( + self.src, + r#"return {fmt}.Errorf("failed to handle `{instance}.{name}` invocation: %w", err)"#, + ); + self.push_str("}\n"); + + uwriteln!(self.src, r#"var buf {bytes}.Buffer"#,); + for (i, ty) in results.iter_types().enumerate() { + self.push_str("if err :="); + self.print_encode_ty(ty, &format!("r{i}"), "&buf"); + self.push_str("; err != nil {\n"); + uwriteln!( + self.src, + r#"return {fmt}.Errorf("failed to write result value {i}: %w", err)"#, + ); + self.src.push_str("}\n"); + } + + uwriteln!( + self.src, + r#"{slog}.DebugContext(ctx, "transmitting `{instance}.{name}` result")"#, + ); + uwriteln!( + self.src, + r#"if err := tx.Transmit({context}.Background(), buf.Bytes()); err != nil {{ + return {fmt}.Errorf("failed to transmit result: %w", err) + }}"#, + ); + self.push_str("return nil\n"); + + uwriteln!( + self.src, + r#"}}) + if err != nil {{ + return nil, {fmt}.Errorf("failed to serve `{instance}.{name}`: %w", err) + }} + stops = append(stops, stop{i})"#, + fmt = self.deps.fmt(), + ); + } + self.push_str("return stop, nil\n"); + self.push_str("}\n"); + true + } + + fn generate_interface_trait<'a>( + &mut self, + trait_name: &str, + methods: &[Source], + resource_traits: impl Iterator, + ) { + uwriteln!(self.src, "type {trait_name} interface {{"); + for (id, trait_name) in resource_traits { + let name = self.resolve.types[id] + .name + .as_ref() + .unwrap() + .to_upper_camel_case(); + uwriteln!(self.src, "//type {name} {trait_name}"); + } + for method in methods { + self.src.push_str(method); + } + uwriteln!(self.src, "}}"); + } + + pub fn generate_imports<'a>( + &mut self, + instance: &str, + funcs: impl Iterator, + ) { + for func in funcs { + self.generate_guest_import(instance, func); + } + } + + pub fn finish(&mut self) -> String { + mem::take(&mut self.src).into() + } + + pub fn start_append_submodule(&mut self, name: &WorldKey) -> (String, Vec) { + let snake = match name { + WorldKey::Name(name) => to_package_ident(name), + WorldKey::Interface(id) => { + to_package_ident(self.resolve.interfaces[*id].name.as_ref().unwrap()) + } + }; + let module_path = crate::compute_module_path(name, self.resolve, !self.in_import); + (snake, module_path) + } + + pub fn finish_append_submodule(mut self, snake: &str, module_path: Vec) { + let module = self.finish(); + let module = format!( + r#"package {snake} + +{} + +{module}"#, + self.deps, + ); + let map = if self.in_import { + &mut self.gen.import_modules + } else { + &mut self.gen.export_modules + }; + map.push((module, module_path)) + } + + fn generate_guest_import(&mut self, instance: &str, func: &Function) { + if self.gen.skip.contains(&func.name) { + return; + } + + let mut sig = FnSig::default(); + match func.kind { + FunctionKind::Freestanding => {} + FunctionKind::Method(id) | FunctionKind::Static(id) | FunctionKind::Constructor(id) => { + let name = self.resolve.types[id].name.as_ref().unwrap(); + let name = to_upper_camel_case(name); + uwriteln!(self.src, "impl {name} {{"); + sig.use_item_name = true; + if let FunctionKind::Method(_) = &func.kind { + sig.self_arg = Some("&self".into()); + sig.self_is_first_param = true; + } + } + } + let params = self.print_signature(func, &sig); + self.src.push_str("{\n"); + self.src.push_str("tx__, txErr__ := wrpc__.Invoke("); + match func.kind { + FunctionKind::Freestanding + | FunctionKind::Static(..) + | FunctionKind::Constructor(..) => { + uwrite!(self.src, r#""{instance}""#); + } + FunctionKind::Method(..) => { + self.src.push_str("self.0"); + } + } + self.src.push_str(r#", ""#); + self.src.push_str(&func.name); + self.src.push_str(r#"", "#); + match ¶ms[..] { + [] => self.src.push_str("()"), + [p] => self.src.push_str(p), + _ => { + self.src.push_str("("); + for p in params { + uwrite!(self.src, "{p}, "); + } + self.src.push_str(")"); + } + } + // TODO: There should be a way to "split" the future and pass I/O to the caller + uwriteln!( + self.src, + r#" + if err != nil {{ + err__ = fmt.Sprintf("failed to invoke `{}`: %w", txErr__) + return + }} + //wrpc__.1.await.context("failed to transmit parameters")?; + //Ok(tx__) + panic("not supported yet") + return + }}"#, + func.name + ); + + match func.kind { + FunctionKind::Freestanding => {} + FunctionKind::Method(_) | FunctionKind::Static(_) | FunctionKind::Constructor(_) => { + self.src.push_str("}\n"); + } + } + } + + fn godoc(&mut self, docs: &Docs) { + let docs = match &docs.contents { + Some(docs) => docs, + None => return, + }; + for line in docs.trim().lines() { + self.push_str("///"); + if !line.is_empty() { + self.push_str(" "); + self.push_str(line); + } + self.push_str("\n"); + } + } + + fn rustdoc_params(&mut self, docs: &[(String, Type)], header: &str) { + let _ = (docs, header); + // let docs = docs + // .iter() + // .filter(|param| param.docs.trim().len() > 0) + // .collect::>(); + // if docs.len() == 0 { + // return; + // } + + // self.push_str("///\n"); + // self.push_str("/// ## "); + // self.push_str(header); + // self.push_str("\n"); + // self.push_str("///\n"); + + // for param in docs { + // for (i, line) in param.docs.lines().enumerate() { + // self.push_str("/// "); + // // Currently wasi only has at most one return value, so there's no + // // need to indent it or name it. + // if header != "Return" { + // if i == 0 { + // self.push_str("* `"); + // self.push_str(to_go_ident(param.name.as_str())); + // self.push_str("` - "); + // } else { + // self.push_str(" "); + // } + // } + // self.push_str(line); + // self.push_str("\n"); + // } + // } + } + + fn print_signature(&mut self, func: &Function, sig: &FnSig) -> Vec { + let params = self.print_docs_and_params(func, sig); + if let FunctionKind::Constructor(_) = &func.kind { + uwrite!(self.src, " (Self, error)",); + } else { + self.print_results(&func.results); + } + params + } + + fn print_docs_and_params(&mut self, func: &Function, sig: &FnSig) -> Vec { + self.godoc(&func.docs); + self.rustdoc_params(&func.params, "Parameters"); + // TODO: re-add this when docs are back + // self.rustdoc_params(&func.results, "Return"); + + if self.in_import { + self.push_str("func "); + } + let func_name = if sig.use_item_name { + if let FunctionKind::Constructor(_) = &func.kind { + "New" + } else { + func.item_name() + } + } else { + &func.name + }; + if let Some(arg) = &sig.self_arg { + self.push_str("("); + self.push_str(arg); + self.push_str(")"); + } + self.push_str(&func_name.to_upper_camel_case()); + uwrite!(&mut self.src, "(ctx__ {}.Context, ", self.deps.context()); + if self.in_import { + uwrite!(&mut self.src, "(wrpc__ {}.Client, ", self.deps.wrpc()); + } + let mut params = Vec::new(); + for (i, (name, param)) in func.params.iter().enumerate() { + if let FunctionKind::Method(..) = &func.kind { + if i == 0 && sig.self_is_first_param { + params.push("self".to_string()); + continue; + } + } + let name = to_go_ident(name); + self.push_str(&name); + self.push_str(" "); + self.print_ty(param); + self.push_str(","); + + params.push(name); + } + self.push_str(")"); + params + } + + fn print_results(&mut self, results: &Results) { + self.src.push_str(" ("); + for (i, ty) in results.iter_types().enumerate() { + uwrite!(self.src, "r{i}__ "); + self.print_ty(ty); + self.src.push_str(", ") + } + self.push_str("err__ error)\n"); + } + + fn print_ty(&mut self, ty: &Type) { + match ty { + Type::Id(t) => self.print_tyid(*t), + Type::Bool => self.push_str("bool"), + Type::U8 => self.push_str("uint8"), + Type::U16 => self.push_str("uint16"), + Type::U32 => self.push_str("uint32"), + Type::U64 => self.push_str("uint64"), + Type::S8 => self.push_str("int8"), + Type::S16 => self.push_str("int16"), + Type::S32 => self.push_str("int32"), + Type::S64 => self.push_str("int64"), + Type::F32 => self.push_str("float32"), + Type::F64 => self.push_str("float64"), + Type::Char => self.push_str("rune"), + Type::String => self.push_str("string"), + } + } + + fn print_optional_ty(&mut self, ty: Option<&Type>) { + match ty { + Some(ty) => self.print_ty(ty), + None => self.push_str("wrpc.Void"), + } + } + + pub fn type_path(&mut self, id: TypeId) -> String { + self.type_path_with_name( + id, + //if owned { + self.result_name(id), //} else { + //self.param_name(id) + //}, + ) + } + + fn type_path_with_name(&mut self, id: TypeId, name: String) -> String { + if let TypeOwner::Interface(id) = self.resolve.types[id].owner { + if let Some(path) = self.path_to_interface(id) { + return format!("{path}.{name}"); + } + } + name + } + + fn print_tyid(&mut self, id: TypeId) { + let ty = &self.resolve.types[id]; + if ty.name.is_some() { + let name = self.type_path(id); + self.push_str(&name); + return; + } + + match &ty.kind { + TypeDefKind::List(t) => self.print_list(t), + + TypeDefKind::Option(t) => { + self.push_str("*"); + self.print_ty(t); + } + + TypeDefKind::Result(r) => { + self.push_str("wrpc.Result["); + self.print_optional_ty(r.ok.as_ref()); + self.push_str(","); + self.print_optional_ty(r.err.as_ref()); + self.push_str("]"); + } + + TypeDefKind::Variant(_) => panic!("unsupported anonymous variant"), + + // Tuple-like records are mapped directly to Rust tuples of + // types. Note the trailing comma after each member to + // appropriately handle 1-tuples. + TypeDefKind::Tuple(t) => { + self.push_str("wrpc.Tuple"); + uwrite!(self.src, "{}[", t.types.len()); + for ty in t.types.iter() { + self.print_ty(ty); + self.push_str(","); + } + self.push_str("]"); + } + TypeDefKind::Resource => { + panic!("unsupported anonymous type reference: resource") + } + TypeDefKind::Record(_) => { + panic!("unsupported anonymous type reference: record") + } + TypeDefKind::Flags(_) => { + panic!("unsupported anonymous type reference: flags") + } + TypeDefKind::Enum(_) => { + panic!("unsupported anonymous type reference: enum") + } + TypeDefKind::Future(ty) => { + self.push_str("wrpc.Receiver["); + self.print_optional_ty(ty.as_ref()); + self.push_str("]"); + } + TypeDefKind::Stream(stream) => { + self.push_str("wrpc.Receiver[[]"); + self.print_optional_ty(stream.element.as_ref()); + self.push_str("]"); + } + + TypeDefKind::Handle(Handle::Own(ty)) => { + self.print_ty(&Type::Id(*ty)); + } + + TypeDefKind::Handle(Handle::Borrow(ty)) => { + if self.is_exported_resource(*ty) { + let camel = self.resolve.types[*ty] + .name + .as_deref() + .unwrap() + .to_upper_camel_case(); + let name = self.type_path_with_name(*ty, format!("{camel}Borrow")); + self.push_str(&name); + } else { + let ty = &Type::Id(*ty); + self.print_ty(ty); + } + } + + TypeDefKind::Type(t) => self.print_ty(t), + + TypeDefKind::Unknown => unreachable!(), + } + } + + fn print_noop_subscribe(&mut self, name: &str) { + uwrite!(self.src, "impl wrpc::Subscribe for {name} {{}}",); + } + + fn print_list(&mut self, ty: &Type) { + self.push_str("[]"); + self.print_ty(ty); + } + + fn print_struct_encode(&mut self, name: &str, ty: &Record) { + self.push_str("impl"); + uwrite!(self.src, " wrpc::Encode for "); + self.push_str(name); + self.push_str(" {\n"); + uwriteln!(self.src, "async fn encode(self, mut payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{"); + if !ty.fields.is_empty() { + uwriteln!(self.src, r#"let span = slog::trace_span!("encode");"#); + self.push_str("let _enter = span.enter();\n"); + self.push_str("let "); + self.push_str(name.trim_start_matches('&')); + self.push_str("{\n"); + for Field { name, .. } in ty.fields.iter() { + let name_rust = to_go_ident(name); + uwriteln!(self.src, "{name_rust} f_{name_rust},") + } + self.push_str("} = self;\n"); + for Field { name, .. } in ty.fields.iter() { + let name_rust = to_go_ident(name); + uwriteln!( + self.src, + r#"let f_{name_rust} = f_{name_rust}.encode(&mut payload).await.context("failed to encode `{name}`")?;"#, + ) + } + self.push_str("if false"); + for Field { name, .. } in ty.fields.iter() { + uwrite!(self.src, r#" || f_{}.is_some()"#, to_go_ident(name)) + } + self.push_str("{\n"); + uwrite!(self.src, "return Ok(Some(wrpc::AsyncValue::Record(vec!["); + for Field { name, .. } in ty.fields.iter() { + uwrite!(self.src, r#"f_{},"#, to_go_ident(name)) + } + self.push_str("\n])))}\n"); + } + self.push_str("Ok(None)\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_struct_subscribe(&mut self, name: &str, ty: &Record) { + self.push_str("impl"); + uwrite!(self.src, " wrpc::Subscribe for "); + self.push_str(name); + self.push_str(" {\n"); + uwriteln!(self.src, "async fn subscribe(subscriber: &T, subject: T::Subject) -> Result>, T::SubscribeError> {{"); + if !ty.fields.is_empty() { + uwriteln!(self.src, r#"let span = slog::trace_span!("subscribe");"#); + self.push_str("let _enter = span.enter();\n"); + for (i, Field { name, ty, .. }) in ty.fields.iter().enumerate() { + let name_rust = to_go_ident(name); + uwrite!(self.src, r#"let f_{name_rust} = <"#,); + self.print_ty(ty); + uwrite!( + self.src, + r#">::subscribe(subscriber, subject.child(Some({i}))).await?;"#, + ) + } + self.push_str("if false"); + for Field { name, .. } in ty.fields.iter() { + uwrite!(self.src, r#" || f_{}.is_some()"#, to_go_ident(name)) + } + self.push_str("{\n"); + uwrite!( + self.src, + "return Ok(Some(wrpc::AsyncSubscription::Record(vec![" + ); + for Field { name, .. } in ty.fields.iter() { + uwrite!(self.src, r#"f_{},"#, to_go_ident(name)) + } + self.push_str("\n])))}\n"); + } + self.push_str("Ok(None)\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_struct_receive(&mut self, name: &str, ty: &Record) { + uwriteln!(self.src, ""); + self.push_str("impl<'wrpc_receive_>"); + uwrite!(self.src, " wrpc::Receive<'wrpc_receive_> for "); + self.push_str(name); + self.push_str(" {"); + uwriteln!( + self.src, + r#" + #[allow(unused_mut)] + async fn receive( + payload: impl bytes::Buf + Send + 'wrpc_receive_, + mut rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> {{"# + ); + if !ty.fields.is_empty() { + uwriteln!(self.src, r#"let span = slog::trace_span!("receive");"#); + self.push_str("let _enter = span.enter();\n"); + uwriteln!( + self.src, + "let mut sub = sub.map(wrpc::AsyncSubscription::try_unwrap_record).transpose()?;" + ); + for (i, Field { name, .. }) in ty.fields.iter().enumerate() { + let name_rust = to_go_ident(name); + uwriteln!( + self.src, + r#"let (f_{name_rust}, payload) = wrpc::Receive::receive( + payload, + &mut rx, + sub.as_mut().and_then(|sub| sub.get_mut({i})).and_then(Option::take) + ) + .await + .context("failed to receive `{name}`")?;"#, + ) + } + } + self.push_str("Ok((Self {\n"); + for Field { name, .. } in ty.fields.iter() { + let name_rust = to_go_ident(name); + uwrite!(self.src, r#"{name_rust}: f_{name_rust},"#) + } + self.push_str("\n}, payload))\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_variant_encode<'a>( + &mut self, + name: &str, + cases: impl IntoIterator)>, + ) { + uwriteln!(self.src, ""); + self.push_str("impl"); + uwrite!(self.src, " wrpc::Encode for "); + self.push_str(name); + self.push_str(" {\n"); + uwriteln!(self.src, "#[allow(unused_mut)]"); + uwriteln!(self.src, "async fn encode(self, mut payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{"); + uwriteln!(self.src, r#"let span = slog::trace_span!("encode");"#); + self.push_str("let _enter = span.enter();\n"); + self.push_str("match self {\n"); + for (i, (case_name, _, payload)) in cases.into_iter().enumerate() { + self.push_str(name.trim_start_matches('&')); + self.push_str("::"); + self.push_str(&case_name); + if payload.is_some() { + self.push_str("(nested)"); + } + self.push_str(" => {\n"); + uwriteln!(self.src, "wrpc::encode_discriminant(&mut payload, {i})?;"); + if payload.is_some() { + self.push_str("nested.encode(payload).await\n"); + } else { + self.push_str("Ok(None)\n"); + } + self.push_str("}\n"); + } + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_variant_subscribe<'a>( + &mut self, + name: &str, + cases: impl IntoIterator)> + Clone, + ) { + self.push_str("impl"); + uwrite!(self.src, " wrpc::Subscribe for "); + self.push_str(name); + self.push_str(" {\n"); + uwriteln!(self.src, "async fn subscribe(subscriber: &T, subject: T::Subject) -> Result>, T::SubscribeError> {{"); + uwriteln!(self.src, r#"let span = slog::trace_span!("subscribe");"#); + self.push_str("let _enter = span.enter();\n"); + for (i, (_, _, payload)) in cases.clone().into_iter().enumerate() { + if let Some(ty) = payload { + uwrite!(self.src, r#"let c_{i} = <"#,); + self.print_ty(ty); + uwrite!( + self.src, + r#">::subscribe(subscriber, subject.child(Some({i}))).await?;"#, + ) + } + } + self.push_str("if false"); + for (i, (_, _, payload)) in cases.clone().into_iter().enumerate() { + if payload.is_some() { + uwrite!(self.src, r#" || c_{i}.is_some()"#) + } + } + self.push_str("{\n"); + uwrite!( + self.src, + "return Ok(Some(wrpc::AsyncSubscription::Variant(vec![" + ); + for (i, (_, _, payload)) in cases.into_iter().enumerate() { + if payload.is_some() { + uwrite!(self.src, r#"c_{i},"#) + } else { + self.push_str("None,"); + } + } + self.push_str("\n])))}\n"); + self.push_str("Ok(None)\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_variant_receive<'a>( + &mut self, + name: &str, + cases: impl IntoIterator)>, + ) { + uwriteln!(self.src, ""); + self.push_str("impl<'wrpc_receive_>"); + uwrite!(self.src, " wrpc::Receive<'wrpc_receive_> for "); + self.push_str(name); + uwriteln!( + self.src, + r#" {{ + async fn receive( + payload: impl bytes::Buf + Send + 'wrpc_receive_, + mut rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> + {{ + let span = slog::trace_span!("receive"); + let _enter = span.enter(); + let (disc, payload) = wrpc::receive_discriminant( + payload, + &mut rx, + ) + .await + .context("failed to receive discriminant")?; + match disc {{"#, + ); + for (i, (case_name, _, payload)) in cases.into_iter().enumerate() { + uwriteln!(self.src, "{i} => "); + if payload.is_some() { + uwriteln!( + self.src, + r#"{{ + let sub = sub + .map(|sub| {{ + let mut sub = sub.try_unwrap_variant()?; + anyhow::Ok(sub.get_mut({i}).and_then(Option::take)) + }}) + .transpose()? + .flatten(); + let (nested, payload) = wrpc::Receive::receive(payload, rx, sub) + .await + .context("failed to receive nested variant value")?; + Ok((Self::{case_name}(nested), payload)) + }}"# + ); + } else { + uwriteln!(self.src, "Ok((Self::{case_name}, payload)),") + } + } + uwriteln!( + self.src, + r#"_ => anyhow::bail!("unknown variant discriminant `{{disc}}`")"# + ); + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_enum_encode(&mut self, name: &str, cases: &[EnumCase]) { + uwriteln!(self.src, ""); + self.push_str("impl"); + uwrite!(self.src, " wrpc::Encode for "); + self.push_str(name); + self.push_str(" {\n"); + uwriteln!(self.src, "#[allow(unused_mut)]"); + uwriteln!(self.src, "async fn encode(self, mut payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{"); + uwriteln!(self.src, r#"let span = slog::trace_span!("encode");"#); + self.push_str("let _enter = span.enter();\n"); + self.push_str("match self {\n"); + for (i, case) in cases.iter().enumerate() { + let case = case.name.to_upper_camel_case(); + self.push_str(name.trim_start_matches('&')); + self.push_str("::"); + self.push_str(&case); + self.push_str(" => {\n"); + uwriteln!(self.src, "wrpc::encode_discriminant(&mut payload, {i})?;"); + self.push_str("Ok(None)\n"); + self.push_str("}\n"); + } + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_enum_receive(&mut self, name: &str, cases: &[EnumCase]) { + uwriteln!(self.src, ""); + self.push_str("impl<'wrpc_receive_>"); + uwrite!(self.src, " wrpc::Receive<'wrpc_receive_> for "); + self.push_str(name); + uwriteln!( + self.src, + r#" {{ + async fn receive( + payload: impl bytes::Buf + Send + 'wrpc_receive_, + mut rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> + {{ + let span = slog::trace_span!("receive"); + let _enter = span.enter(); + let (disc, payload) = wrpc::receive_discriminant( + payload, + &mut rx, + ) + .await + .context("failed to receive discriminant")?; + match disc {{"#, + ); + for (i, case) in cases.iter().enumerate() { + let case = case.name.to_upper_camel_case(); + uwriteln!(self.src, "{i} => Ok((Self::{case}, payload)),"); + } + uwriteln!( + self.src, + r#"_ => anyhow::bail!("unknown enum discriminant `{{disc}}`")"# + ); + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn int_repr(&mut self, repr: Int) { + self.push_str(int_repr(repr)); + } + + fn modes_of(&self, ty: TypeId) -> Vec { + let info = self.info(ty); + // If this type isn't actually used, no need to generate it. + if !info.owned && !info.borrowed { + return vec![]; + } + vec![self.result_name(ty)] + } + + fn print_typedef_record(&mut self, id: TypeId, record: &Record, docs: &Docs) { + let info = self.info(id); + // We use a BTree set to make sure we don't have any duplicates and we have a stable order + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("pub struct {}", name)); + self.push_str(" {\n"); + for Field { name, ty, docs } in record.fields.iter() { + self.godoc(docs); + self.push_str(&to_go_ident(name)); + self.push_str(": "); + self.print_ty(ty); + self.push_str(",\n"); + } + self.push_str("}\n"); + + self.print_struct_encode(&name, record); + self.print_struct_encode(&format!("&{name}"), record); + + self.print_struct_subscribe(&name, record); + self.print_struct_receive(&name, record); + + self.push_str("impl"); + self.push_str(" ::core::fmt::Debug for "); + self.push_str(&name); + self.push_str(" {\n"); + self.push_str( + "fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {\n", + ); + self.push_str(&format!("f.debug_struct(\"{}\")", name)); + for field in record.fields.iter() { + self.push_str(&format!( + ".field(\"{}\", &self.{})", + field.name, + to_go_ident(&field.name) + )); + } + self.push_str(".finish()\n"); + self.push_str("}\n"); + self.push_str("}\n"); + + if info.error { + self.push_str("impl"); + self.push_str(" ::core::fmt::Display for "); + self.push_str(&name); + self.push_str(" {\n"); + self.push_str( + "fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {\n", + ); + self.push_str("write!(f, \"{:?}\", self)\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + } + } + + fn print_typedef_variant(&mut self, id: TypeId, variant: &Variant, docs: &Docs) + where + Self: Sized, + { + self.print_rust_enum( + id, + variant + .cases + .iter() + .map(|c| (c.name.to_upper_camel_case(), &c.docs, c.ty.as_ref())), + docs, + ); + } + + fn print_rust_enum<'b>( + &mut self, + id: TypeId, + cases: impl IntoIterator)> + Clone, + docs: &Docs, + ) where + Self: Sized, + { + let info = self.info(id); + // We use a BTree set to make sure we don't have any duplicates and have a stable order + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("pub enum {name}")); + self.push_str(" {\n"); + for (case_name, docs, payload) in cases.clone() { + self.godoc(docs); + self.push_str(&case_name); + if let Some(ty) = payload { + self.push_str("("); + self.print_ty(ty); + self.push_str(")") + } + self.push_str(",\n"); + } + self.push_str("}\n"); + + self.print_rust_enum_debug( + &name, + cases + .clone() + .into_iter() + .map(|(name, _docs, ty)| (name, ty)), + ); + + if info.error { + self.push_str("impl"); + self.push_str(" ::core::fmt::Display for "); + self.push_str(&name); + self.push_str(" {\n"); + self.push_str( + "fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {\n", + ); + self.push_str("write!(f, \"{:?}\", self)\n"); + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("\n"); + } + + self.print_variant_encode(&name, cases.clone()); + self.print_variant_encode(&format!("&{name}"), cases.clone()); + + self.print_variant_subscribe(&name, cases.clone()); + + self.print_variant_receive(&name, cases.clone()); + } + } + + fn print_rust_enum_debug<'b>( + &mut self, + name: &str, + cases: impl IntoIterator)>, + ) where + Self: Sized, + { + self.push_str("impl"); + self.push_str(" ::core::fmt::Debug for "); + self.push_str(name); + self.push_str(" {\n"); + self.push_str( + "fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {\n", + ); + self.push_str("match self {\n"); + for (case_name, payload) in cases { + self.push_str(name); + self.push_str("::"); + self.push_str(&case_name); + if payload.is_some() { + self.push_str("(e)"); + } + self.push_str(" => {\n"); + self.push_str(&format!("f.debug_tuple(\"{}::{}\")", name, case_name)); + if payload.is_some() { + self.push_str(".field(e)"); + } + self.push_str(".finish()\n"); + self.push_str("}\n"); + } + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("}\n"); + } + + fn print_typedef_option(&mut self, id: TypeId, payload: &Type, docs: &Docs) { + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("type {}", name)); + self.push_str("= Option<"); + self.print_ty(payload); + self.push_str(">;\n"); + } + } + + fn print_typedef_result(&mut self, id: TypeId, result: &Result_, docs: &Docs) { + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("type {}", name)); + self.push_str("= Result<"); + self.print_optional_ty(result.ok.as_ref()); + self.push_str(","); + self.print_optional_ty(result.err.as_ref()); + self.push_str(">;\n"); + } + } + + fn print_typedef_enum( + &mut self, + id: TypeId, + name: &str, + enum_: &Enum, + docs: &Docs, + attrs: &[String], + case_attr: Box String>, + ) where + Self: Sized, + { + let info = self.info(id); + + let name = to_upper_camel_case(name); + self.godoc(docs); + for attr in attrs { + self.push_str(&format!("{}\n", attr)); + } + self.push_str("#[repr("); + self.int_repr(enum_.tag()); + self.push_str(")]\n"); + // We use a BTree set to make sure we don't have any duplicates and a stable order + self.push_str(&format!("pub enum {name} {{\n")); + for case in enum_.cases.iter() { + self.godoc(&case.docs); + self.push_str(&case_attr(case)); + self.push_str(&case.name.to_upper_camel_case()); + self.push_str(",\n"); + } + self.push_str("}\n"); + + // Auto-synthesize an implementation of the standard `Error` trait for + // error-looking types based on their name. + if info.error { + self.push_str("impl "); + self.push_str(&name); + self.push_str("{\n"); + + self.push_str("pub fn name(&self) -> &'static str {\n"); + self.push_str("match self {\n"); + for case in enum_.cases.iter() { + self.push_str(&name); + self.push_str("::"); + self.push_str(&case.name.to_upper_camel_case()); + self.push_str(" => \""); + self.push_str(case.name.as_str()); + self.push_str("\",\n"); + } + self.push_str("}\n"); + self.push_str("}\n"); + + self.push_str("pub fn message(&self) -> &'static str {\n"); + self.push_str("match self {\n"); + for case in enum_.cases.iter() { + self.push_str(&name); + self.push_str("::"); + self.push_str(&case.name.to_upper_camel_case()); + self.push_str(" => \""); + if let Some(contents) = &case.docs.contents { + self.push_str(contents.trim()); + } + self.push_str("\",\n"); + } + self.push_str("}\n"); + self.push_str("}\n"); + + self.push_str("}\n"); + + self.push_str("impl ::core::fmt::Debug for "); + self.push_str(&name); + self.push_str( + "{\nfn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {\n", + ); + self.push_str("f.debug_struct(\""); + self.push_str(&name); + self.push_str("\")\n"); + self.push_str(".field(\"code\", &(*self as i32))\n"); + self.push_str(".field(\"name\", &self.name())\n"); + self.push_str(".field(\"message\", &self.message())\n"); + self.push_str(".finish()\n"); + self.push_str("}\n"); + self.push_str("}\n"); + + self.push_str("impl ::core::fmt::Display for "); + self.push_str(&name); + self.push_str( + "{\nfn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {\n", + ); + self.push_str("write!(f, \"{} (error {})\", self.name(), *self as i32)\n"); + self.push_str("}\n"); + self.push_str("}\n"); + self.push_str("\n"); + self.push_str("impl std::error::Error for "); + self.push_str(&name); + self.push_str(" {}\n"); + } else { + self.print_rust_enum_debug( + &name, + enum_ + .cases + .iter() + .map(|c| (c.name.to_upper_camel_case(), None)), + ) + } + } + + fn print_typedef_alias(&mut self, id: TypeId, ty: &Type, docs: &Docs) { + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("type {name}")); + self.push_str(" = "); + self.print_ty(ty); + self.push_str(";\n"); + } + + if self.is_exported_resource(id) { + self.godoc(docs); + let name = self.resolve.types[id].name.as_ref().unwrap(); + let name = name.to_upper_camel_case(); + self.push_str(&format!("type {name}Borrow")); + self.push_str(" = "); + self.print_ty(ty); + self.push_str("Borrow"); + self.push_str(";\n"); + } + } + + fn param_name(&self, ty: TypeId) -> String { + let info = self.info(ty); + let name = to_upper_camel_case(self.resolve.types[ty].name.as_ref().unwrap()); + if self.uses_two_names(&info) { + format!("{}Param", name) + } else { + name + } + } + + fn result_name(&self, ty: TypeId) -> String { + let info = self.info(ty); + let name = to_upper_camel_case(self.resolve.types[ty].name.as_ref().unwrap()); + if self.uses_two_names(&info) { + format!("{}Result", name) + } else { + name + } + } + + fn uses_two_names(&self, info: &TypeInfo) -> bool { + info.borrowed + && info.owned + // ... and they have a list ... + && info.has_list + // ... and if there's NOT an `own` handle since those are always + // done by ownership. + && !info.has_own_handle + } + + fn path_to_interface(&mut self, interface: InterfaceId) -> Option { + let InterfaceName { + import_name, + import_path, + } = &self.gen.interface_names[&interface]; + if let Identifier::Interface(cur, _) = self.identifier { + if cur == interface { + return None; + } + } + Some(self.deps.import(import_name.clone(), import_path.clone())) + } + + pub fn is_exported_resource(&self, ty: TypeId) -> bool { + let ty = dealias(self.resolve, ty); + let ty = &self.resolve.types[ty]; + match &ty.kind { + TypeDefKind::Resource => {} + _ => return false, + } + + match ty.owner { + // Worlds cannot export types of any kind as of this writing. + TypeOwner::World(_) => false, + + // Interfaces are "stateful" currently where whatever we last saw + // them as dictates whether it's exported or not. + TypeOwner::Interface(i) => !self.gen.interface_last_seen_as_import[&i], + + // Shouldn't be the case for resources + TypeOwner::None => unreachable!(), + } + } + + fn push_str(&mut self, s: &str) { + self.src.push_str(s); + } + + fn info(&self, ty: TypeId) -> TypeInfo { + self.gen.types.get(ty) + } +} + +impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { + fn resolve(&self) -> &'a Resolve { + self.resolve + } + + fn type_record(&mut self, id: TypeId, _name: &str, record: &Record, docs: &Docs) { + self.print_typedef_record(id, record, docs); + } + + fn type_resource(&mut self, _id: TypeId, name: &str, docs: &Docs) { + self.godoc(docs); + let camel = to_upper_camel_case(name); + + if self.in_import { + uwriteln!( + self.src, + r#" +#[derive(Debug)] +pub struct {camel}(String); + + +impl wrpc::Encode for {camel} {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + self.0.encode(payload).await + }} +}} + + +impl wrpc::Encode for &{camel} {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + self.0.as_str().encode(payload).await + }} +}} + +impl wrpc::Subscribe for {camel} {{}} + +impl wrpc::Subscribe for &{camel} {{}} + + +impl<'a> wrpc::Receive<'a> for {camel} {{ + async fn receive( + payload: impl bytes::Buf, + rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> + {{ + let span = slog::trace_span!("receive"); + let _enter = span.enter(); + let (subject, payload) = wrpc::Receive::receive(payload, rx, sub).await?; + Ok((Self(subject), payload)) + }} +}} +"# + ); + } else { + uwriteln!( + self.src, + r#" +#[derive(Debug)] +#[repr(transparent)] +pub struct {camel}; + +impl {camel} {{ + /// Creates a new resource from the specified representation. + pub fn new() -> Self {{ Self {{ }} }} +}} + +/// A borrowed version of [`{camel}`] which represents a borrowed value +#[derive(Debug)] +#[repr(transparent)] +pub struct {camel}Borrow; + + +impl wrpc::Encode for {camel}Borrow {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + anyhow::bail!("exported resources not supported yet") + }} +}} + + +impl wrpc::Encode for &{camel}Borrow {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + anyhow::bail!("exported resources not supported yet") + }} +}} + +impl wrpc::Subscribe for {camel}Borrow {{}} + +impl wrpc::Subscribe for &{camel}Borrow {{}} + + +impl<'a> wrpc::Receive<'a> for {camel}Borrow {{ + async fn receive( + payload: impl bytes::Buf, + rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> + {{ + let span = slog::trace_span!("receive"); + let _enter = span.enter(); + anyhow::bail!("exported resources not supported yet") + }} +}} + + +impl wrpc::Encode for {camel} {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + anyhow::bail!("exported resources not supported yet") + }} +}} + + +impl wrpc::Encode for &{camel} {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + anyhow::bail!("exported resources not supported yet") + }} +}} + +impl wrpc::Subscribe for {camel} {{}} + +impl wrpc::Subscribe for &{camel} {{}} + + +impl<'a> wrpc::Receive<'a> for {camel} {{ + async fn receive( + payload: impl bytes::Buf, + rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> + {{ + let span = slog::trace_span!("receive"); + let _enter = span.enter(); + anyhow::bail!("exported resources not supported yet") + }} +}} +"#, + ); + } + } + + fn type_tuple(&mut self, id: TypeId, _name: &str, tuple: &Tuple, docs: &Docs) { + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("type {}", name)); + self.push_str(" = ("); + for ty in tuple.types.iter() { + self.print_ty(ty); + self.push_str(","); + } + self.push_str(");\n"); + } + } + + fn type_flags(&mut self, _id: TypeId, name: &str, flags: &Flags, docs: &Docs) { + self.src.push_str(&format!("bitflags::bitflags! {{\n",)); + self.godoc(docs); + let repr = RustFlagsRepr::new(flags); + let name = to_upper_camel_case(name); + self.src.push_str(&format!( + "#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]\npub struct {name}: {repr} {{\n", + )); + for (i, flag) in flags.flags.iter().enumerate() { + self.godoc(&flag.docs); + self.src.push_str(&format!( + "const {} = 1 << {};\n", + flag.name.to_shouty_snake_case(), + i, + )); + } + self.src.push_str("}\n"); + self.src.push_str("}\n"); + + self.print_noop_subscribe(&name); + self.print_noop_subscribe(&format!("&{name}")); + + uwriteln!( + self.src, + r#" + impl wrpc::Encode for {name} {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + self.bits().encode(payload).await + }} + }} + + impl wrpc::Encode for &{name} {{ + async fn encode(self, payload: &mut (impl bytes::BufMut + Send)) -> anyhow::Result> {{ + let span = slog::trace_span!("encode"); + let _enter = span.enter(); + self.bits().encode(payload).await + }} + }} + + impl<'a> wrpc::Receive<'a> for {name} {{ + async fn receive( + payload: impl bytes::Buf, + rx: &mut (impl futures::Stream> + Send + Sync + Unpin), + sub: Option>, + ) -> anyhow::Result<(Self, Box)> + where + T: futures::Stream> + {{ + let span = slog::trace_span!("receive"); + let _enter = span.enter(); + + let (bits, payload) = ::Bits::receive(payload, rx, sub).await.context("failed to receive bits")?; + let ret = Self::from_bits(bits).context("failed to convert bits into flags")?; + Ok((ret, payload)) + }} + }}"#, + ); + } + + fn type_variant(&mut self, id: TypeId, _name: &str, variant: &Variant, docs: &Docs) { + self.print_typedef_variant(id, variant, docs); + } + + fn type_option(&mut self, id: TypeId, _name: &str, payload: &Type, docs: &Docs) { + self.print_typedef_option(id, payload, docs); + } + + fn type_result(&mut self, id: TypeId, _name: &str, result: &Result_, docs: &Docs) { + self.print_typedef_result(id, result, docs); + } + + fn type_enum(&mut self, id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { + self.print_typedef_enum(id, name, enum_, docs, &[], Box::new(|_| String::new())); + + let name = to_upper_camel_case(name); + + self.print_enum_encode(&name, &enum_.cases); + self.print_enum_encode(&format!("&{name}"), &enum_.cases); + + self.print_noop_subscribe(&name); + self.print_noop_subscribe(&format!("&{name}")); + + self.print_enum_receive(&name, &enum_.cases); + } + + fn type_alias(&mut self, id: TypeId, _name: &str, ty: &Type, docs: &Docs) { + self.print_typedef_alias(id, ty, docs); + } + + fn type_list(&mut self, id: TypeId, _name: &str, ty: &Type, docs: &Docs) { + for name in self.modes_of(id) { + self.godoc(docs); + self.push_str(&format!("type {}", name)); + self.push_str(" = "); + self.print_list(ty); + self.push_str(";\n"); + } + } + + fn type_builtin(&mut self, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { + self.godoc(docs); + self.src + .push_str(&format!("type {}", name.to_upper_camel_case())); + self.src.push_str(" = "); + self.print_ty(ty); + self.src.push_str(";\n"); + } +} diff --git a/crates/wit-bindgen-go/src/lib.rs b/crates/wit-bindgen-go/src/lib.rs new file mode 100644 index 000000000..3d5e5c1d8 --- /dev/null +++ b/crates/wit-bindgen-go/src/lib.rs @@ -0,0 +1,679 @@ +use crate::interface::InterfaceGenerator; +use anyhow::Result; +use core::fmt::Display; +use heck::*; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fmt::{self, Write as _}; +use std::io::{Read, Write}; +use std::mem; +use std::process::{Command, Stdio}; +use wit_bindgen_core::{ + uwrite, uwriteln, wit_parser::*, Files, InterfaceGenerator as _, Source, Types, WorldGenerator, +}; + +mod interface; + +#[derive(Clone)] +struct InterfaceName { + /// The string import name for this interface. + import_name: String, + + /// The string import path for this interface. + import_path: String, +} + +#[derive(Default)] +struct GoWrpc { + types: Types, + src: Source, + opts: Opts, + import_modules: Vec<(String, Vec)>, + export_modules: Vec<(String, Vec)>, + skip: HashSet, + interface_names: HashMap, + /// Each imported and exported interface is stored in this map. Value indicates if last use was import. + interface_last_seen_as_import: HashMap, + import_funcs_called: bool, + world: Option, + + export_paths: Vec, + deps: Deps, +} + +#[derive(Default)] +struct Deps { + map: BTreeMap, + package: String, +} + +impl Deps { + fn binary(&mut self) -> &'static str { + self.map + .insert("binary".to_string(), "encoding/binary".to_string()); + "binary" + } + fn bytes(&mut self) -> &'static str { + self.map.insert("bytes".to_string(), "bytes".to_string()); + "bytes" + } + + fn bufio(&mut self) -> &'static str { + self.map.insert("bufio".to_string(), "bufio".to_string()); + "bufio" + } + + fn context(&mut self) -> &'static str { + self.map + .insert("context".to_string(), "context".to_string()); + "context" + } + + fn errors(&mut self) -> &'static str { + self.map.insert("errors".to_string(), "errors".to_string()); + "errors" + } + + fn fmt(&mut self) -> &'static str { + self.map.insert("fmt".to_string(), "fmt".to_string()); + "fmt" + } + + fn math(&mut self) -> &'static str { + self.map.insert("math".to_string(), "math".to_string()); + "math" + } + + fn slog(&mut self) -> &'static str { + self.map.insert("slog".to_string(), "log/slog".to_string()); + "slog" + } + + fn sync(&mut self) -> &'static str { + self.map.insert("sync".to_string(), "sync".to_string()); + "sync" + } + + fn wrpc(&mut self) -> &'static str { + self.map + .insert("wrpc".to_string(), "github.com/wrpc/wrpc/go".to_string()); + "wrpc" + } + + fn import<'a>(&mut self, name: String, path: String) -> String { + if let Some(old) = self.map.insert(name.clone(), path.clone()) { + if old != path { + panic!("dependency path mismatch, import of `{name}` refers to both `{old}` and `{path}`") + } + } + name + } +} + +impl Display for Deps { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "import (")?; + for (k, v) in &self.map { + writeln!(f, r#"{k} "{v}""#)? + } + writeln!(f, ")") + } +} + +fn generated_preamble() -> String { + format!( + "// Generated by `wit-bindgen-wrpc-go` {}. DO NOT EDIT!\n", + env!("CARGO_PKG_VERSION") + ) +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::Args))] +pub struct Opts { + /// Whether or not `gofmt` is executed to format generated code. + #[cfg_attr(feature = "clap", arg(long, default_value_t = true))] + pub gofmt: bool, + + /// Go package path containing the generated bindings + #[cfg_attr(feature = "clap", arg(long, default_value = ""))] + pub package: String, +} + +impl Default for Opts { + fn default() -> Self { + Self { + gofmt: true, + package: String::new(), + } + } +} + +impl Opts { + pub fn build(self) -> Box { + let mut r = GoWrpc::new(); + r.opts = self; + r.deps.package = r.opts.package.clone(); + Box::new(r) + } +} + +impl GoWrpc { + fn new() -> GoWrpc { + GoWrpc::default() + } + + fn interface<'a>( + &'a mut self, + identifier: Identifier<'a>, + resolve: &'a Resolve, + in_import: bool, + ) -> InterfaceGenerator<'a> { + let package = self.opts.package.clone(); + InterfaceGenerator { + identifier, + src: Source::default(), + in_import, + gen: self, + resolve, + deps: Deps { + map: BTreeMap::default(), + package, + }, + } + } + + fn emit_modules(&mut self, modules: Vec<(String, Vec)>, files: &mut Files) { + for (mut module, path) in modules { + if self.opts.gofmt { + gofmt(&mut module); + } + let file = format!("{}/bindings.wrpc.go", path.join("/")); + files.push(&file, generated_preamble().as_bytes()); + files.push(&file, module.as_bytes()); + } + } + + fn name_interface( + &mut self, + resolve: &Resolve, + id: InterfaceId, + name: &WorldKey, + is_export: bool, + ) { + let path = compute_module_path(name, resolve, is_export); + let import_name = path.join("__"); + let import_path = if self.opts.package != "" { + format!("{}/{}", self.opts.package, path.join("/")) + } else { + path.join("/") + }; + self.interface_names.insert( + id, + InterfaceName { + import_name, + import_path, + }, + ); + } + + /// Generates imports and a `Serve` function for the world + fn finish_serve_function(&mut self) { + let mut traits: Vec = self + .export_paths + .iter() + .map(|path| { + if path.is_empty() { + "Handler".to_string() + } else { + format!("{path}.Handler") + } + }) + .collect(); + let bound = match traits.len() { + 0 => return, + 1 => traits.pop().unwrap(), + _ => traits.join(" "), + }; + uwriteln!( + self.src, + r#" +func Serve(c {wrpc}.Client, h interface{{ {bound} }}) (stop func() error, err error) {{"#, + wrpc = self.deps.wrpc() + ); + uwriteln!( + self.src, + "stops := make([]func() error, 0, {})", + self.export_paths.len() + ); + self.src.push_str( + r#"stop = func() error { + for _, stop := range stops { + if err := stop(); err != nil { + return err + } + } + return nil + } +"#, + ); + + for (i, path) in self.export_paths.iter().enumerate() { + uwrite!(self.src, "stop{i}, err := "); + if !path.is_empty() { + self.src.push_str(path); + self.src.push_str("."); + } + self.src.push_str("ServeInterface(c, h)\n"); + self.src.push_str("if err != nil { return }\n"); + uwriteln!(self.src, "stops = append(stops, stop{i})"); + } + self.src.push_str("stop = func() error {\n"); + for (i, _) in self.export_paths.iter().enumerate() { + uwriteln!(self.src, "if err := stop{i}(); err != nil {{ return err }}"); + } + self.src.push_str("return nil\n"); + self.src.push_str("}\n"); + + self.src.push_str("return\n"); + self.src.push_str("}\n"); + } +} + +/// If the package `id` is the only package with its namespace/name combo +/// then pass through the name unmodified. If, however, there are multiple +/// versions of this package then the package module is going to get version +/// information. +fn name_package_module(resolve: &Resolve, id: PackageId) -> String { + let pkg = &resolve.packages[id]; + let versions_with_same_name = resolve + .packages + .iter() + .filter_map(|(_, p)| { + if p.name.namespace == pkg.name.namespace && p.name.name == pkg.name.name { + Some(&p.name.version) + } else { + None + } + }) + .collect::>(); + let base = pkg.name.name.to_snake_case(); + if versions_with_same_name.len() == 1 { + return base; + } + + let version = match &pkg.name.version { + Some(version) => version, + // If this package didn't have a version then don't mangle its name + // and other packages with the same name but with versions present + // will have their names mangled. + None => return base, + }; + + // Here there's multiple packages with the same name that differ only in + // version, so the version needs to be mangled into the Rust module name + // that we're generating. This in theory could look at all of + // `versions_with_same_name` and produce a minimal diff, e.g. for 0.1.0 + // and 0.2.0 this could generate "foo1" and "foo2", but for now + // a simpler path is chosen to generate "foo0_1_0" and "foo0_2_0". + let version = version + .to_string() + .replace(['.', '-', '+'], "_") + .to_snake_case(); + format!("{base}{version}") +} + +fn gofmt(src: &mut String) { + let mut child = Command::new("gofmt") + .args(["-s"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to spawn `gofmt`"); + child + .stdin + .take() + .unwrap() + .write_all(src.as_bytes()) + .unwrap(); + src.truncate(0); + child.stdout.take().unwrap().read_to_string(src).unwrap(); + let status = child.wait().unwrap(); + assert!(status.success()); +} + +impl WorldGenerator for GoWrpc { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + self.types.analyze(resolve); + self.world = Some(world); + } + + fn import_interface( + &mut self, + resolve: &Resolve, + name: &WorldKey, + id: InterfaceId, + _files: &mut Files, + ) { + self.interface_last_seen_as_import.insert(id, true); + let mut gen = self.interface(Identifier::Interface(id, name), resolve, true); + let (snake, module_path) = gen.start_append_submodule(name); + gen.gen.name_interface(resolve, id, name, false); + gen.types(id); + + let interface = &resolve.interfaces[id]; + let name = match name { + WorldKey::Name(s) => s.to_string(), + WorldKey::Interface(..) => interface + .name + .as_ref() + .expect("interface name missing") + .to_string(), + }; + let instance = if let Some(package) = interface.package { + resolve.id_of_name(package, &name) + } else { + name + }; + gen.generate_imports(&instance, resolve.interfaces[id].functions.values()); + + gen.finish_append_submodule(&snake, module_path); + } + + fn import_funcs( + &mut self, + resolve: &Resolve, + world: WorldId, + funcs: &[(&str, &Function)], + _files: &mut Files, + ) { + self.import_funcs_called = true; + + let mut gen = self.interface(Identifier::World(world), resolve, true); + let World { + ref name, package, .. + } = resolve.worlds[world]; + let instance = if let Some(package) = package { + resolve.id_of_name(package, name) + } else { + name.to_string() + }; + gen.generate_imports(&instance, funcs.iter().map(|(_, func)| *func)); + + let src = gen.finish(); + self.src.push_str(&src); + } + + fn export_interface( + &mut self, + resolve: &Resolve, + name: &WorldKey, + id: InterfaceId, + _files: &mut Files, + ) -> Result<()> { + self.interface_last_seen_as_import.insert(id, false); + let mut gen = self.interface(Identifier::Interface(id, name), resolve, false); + let (snake, module_path) = gen.start_append_submodule(name); + gen.gen.name_interface(resolve, id, name, true); + gen.types(id); + let exports = gen.generate_exports( + Identifier::Interface(id, name), + resolve.interfaces[id].functions.values(), + ); + gen.finish_append_submodule(&snake, module_path); + if exports { + let InterfaceName { + import_name, + import_path, + } = &self.interface_names[&id]; + self.export_paths + .push(self.deps.import(import_name.clone(), import_path.clone())); + } + + Ok(()) + } + + fn export_funcs( + &mut self, + resolve: &Resolve, + world: WorldId, + funcs: &[(&str, &Function)], + _files: &mut Files, + ) -> Result<()> { + let mut gen = self.interface(Identifier::World(world), resolve, false); + let exports = gen.generate_exports(Identifier::World(world), funcs.iter().map(|f| f.1)); + let src = gen.finish(); + for (k, v) in gen.deps.map { + self.deps.import(k, v); + } + self.src.push_str(&src); + if exports { + self.export_paths.push(String::new()); + } + Ok(()) + } + + fn import_types( + &mut self, + resolve: &Resolve, + world: WorldId, + types: &[(&str, TypeId)], + _files: &mut Files, + ) { + let mut gen = self.interface(Identifier::World(world), resolve, true); + for (name, ty) in types { + gen.define_type(name, *ty); + } + let src = gen.finish(); + self.src.push_str(&src); + } + + fn finish_imports(&mut self, resolve: &Resolve, world: WorldId, files: &mut Files) { + if !self.import_funcs_called { + // We call `import_funcs` even if the world doesn't import any + // functions since one of the side effects of that method is to + // generate `struct`s for any imported resources. + self.import_funcs(resolve, world, &[], files); + } + } + + fn finish(&mut self, resolve: &Resolve, world: WorldId, files: &mut Files) -> Result<()> { + let import_modules = mem::take(&mut self.import_modules); + self.emit_modules(import_modules, files); + + let export_modules = mem::take(&mut self.export_modules); + self.emit_modules(export_modules, files); + + self.finish_serve_function(); + + let mut src = mem::take(&mut self.src); + let s = src.as_mut_string(); + + let name = &resolve.worlds[world].name; + let go_name = to_package_ident(name); + *s = format!( + r#"// {go_name} package contains wRPC bindings for `{name}` world +package {go_name} + +{} + +{s}"#, + self.deps + ); + if self.opts.gofmt { + gofmt(src.as_mut_string()) + } + let file = format!("{go_name}.wrpc.go"); + files.push(&file, generated_preamble().as_bytes()); + files.push(&file, src.as_bytes()); + Ok(()) + } +} + +fn compute_module_path(name: &WorldKey, resolve: &Resolve, is_export: bool) -> Vec { + let mut path = Vec::new(); + if is_export { + path.push("exports".to_string()); + } + match name { + WorldKey::Name(name) => { + path.push(to_package_ident(name)); + } + WorldKey::Interface(id) => { + let iface = &resolve.interfaces[*id]; + let pkg = iface.package.unwrap(); + let pkgname = resolve.packages[pkg].name.clone(); + path.push(to_package_ident(&pkgname.namespace)); + path.push(name_package_module(resolve, pkg)); + path.push(to_package_ident(iface.name.as_ref().unwrap())); + } + } + path +} + +enum Identifier<'a> { + World(WorldId), + Interface(InterfaceId, &'a WorldKey), +} + +fn group_by_resource<'a>( + funcs: impl Iterator, +) -> BTreeMap, Vec<&'a Function>> { + let mut by_resource = BTreeMap::<_, Vec<_>>::new(); + for func in funcs { + match &func.kind { + FunctionKind::Freestanding => by_resource.entry(None).or_default().push(func), + FunctionKind::Method(ty) | FunctionKind::Static(ty) | FunctionKind::Constructor(ty) => { + by_resource.entry(Some(*ty)).or_default().push(func); + } + } + } + by_resource +} + +#[derive(Default)] +struct FnSig { + use_item_name: bool, + self_arg: Option, + self_is_first_param: bool, +} + +pub fn to_package_ident(name: &str) -> String { + match name { + // Escape Go keywords. + "break" => "break_".into(), + "case" => "case_".into(), + "chan" => "chan_".into(), + "const" => "const_".into(), + "continue" => "continue_".into(), + "default" => "default_".into(), + "defer" => "defer_".into(), + "else" => "else_".into(), + "enum" => "enum_".into(), + "exports" => "exports_".into(), + "fallthrough" => "fallthrough_".into(), + "false" => "false_".into(), + "for" => "for_".into(), + "func" => "func_".into(), + "go" => "go_".into(), + "goto" => "goto_".into(), + "if" => "if_".into(), + "import" => "import_".into(), + "interface" => "interface_".into(), + "map" => "map_".into(), + "package" => "package_".into(), + "range" => "range_".into(), + "return" => "return_".into(), + "select" => "select_".into(), + "struct" => "struct_".into(), + "switch" => "switch_".into(), + "true" => "true_".into(), + "type" => "type_".into(), + "var" => "var_".into(), + s => s.to_snake_case(), + } +} + +pub fn to_go_ident(name: &str) -> String { + match name { + // Escape Go keywords. + "break" => "break_".into(), + "case" => "case_".into(), + "chan" => "chan_".into(), + "const" => "const_".into(), + "continue" => "continue_".into(), + "default" => "default_".into(), + "defer" => "defer_".into(), + "else" => "else_".into(), + "enum" => "enum_".into(), + "fallthrough" => "fallthrough_".into(), + "false" => "false_".into(), + "for" => "for_".into(), + "func" => "func_".into(), + "go" => "go_".into(), + "goto" => "goto_".into(), + "if" => "if_".into(), + "import" => "import_".into(), + "interface" => "interface_".into(), + "map" => "map_".into(), + "package" => "package_".into(), + "range" => "range_".into(), + "return" => "return_".into(), + "select" => "select_".into(), + "struct" => "struct_".into(), + "switch" => "switch_".into(), + "true" => "true_".into(), + "type" => "type_".into(), + "var" => "var_".into(), + s => s.to_lower_camel_case(), + } +} + +fn to_upper_camel_case(name: &str) -> String { + match name { + // The name "Handler" is reserved for traits generated by exported + // interfaces, so remap types defined in wit to something else. + "handler" => "Handler_".to_string(), + s => s.to_upper_camel_case(), + } +} + +fn int_repr(repr: Int) -> &'static str { + match repr { + Int::U8 => "uint8", + Int::U16 => "uint16", + Int::U32 => "uint32", + Int::U64 => "uint64", + } +} + +enum RustFlagsRepr { + U8, + U16, + U32, + U64, + U128, +} + +impl RustFlagsRepr { + fn new(f: &Flags) -> RustFlagsRepr { + match f.repr() { + FlagsRepr::U8 => RustFlagsRepr::U8, + FlagsRepr::U16 => RustFlagsRepr::U16, + FlagsRepr::U32(1) => RustFlagsRepr::U32, + FlagsRepr::U32(2) => RustFlagsRepr::U64, + FlagsRepr::U32(3 | 4) => RustFlagsRepr::U128, + FlagsRepr::U32(n) => panic!("unsupported number of flags: {}", n * 32), + } + } +} + +impl fmt::Display for RustFlagsRepr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RustFlagsRepr::U8 => "u8".fmt(f), + RustFlagsRepr::U16 => "u16".fmt(f), + RustFlagsRepr::U32 => "u32".fmt(f), + RustFlagsRepr::U64 => "u64".fmt(f), + RustFlagsRepr::U128 => "u128".fmt(f), + } + } +} diff --git a/crates/wit-bindgen-go/tests/codegen.rs b/crates/wit-bindgen-go/tests/codegen.rs new file mode 100644 index 000000000..007a80e76 --- /dev/null +++ b/crates/wit-bindgen-go/tests/codegen.rs @@ -0,0 +1,86 @@ +use std::io::prelude::*; +use std::io::BufReader; +use std::path::Path; +use std::process::Command; + +use heck::*; + +macro_rules! codegen_test { + (issue668 $name:tt $test:tt) => {}; + (multiversion $name:tt $test:tt) => {}; + ($id:ident $name:tt $test:tt) => { + #[test] + fn $id() { + test_helpers::run_world_codegen_test( + "guest-go", + $test.as_ref(), + |resolve, world, files| { + wit_bindgen_wrpc_go::Opts::default() + .build() + .generate(resolve, world, files) + .unwrap() + }, + verify, + ) + } + }; +} + +test_helpers::codegen_tests!(); + +fn verify(dir: &Path, name: &str) { + let name = name.to_snake_case(); + let main = dir.join(format!("{name}.go")); + + // The generated go package is named after the world's name. + // But tinygo currently does not support non-main package and requires + // a `main()` function in the module to compile. + // The following code replaces the package name to `package main` and + // adds a `func main() {}` function at the bottom of the file. + + // TODO: However, there is still an issue. Since the go module does not + // invoke the imported functions, they will be skipped by the compiler. + // This will weaken the test's ability to verify imported functions + let file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&main) + .expect("failed to open file"); + let mut reader = BufReader::new(file); + let mut buf = Vec::new(); + reader.read_until(b'\n', &mut buf).unwrap(); + // Skip over `package $WORLD` line + reader.read_until(b'\n', &mut Vec::new()).unwrap(); + buf.append(&mut "package main\n".as_bytes().to_vec()); + + // check if {name}_types.go exists + let types_file = dir.join(format!("{name}_types.go")); + if std::fs::metadata(types_file).is_ok() { + // create a directory called option and move the type file to option + std::fs::create_dir(dir.join("option")).expect("Failed to create directory"); + std::fs::rename( + dir.join(format!("{name}_types.go")), + dir.join("option").join(format!("{name}_types.go")), + ) + .expect("Failed to move file"); + buf.append(&mut format!("import . \"{name}/option\"\n").as_bytes().to_vec()); + } + + reader.read_to_end(&mut buf).expect("Failed to read file"); + buf.append(&mut "func main() {}".as_bytes().to_vec()); + std::fs::write(&main, buf).expect("Failed to write to file"); + + // create go.mod file + let mod_file = dir.join("go.mod"); + let mut file = std::fs::File::create(mod_file).expect("Failed to create file go.mod"); + file.write_all(format!("module {name}\n\ngo 1.20").as_bytes()) + .expect("Failed to write to file"); + + // run tinygo on Dir directory + + let mut cmd = Command::new("go"); + cmd.arg("build"); + cmd.arg(format!("{name}.go")); + cmd.current_dir(dir); + test_helpers::run_command(&mut cmd); +} diff --git a/crates/wit-bindgen-rust/src/interface.rs b/crates/wit-bindgen-rust/src/interface.rs index 311613a6e..e1275e2bc 100644 --- a/crates/wit-bindgen-rust/src/interface.rs +++ b/crates/wit-bindgen-rust/src/interface.rs @@ -1,6 +1,6 @@ use crate::{ int_repr, to_rust_ident, to_upper_camel_case, FnSig, Identifier, InterfaceName, Ownership, - RustFlagsRepr, RustWasm, + RustFlagsRepr, RustWrpc, }; use heck::*; use std::collections::{BTreeMap, BTreeSet}; @@ -12,7 +12,7 @@ pub struct InterfaceGenerator<'a> { pub src: Source, pub(super) identifier: Identifier<'a>, pub in_import: bool, - pub(super) gen: &'a mut RustWasm, + pub(super) gen: &'a mut RustWrpc, pub resolve: &'a Resolve, } diff --git a/crates/wit-bindgen-rust/src/lib.rs b/crates/wit-bindgen-rust/src/lib.rs index e83c90ae0..42c043d3f 100644 --- a/crates/wit-bindgen-rust/src/lib.rs +++ b/crates/wit-bindgen-rust/src/lib.rs @@ -22,7 +22,7 @@ struct InterfaceName { } #[derive(Default)] -struct RustWasm { +struct RustWrpc { types: Types, src: Source, opts: Opts, @@ -176,16 +176,16 @@ pub struct Opts { impl Opts { pub fn build(self) -> Box { - let mut r = RustWasm::new(); + let mut r = RustWrpc::new(); r.skip = self.skip.iter().cloned().collect(); r.opts = self; Box::new(r) } } -impl RustWasm { - fn new() -> RustWasm { - RustWasm::default() +impl RustWrpc { + fn new() -> RustWrpc { + RustWrpc::default() } fn interface<'a>( @@ -430,7 +430,7 @@ fn name_package_module(resolve: &Resolve, id: PackageId) -> String { format!("{base}{version}") } -impl WorldGenerator for RustWasm { +impl WorldGenerator for RustWrpc { fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { wit_bindgen_core::generated_preamble(&mut self.src, env!("CARGO_PKG_VERSION")); diff --git a/examples/go/hello-nats-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go b/examples/go/hello-nats-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go new file mode 100644 index 000000000..0c6061c72 --- /dev/null +++ b/examples/go/hello-nats-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go @@ -0,0 +1,74 @@ +// Generated by `wit-bindgen-wrpc-go` 0.1.0. DO NOT EDIT! +package handler + +import ( + bytes "bytes" + context "context" + binary "encoding/binary" + fmt "fmt" + wrpc "github.com/wrpc/wrpc/go" + slog "log/slog" + math "math" +) + +type Handler interface { + Hello(ctx__ context.Context) (r0__ string, err__ error) +} + +func ServeInterface(c wrpc.Client, h Handler) (stop func() error, err error) { + stops := make([]func() error, 0, 1) + stop = func() error { + for _, stop := range stops { + if err := stop(); err != nil { + return err + } + } + return nil + } + stop0, err := c.Serve("wrpc-examples:hello/handler", "hello", func(ctx context.Context, buffer []byte, tx wrpc.Transmitter, inv wrpc.IncomingInvocation) error { + slog.DebugContext(ctx, "accepting handshake") + if err := inv.Accept(ctx, nil); err != nil { + return fmt.Errorf("failed to complete handshake: %w", err) + } + slog.DebugContext(ctx, "calling `wrpc-examples:hello/handler.hello` handler") + r0, err := h.Hello(ctx) + if err != nil { + return fmt.Errorf("failed to handle `wrpc-examples:hello/handler.hello` invocation: %w", err) + } + var buf bytes.Buffer + if err := func(v string, w wrpc.ByteWriter) error { + n := len(v) + if n > math.MaxUint32 { + return fmt.Errorf("string byte length of %d overflows a 32-bit integer", n) + } + slog.Debug("writing string byte length", "len", n) + if err := func(v uint32, w wrpc.ByteWriter) error { + b := make([]byte, binary.MaxVarintLen32) + i := binary.PutUvarint(b, uint64(v)) + slog.Debug("writing u32") + _, err := w.Write(b[:i]) + return err + }(uint32(n), w); err != nil { + return fmt.Errorf("failed to write string length of %d: %w", n, err) + } + slog.Debug("writing string bytes") + _, err := w.Write([]byte(v)) + if err != nil { + return fmt.Errorf("failed to write string bytes: %w", err) + } + return nil + }(r0, &buf); err != nil { + return fmt.Errorf("failed to write result value 0: %w", err) + } + slog.DebugContext(ctx, "transmitting `wrpc-examples:hello/handler.hello` result") + if err := tx.Transmit(context.Background(), buf.Bytes()); err != nil { + return fmt.Errorf("failed to transmit result: %w", err) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to serve `wrpc-examples:hello/handler.hello`: %w", err) + } + stops = append(stops, stop0) + return stop, nil +} diff --git a/examples/go/hello-nats-server/bindings/server.wrpc.go b/examples/go/hello-nats-server/bindings/server.wrpc.go new file mode 100644 index 000000000..0200d61fa --- /dev/null +++ b/examples/go/hello-nats-server/bindings/server.wrpc.go @@ -0,0 +1,34 @@ +// Generated by `wit-bindgen-wrpc-go` 0.1.0. DO NOT EDIT! +// server package contains wRPC bindings for `server` world +package server + +import ( + exports__wrpc_examples__hello__handler "github.com/wrpc/wrpc/examples/go/hello-nats-server/bindings/exports/wrpc_examples/hello/handler" + wrpc "github.com/wrpc/wrpc/go" +) + +func Serve(c wrpc.Client, h interface { + exports__wrpc_examples__hello__handler.Handler +}) (stop func() error, err error) { + stops := make([]func() error, 0, 1) + stop = func() error { + for _, stop := range stops { + if err := stop(); err != nil { + return err + } + } + return nil + } + stop0, err := exports__wrpc_examples__hello__handler.ServeInterface(c, h) + if err != nil { + return + } + stops = append(stops, stop0) + stop = func() error { + if err := stop0(); err != nil { + return err + } + return nil + } + return +} diff --git a/examples/go/hello-nats-server/cmd/hello-nats-server/main.go b/examples/go/hello-nats-server/cmd/hello-nats-server/main.go new file mode 100644 index 000000000..69b982a58 --- /dev/null +++ b/examples/go/hello-nats-server/cmd/hello-nats-server/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "fmt" + "log" + "log/slog" + "os" + "os/signal" + "syscall" + + "github.com/nats-io/nats.go" + server "github.com/wrpc/wrpc/examples/go/hello-nats-server/bindings" + wrpcnats "github.com/wrpc/wrpc/go/nats" +) + +type Handler struct{} + +func (Handler) Hello(ctx context.Context) (string, error) { + return "hello from Go", nil +} + +func run() (err error) { + nc, err := nats.Connect(nats.DefaultURL) + if err != nil { + return fmt.Errorf("failed to connect to NATS.io: %w", err) + } + defer nc.Close() + defer func() { + if dErr := nc.Drain(); dErr != nil { + if err == nil { + err = fmt.Errorf("failed to drain NATS.io connection: %w", dErr) + } else { + slog.Error("failed to drain NATS.io connection", "err", dErr) + } + } + }() + + wrpc := wrpcnats.NewClient(nc, "go") + stop, err := server.Serve(wrpc, Handler{}) + if err != nil { + return fmt.Errorf("failed to serve `server` world: %w", err) + } + + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, syscall.SIGINT) + <-signalCh + + if err = stop(); err != nil { + return fmt.Errorf("failed to stop `server` world: %w", err) + } + return nil +} + +func init() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelInfo, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + }))) +} + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} diff --git a/examples/go/hello-nats-server/hello_nats_server.go b/examples/go/hello-nats-server/hello_nats_server.go new file mode 100644 index 000000000..44b6047c7 --- /dev/null +++ b/examples/go/hello-nats-server/hello_nats_server.go @@ -0,0 +1,3 @@ +//go:generate cargo run --bin wit-bindgen-wrpc go --out-dir bindings --package github.com/wrpc/wrpc/examples/go/hello-nats-server/bindings wit + +package hello_nats_server diff --git a/examples/go/hello-nats-server/main.go b/examples/go/hello-nats-server/main.go deleted file mode 100644 index 3beb3ec3e..000000000 --- a/examples/go/hello-nats-server/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - - "github.com/nats-io/nats.go" - wrpc "github.com/wrpc/wrpc/go" -) - -func handleHello(nc *nats.Conn, results string) (err error) { - b, err := wrpc.AppendString([]byte{}, "hello from Go") - if err != nil { - return fmt.Errorf("failed to encode `hello`: %w", err) - } - n := len(b) - maxPayload := nc.MaxPayload() - if int64(n) > maxPayload { - return fmt.Errorf("response length of %d exceeds NATS.io max payload of %d", n, maxPayload) - } - if err := nc.Publish(results, b); err != nil { - return fmt.Errorf("failed to publish response on result subject `%s`: %w", results, err) - } - return nil -} - -func run() error { - nc, err := nats.Connect(nats.DefaultURL) - if err != nil { - return fmt.Errorf("failed to connect to NATS.io: %w", err) - } - defer func() { - if err := nc.Drain(); err != nil { - log.Printf("failed to drain NATS.io connection: %s", err) - } - }() - defer nc.Close() - - hello := make(chan *nats.Msg) - helloSub, err := nc.ChanSubscribe("go.wrpc.0.0.1.wrpc-examples:hello/handler.hello", hello) - if err != nil { - return fmt.Errorf("failed to subscribe to `hello` invocations: %w", err) - } - defer func() { - if err := helloSub.Unsubscribe(); err != nil { - log.Printf("failed to unsubscribe from `hello`: %s", err) - return - } - close(hello) - }() - for msg := range hello { - if err := msg.Respond([]byte{}); err != nil { - log.Printf("failed to complete handshake on `%s` subject", msg.Reply) - continue - } - - if err := handleHello(nc, fmt.Sprintf("%s.results", msg.Reply)); err != nil { - log.Printf("failed to handle `hello`: %s", err) - b, err := wrpc.AppendString([]byte{}, fmt.Sprintf("%s", err)) - if err != nil { - log.Printf("failed to encode `hello` handling error: %s", err) - // Encoding the error failed, let's try encoding the encoding error, shall we? - b, err = wrpc.AppendString([]byte{}, fmt.Sprintf("failed to encode error: %s", err)) - if err != nil { - log.Printf("failed to encode `hello` handling error encoding error: %s", err) - // Well, we're out of luck at this point, let's just send an empty string - b = []byte{0} - } - } - if len(b) > int(nc.MaxPayload()) { - // TODO: split the payload into multiple chunks - b = []byte{0} - } - if err = nc.Publish(fmt.Sprintf("%s.error", msg.Reply), b); err != nil { - log.Printf("failed to send error to client: %s", err) - } - } - } - return nil -} - -func init() { - log.SetFlags(0) - log.SetOutput(os.Stderr) -} - -func main() { - if err := run(); err != nil { - log.Fatal(err) - } -} diff --git a/examples/go/hello-nats-server/wit/deps.lock b/examples/go/hello-nats-server/wit/deps.lock new file mode 100644 index 000000000..275cb66c0 --- /dev/null +++ b/examples/go/hello-nats-server/wit/deps.lock @@ -0,0 +1,4 @@ +[hello] +path = "../../../wit/hello" +sha256 = "3680bb734f3fa9f7325674142a2a9b558efd34ea2cb2df7ccb651ad869078d27" +sha512 = "688fdae594dc43bd65bd15ea66b77a8f97cb4bc1c3629719e91d6c1391c66f7c8c6517d096f686cca996188f64f075c4ccb0d70a40097ce76b8b4bcc71dc7506" diff --git a/examples/go/hello-nats-server/wit/deps.toml b/examples/go/hello-nats-server/wit/deps.toml new file mode 100644 index 000000000..084f03eb0 --- /dev/null +++ b/examples/go/hello-nats-server/wit/deps.toml @@ -0,0 +1 @@ +hello = "../../../wit/hello" diff --git a/examples/go/hello-nats-server/wit/deps/hello/hello.wit b/examples/go/hello-nats-server/wit/deps/hello/hello.wit new file mode 100644 index 000000000..6c84d66cc --- /dev/null +++ b/examples/go/hello-nats-server/wit/deps/hello/hello.wit @@ -0,0 +1,13 @@ +package wrpc-examples:hello; + +interface handler { + hello: func() -> string; +} + +world client { + import handler; +} + +world server { + export handler; +} diff --git a/examples/go/hello-nats-server/wit/world.wit b/examples/go/hello-nats-server/wit/world.wit new file mode 100644 index 000000000..a43c8c595 --- /dev/null +++ b/examples/go/hello-nats-server/wit/world.wit @@ -0,0 +1,5 @@ +package wrpc-examples:hello-go-server; + +world server { + include wrpc-examples:hello/server; +} diff --git a/examples/go/http-outgoing-nats-server/main.go b/examples/go/http-outgoing-nats-server/main.go index cbb578db8..56ac0f83a 100644 --- a/examples/go/http-outgoing-nats-server/main.go +++ b/examples/go/http-outgoing-nats-server/main.go @@ -199,11 +199,12 @@ func run() error { signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT) <-signalCh + if err = stop(); err != nil { - slog.Error("failed to stop serving", "err", err) + return fmt.Errorf("failed to stop serving: %w", err) } if err = nc.Drain(); err != nil { - slog.Error("failed to drain NATS.io connection", "err", err) + return fmt.Errorf("failed to drain NATS.io connection: %w", err) } return nil } diff --git a/src/bin/wit-bindgen-wrpc.rs b/src/bin/wit-bindgen-wrpc.rs new file mode 100644 index 000000000..2acac1770 --- /dev/null +++ b/src/bin/wit-bindgen-wrpc.rs @@ -0,0 +1,123 @@ +use anyhow::{bail, Context, Result}; +use clap::Parser; +use std::path::PathBuf; +use std::str; +use wit_bindgen_core::{wit_parser, Files, WorldGenerator}; +use wit_parser::Resolve; + +/// Helper for passing VERSION to opt. +/// If CARGO_VERSION_INFO is set, use it, otherwise use CARGO_PKG_VERSION. +fn version() -> &'static str { + option_env!("CARGO_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION")) +} + +#[derive(Debug, Parser)] +#[command(version = version())] +enum Opt { + /// Generates bindings for Rust wRPC applications + Rust { + #[clap(flatten)] + opts: wit_bindgen_wrpc_rust::Opts, + #[clap(flatten)] + args: Common, + }, + /// Generates bindings for Go wRPC applications + Go { + #[clap(flatten)] + opts: wit_bindgen_wrpc_go::Opts, + #[clap(flatten)] + args: Common, + }, +} + +#[derive(Debug, Parser)] +struct Common { + /// Where to place output files + #[clap(long = "out-dir")] + out_dir: Option, + + /// WIT document to generate bindings for. + #[clap(value_name = "DOCUMENT", index = 1)] + wit: PathBuf, + + /// World within the WIT document specified to generate bindings for. + /// + /// This can either be `foo` which is the default world in document `foo` or + /// it's `foo.bar` which is the world named `bar` within document `foo`. + #[clap(short, long)] + world: Option, + + /// Indicates that no files are written and instead files are checked if + /// they're up-to-date with the source files. + #[clap(long)] + check: bool, +} + +fn main() -> Result<()> { + let mut files = Files::default(); + let (generator, opt) = match Opt::parse() { + Opt::Rust { opts, args } => (opts.build(), args), + Opt::Go { opts, args } => (opts.build(), args), + }; + + gen_world(generator, &opt, &mut files)?; + + for (name, contents) in files.iter() { + let dst = match &opt.out_dir { + Some(path) => path.join(name), + None => name.into(), + }; + println!("Generating {:?}", dst); + + if opt.check { + let prev = std::fs::read(&dst).with_context(|| format!("failed to read {:?}", dst))?; + if prev != contents { + // The contents differ. If it looks like textual contents, do a + // line-by-line comparison so that we can tell users what the + // problem is directly. + if let (Ok(utf8_prev), Ok(utf8_contents)) = + (str::from_utf8(&prev), str::from_utf8(contents)) + { + if !utf8_prev + .chars() + .any(|c| c.is_control() && !matches!(c, '\n' | '\r' | '\t')) + && utf8_prev.lines().eq(utf8_contents.lines()) + { + bail!("{} differs only in line endings (CRLF vs. LF). If this is a text file, configure git to mark the file as `text eol=lf`.", dst.display()); + } + } + // The contents are binary or there are other differences; just + // issue a generic error. + bail!("not up to date: {}", dst.display()); + } + continue; + } + + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create {:?}", parent))?; + } + std::fs::write(&dst, contents).with_context(|| format!("failed to write {:?}", dst))?; + } + + Ok(()) +} + +fn gen_world( + mut generator: Box, + opts: &Common, + files: &mut Files, +) -> Result<()> { + let mut resolve = Resolve::default(); + let (pkg, _files) = resolve.push_path(&opts.wit)?; + let world = resolve.select_world(pkg, opts.world.as_deref())?; + generator.generate(&resolve, world, files)?; + + Ok(()) +} + +#[test] +fn verify_cli() { + use clap::CommandFactory; + Opt::command().debug_assert() +}