diff --git a/.github/workflows/smithy-bindgen.yml b/.github/workflows/smithy-bindgen.yml new file mode 100644 index 0000000..d31fb06 --- /dev/null +++ b/.github/workflows/smithy-bindgen.yml @@ -0,0 +1,61 @@ +name: SMITHY_BINDGEN + +on: + push: + branches: [main] + paths: + - "smithy-bindgen/**" + tags: + - "smithy-bindgen-v*" + pull_request: + branches: [main] + paths: + - "smithy-bindgen/**" + +env: + CARGO_TERM_COLOR: always + working-directory: ./smithy-bindgen + +jobs: + rust_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Update rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + - name: run_all_tests_clippy_fmt + working-directory: ${{ env.working-directory }} + run: | + make test + make rust-check + + github_release: + if: startswith(github.ref, 'refs/tags/') # Only run on tag push + needs: rust_test + runs-on: ubuntu-latest + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: true + + crates_release: + if: startswith(github.ref, 'refs/tags/') # Only run on tag push + needs: github_release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - id: crates-release-action + uses: wasmcloud/common-actions/crates-release@main + with: + working-directory: ${{ env.working-directory }} + crates-token: ${{ secrets.CRATES_PUBLISH_TOKEN }} diff --git a/Makefile b/Makefile index a97b9e6..eb79c62 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # If you're on mac, `brew install make` # and ensure `/usr/local/opt/make/libexec/gnubin` is in your PATH before /usr/bin -subdirs = codegen macros rpc-rs +subdirs = codegen macros smithy-bindgen rpc-rs all build release clean test update lint validate rust-check:: for dir in $(subdirs); do \ diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index af52009..3cef2ef 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "weld-codegen" -version = "0.6.0" +version = "0.7.0" edition = "2021" authors = [ "wasmcloud Team" ] license = "Apache-2.0" @@ -40,7 +40,7 @@ serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tempfile = "3.2" thiserror = "1.0" -toml = "0.5" +toml = "0.7" [lib] name = "weld_codegen" diff --git a/codegen/src/codegen_go.rs b/codegen/src/codegen_go.rs index 4e199d5..c54a5e8 100644 --- a/codegen/src/codegen_go.rs +++ b/codegen/src/codegen_go.rs @@ -211,7 +211,7 @@ impl<'model> CodeGen for GoCodeGen<'model> { &mut self, model: Option<&Model>, _lc: &LanguageConfig, - _output_dir: &Path, + _output_dir: Option<&Path>, _renderer: &mut Renderer, ) -> std::result::Result<(), Error> { self.namespace = None; diff --git a/codegen/src/codegen_py.rs b/codegen/src/codegen_py.rs index d8188f4..3d6b03b 100644 --- a/codegen/src/codegen_py.rs +++ b/codegen/src/codegen_py.rs @@ -90,7 +90,7 @@ impl<'model> CodeGen for PythonCodeGen<'model> { &mut self, model: Option<&Model>, _lc: &LanguageConfig, - _output_dir: &Path, + _output_dir: Option<&Path>, _renderer: &mut Renderer, ) -> std::result::Result<(), Error> { self.namespace = None; diff --git a/codegen/src/codegen_rust.rs b/codegen/src/codegen_rust.rs index 3d02cac..c4b380e 100644 --- a/codegen/src/codegen_rust.rs +++ b/codegen/src/codegen_rust.rs @@ -159,7 +159,7 @@ impl<'model> CodeGen for RustCodeGen<'model> { &mut self, model: Option<&Model>, _lc: &LanguageConfig, - _output_dir: &Path, + _output_dir: Option<&Path>, _renderer: &mut Renderer, ) -> std::result::Result<(), Error> { self.namespace = None; @@ -1212,7 +1212,7 @@ impl<'model> RustCodeGen<'model> { writeln!( w, r#"/// client for sending {} messages - #[derive(Debug)] + #[derive(Clone, Debug)] pub struct {}Sender {{ transport: T }} impl {}Sender {{ diff --git a/codegen/src/docgen.rs b/codegen/src/docgen.rs index 472bd4b..6b18208 100644 --- a/codegen/src/docgen.rs +++ b/codegen/src/docgen.rs @@ -12,6 +12,7 @@ use crate::{ format::SourceFormatter, gen::{to_json, CodeGen}, render::Renderer, + writer::Writer, Bytes, Error, JsonValue, ParamMap, }; @@ -42,9 +43,13 @@ impl CodeGen for DocGen { &mut self, model: Option<&Model>, lc: &LanguageConfig, - output_dir: &Path, + output_dir: Option<&Path>, renderer: &mut Renderer, ) -> std::result::Result<(), Error> { + let output_dir = match output_dir { + None => return Ok(()), + Some(d) => d, + }; let model = match model { None => return Ok(()), Some(model) => model, @@ -75,18 +80,9 @@ impl CodeGen for DocGen { .map(|id| id.to_string()) .collect::>(); - std::fs::create_dir_all(output_dir).map_err(|e| { - Error::Io(format!( - "creating directory {}: {}", - output_dir.display(), - e - )) - })?; - for ns in namespaces.iter() { let output_file = output_dir.join(format!("{}.html", crate::strings::to_snake_case(ns))); - let mut out = std::fs::File::create(&output_file).map_err(|e| { Error::Io(format!( "writing output file {}: {}", @@ -105,6 +101,7 @@ impl CodeGen for DocGen { /// DocGen doesn't do per-file generation so this is a no-op fn generate_file( &mut self, + _w: &mut Writer, _model: &Model, _file_config: &OutputFile, _params: &ParamMap, diff --git a/codegen/src/gen.rs b/codegen/src/gen.rs index 81996cd..85634b5 100644 --- a/codegen/src/gen.rs +++ b/codegen/src/gen.rs @@ -87,13 +87,6 @@ impl<'model> Generator { for (name, template) in COMMON_TEMPLATES.iter() { renderer.add_template((name, template))?; } - std::fs::create_dir_all(&output_dir).map_err(|e| { - Error::Io(format!( - "creating directory {}: {}", - &output_dir.display(), - e - )) - })?; for (language, mut lc) in config.languages.into_iter() { if !config.output_languages.is_empty() && !config.output_languages.contains(&language) { @@ -117,27 +110,26 @@ impl<'model> Generator { } // if language output_dir is relative, append it, otherwise use it let output_dir = if lc.output_dir.is_absolute() { - std::fs::create_dir_all(&lc.output_dir).map_err(|e| { - Error::Io(format!( - "creating directory {}: {}", - &lc.output_dir.display(), - e - )) - })?; lc.output_dir.clone() } else { output_dir.join(&lc.output_dir) }; + std::fs::create_dir_all(&output_dir).map_err(|e| { + Error::Io(format!( + "creating directory {}: {}", + output_dir.display(), + e + )) + })?; // add command-line overrides for (k, v) in defines.iter() { lc.parameters.insert(k.to_string(), v.clone()); } let base_params: BTreeMap = to_json(&lc.parameters)?; - let mut cgen = gen_for_language(&language, model); // initialize generator - cgen.init(model, &lc, &output_dir, &mut renderer)?; + cgen.init(model, &lc, Some(&output_dir), &mut renderer)?; // A common param dictionary is shared (read-only) by the renderer and the code generator, // Parameters include the following: @@ -200,7 +192,8 @@ impl<'model> Generator { )) })?; } else if let Some(model) = model { - let bytes = cgen.generate_file(model, file_config, ¶ms)?; + let mut w: Writer = Writer::default(); + let bytes = cgen.generate_file(&mut w, model, file_config, ¶ms)?; std::fs::write(&out_path, &bytes).map_err(|e| { Error::Io(format!("writing output file {}: {}", out_path.display(), e)) })?; @@ -243,7 +236,7 @@ fn gen_for_language<'model>( /// - write_services() /// - finalize() /// -pub(crate) trait CodeGen { +pub trait CodeGen { /// Initialize code generator and renderer for language output.j /// This hook is called before any code is generated and can be used to initialize code generator /// and/or perform additional processing before output files are created. @@ -252,7 +245,7 @@ pub(crate) trait CodeGen { &mut self, model: Option<&Model>, lc: &LanguageConfig, - output_dir: &Path, + output_dir: Option<&Path>, renderer: &mut Renderer, ) -> std::result::Result<(), Error> { Ok(()) @@ -263,17 +256,16 @@ pub(crate) trait CodeGen { /// The return value is Bytes containing the data that should be written to the output file. fn generate_file( &mut self, + w: &mut Writer, model: &Model, file_config: &OutputFile, params: &ParamMap, ) -> Result { - let mut w: Writer = Writer::default(); - - self.init_file(&mut w, model, file_config, params)?; - self.write_source_file_header(&mut w, model, params)?; - self.declare_types(&mut w, model, params)?; - self.write_services(&mut w, model, params)?; - self.finalize(&mut w) + self.init_file(w, model, file_config, params)?; + self.write_source_file_header(w, model, params)?; + self.declare_types(w, model, params)?; + self.write_services(w, model, params)?; + self.finalize(w) } /// Perform any initialization required prior to code generation for a file diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index c4c98c7..8fa5743 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -34,6 +34,13 @@ pub(crate) mod wasmbus_model { include!("./wasmbus_model.rs"); } +// enable other tools to invoke codegen directly. Add other languages as needed +pub mod generators { + pub use crate::codegen_go::GoCodeGen; + pub use crate::codegen_rust::RustCodeGen; + pub use crate::gen::CodeGen; +} + // re-export pub use bytes::Bytes; pub(crate) use bytes::BytesMut; diff --git a/codegen/src/model.rs b/codegen/src/model.rs index 22f9670..b36fde4 100644 --- a/codegen/src/model.rs +++ b/codegen/src/model.rs @@ -369,7 +369,7 @@ pub fn has_default(model: &'_ Model, member: &MemberShape) -> bool { } } -pub(crate) struct NumberedMember { +pub struct NumberedMember { field_num: Option, shape: MemberShape, } diff --git a/codegen/src/wasmbus_model.rs b/codegen/src/wasmbus_model.rs index a14a662..87c64c2 100644 --- a/codegen/src/wasmbus_model.rs +++ b/codegen/src/wasmbus_model.rs @@ -1,4 +1,4 @@ -// This file is @generated by wasmcloud/weld-codegen 0.5.0. +// This file is @generated by wasmcloud/weld-codegen 0.7.0. // It is not intended for manual editing. // namespace: org.wasmcloud.model diff --git a/rpc-rs/Cargo.toml b/rpc-rs/Cargo.toml index a3a5597..1f907a4 100644 --- a/rpc-rs/Cargo.toml +++ b/rpc-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wasmbus-rpc" -version = "0.11.2" +version = "0.12.0" authors = [ "wasmcloud Team" ] license = "Apache-2.0" description = "Runtime library for actors and capability providers" @@ -46,7 +46,7 @@ num-bigint = { version = "0.4", optional = true } bigdecimal = { version = "0.3", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -async-nats = "0.23.0" +async-nats = "0.27.1" atty = "0.2" data-encoding = "2.3" futures = "0.3" @@ -70,4 +70,4 @@ clap = { version = "4.0.22", features = ["derive"] } test-log = { version = "0.2.10", default-features = false, features = ["trace"] } [build-dependencies] -weld-codegen = { version = "0.6.0", path = "../codegen" } +weld-codegen = { version = "0.7.0", path = "../codegen" } diff --git a/rpc-rs/examples/request.rs b/rpc-rs/examples/request.rs index 05e18fb..db0a7ed 100644 --- a/rpc-rs/examples/request.rs +++ b/rpc-rs/examples/request.rs @@ -4,7 +4,7 @@ use nkeys::KeyPairType; use std::path::PathBuf; use std::sync::Arc; use wascap::prelude::KeyPair; -use wasmbus_rpc::rpc_client::RpcClient; +use wasmbus_rpc::{async_nats::connect, rpc_client::RpcClient}; /// RpcClient test CLI for making nats request #[derive(Parser)] @@ -54,7 +54,7 @@ async fn main() -> Result<()> { let timeout = args.timeout_ms.map(|n| std::time::Duration::from_millis(n as u64)); let kp = Arc::new(KeyPair::new(KeyPairType::User)); let nats_uri = args.nats.unwrap_or_else(|| "nats://127.0.0.1:4222".to_string()); - let nc = async_nats::connect(&nats_uri).await?; + let nc = connect(&nats_uri).await?; let client = RpcClient::new(nc, "HOST".into(), timeout, kp); let resp = client.request(args.subject, data).await?; diff --git a/rpc-rs/examples/sub.rs b/rpc-rs/examples/sub.rs index 453332a..07018a0 100644 --- a/rpc-rs/examples/sub.rs +++ b/rpc-rs/examples/sub.rs @@ -4,7 +4,7 @@ use futures::StreamExt; use nkeys::KeyPairType; use std::sync::Arc; use wascap::prelude::KeyPair; -use wasmbus_rpc::rpc_client::RpcClient; +use wasmbus_rpc::{async_nats::connect, rpc_client::RpcClient}; /// RpcClient test CLI for connection and subscription #[derive(Parser)] @@ -27,7 +27,7 @@ async fn main() -> Result<()> { } let kp = Arc::new(KeyPair::new(KeyPairType::User)); let nats_uri = args.nats.unwrap_or_else(|| "nats://127.0.0.1:4222".to_string()); - let nc = async_nats::connect(&nats_uri).await?; + let nc = connect(&nats_uri).await?; let client = RpcClient::new(nc, "HOST".into(), None, kp); println!("Subscribing to {}", &args.subject); diff --git a/rpc-rs/src/chunkify.rs b/rpc-rs/src/chunkify.rs index f3545e0..cda2908 100644 --- a/rpc-rs/src/chunkify.rs +++ b/rpc-rs/src/chunkify.rs @@ -23,7 +23,7 @@ use std::{ sync::{Arc, RwLock}, }; -use async_nats::jetstream::{ +use crate::async_nats::jetstream::{ self, object_store::{Config, ObjectStore}, Context, @@ -73,7 +73,7 @@ impl ChunkEndpoint { pub(crate) fn with_client( lattice: String, - nc: async_nats::Client, + nc: crate::async_nats::Client, domain: Option, ) -> Self { let map = jetstream_map(); diff --git a/rpc-rs/src/common.rs b/rpc-rs/src/common.rs index a54b8a4..fea6b12 100644 --- a/rpc-rs/src/common.rs +++ b/rpc-rs/src/common.rs @@ -55,7 +55,7 @@ impl SendOpts { /// Transport determines how messages are sent /// Alternate implementations could be mock-server, or test-fuzz-server / test-fuzz-client #[async_trait] -pub trait Transport: Send { +pub trait Transport: Send + Sync + Clone { async fn send( &self, ctx: &Context, @@ -221,7 +221,7 @@ impl AnySender { } } -impl AnySender { +impl AnySender { /// Send encoded payload #[inline] async fn send_raw<'s, 'ctx, 'msg>( diff --git a/rpc-rs/src/lib.rs b/rpc-rs/src/lib.rs index 9c7e7c3..e7b51e7 100644 --- a/rpc-rs/src/lib.rs +++ b/rpc-rs/src/lib.rs @@ -11,6 +11,13 @@ pub use timestamp::Timestamp; #[cfg(not(target_arch = "wasm32"))] pub use wascap; +// re-export async-nats. work-around for +// https://github.com/rust-lang/rust/issues/44663 and https://rust-lang.github.io/rfcs/1977-public-private-dependencies.html +// longer term: if public-private is not implemented, +// split out rpc-client to separate lib, and make interfaces build locally (as wit-bindgen does) +#[cfg(not(target_arch = "wasm32"))] +pub use async_nats; + #[cfg(all(not(target_arch = "wasm32"), feature = "otel"))] #[macro_use] pub mod otel; @@ -74,19 +81,19 @@ pub mod core { } /// Connect to nats using options provided by host - pub async fn nats_connect(&self) -> RpcResult { + pub async fn nats_connect(&self) -> RpcResult { use std::str::FromStr as _; let nats_addr = if !self.lattice_rpc_url.is_empty() { self.lattice_rpc_url.as_str() } else { crate::provider::DEFAULT_NATS_ADDR }; - let nats_server = async_nats::ServerAddr::from_str(nats_addr).map_err(|e| { - RpcError::InvalidParameter(format!("Invalid nats server url '{nats_addr}': {e}")) + let nats_server = crate::async_nats::ServerAddr::from_str(nats_addr).map_err(|e| { + RpcError::InvalidParameter(format!("Invalid nats server url '{}': {}", nats_addr, e)) })?; // Connect to nats - let nc = async_nats::ConnectOptions::default() + let nc = crate::async_nats::ConnectOptions::default() .connect(nats_server) .await .map_err(|e| { diff --git a/rpc-rs/src/otel.rs b/rpc-rs/src/otel.rs index 1fda004..1f4cd26 100644 --- a/rpc-rs/src/otel.rs +++ b/rpc-rs/src/otel.rs @@ -2,7 +2,7 @@ //! wasmbus-rpc calls. Please note that right now this is only supported for providers. This module //! is only available with the `otel` feature enabled -use async_nats::header::HeaderMap; +use crate::async_nats::header::HeaderMap; use opentelemetry::{ propagation::{Extractor, Injector, TextMapPropagator}, sdk::propagation::TraceContextPropagator, @@ -27,7 +27,7 @@ impl<'a> OtelHeaderExtractor<'a> { } /// Creates a new extractor using the given message - pub fn new_from_message(msg: &'a async_nats::Message) -> Self { + pub fn new_from_message(msg: &'a crate::async_nats::Message) -> Self { let inner = msg.headers.as_ref().unwrap_or(&EMPTY_HEADERS); OtelHeaderExtractor { inner } } @@ -115,7 +115,7 @@ impl From for HeaderMap { /// A convenience function that will extract the current context from NATS message headers and set /// the parent span for the current tracing Span. If you want to do something more advanced, use the /// [`OtelHeaderExtractor`] type directly -pub fn attach_span_context(msg: &async_nats::Message) { +pub fn attach_span_context(msg: &crate::async_nats::Message) { let header_map = OtelHeaderExtractor::new_from_message(msg); let ctx_propagator = TraceContextPropagator::new(); let parent_ctx = ctx_propagator.extract(&header_map); diff --git a/rpc-rs/src/provider.rs b/rpc-rs/src/provider.rs index 7c7e260..2699c4f 100644 --- a/rpc-rs/src/provider.rs +++ b/rpc-rs/src/provider.rs @@ -8,7 +8,7 @@ use std::{ convert::Infallible, fmt::Formatter, ops::Deref, - sync::{Arc, Mutex as StdMutex}, + sync::{atomic::AtomicU64, atomic::Ordering, Arc}, time::Duration, }; @@ -152,7 +152,10 @@ pub struct HostBridge { impl HostBridge { #[doc(hidden)] - pub fn new_client(nats: async_nats::Client, host_data: &HostData) -> RpcResult { + pub fn new_client( + nats: crate::async_nats::Client, + host_data: &HostData, + ) -> RpcResult { let key = Arc::new(if host_data.is_test() { KeyPair::new_user() } else { @@ -233,7 +236,10 @@ impl Deref for HostBridge { /// Initialize host bridge for use by wasmbus-test-util. /// The purpose is so that test code can get the nats configuration /// This is never called inside a provider process (and will fail if a provider calls it) -pub fn init_host_bridge_for_test(nc: async_nats::Client, host_data: &HostData) -> RpcResult<()> { +pub fn init_host_bridge_for_test( + nc: crate::async_nats::Client, + host_data: &HostData, +) -> RpcResult<()> { let hb = HostBridge::new_client(nc, host_data)?; crate::provider_main::set_host_bridge(hb) .map_err(|_| RpcError::Other("HostBridge already initialized".to_string()))?; @@ -269,7 +275,11 @@ impl HostBridge { // parse incoming subscription message // if it fails deserialization, we can't really respond; // so log the error - fn parse_msg(&self, msg: &async_nats::Message, topic: &str) -> Option { + fn parse_msg( + &self, + msg: &crate::async_nats::Message, + topic: &str, + ) -> Option { match if self.host_data.is_test() { serde_json::from_slice(&msg.payload).map_err(|e| RpcError::Deser(e.to_string())) } else { @@ -526,7 +536,7 @@ impl HostBridge { // When the above issue is fixed, verify the source and keep looping if it's invalid. // Check if we really need to shut down - if let Some(async_nats::Message { reply: Some(reply_to), payload, .. }) = msg { + if let Some(crate::async_nats::Message { reply: Some(reply_to), payload, .. }) = msg { let shutmsg: ShutdownMessage = serde_json::from_slice(&payload).unwrap_or_default(); // Backwards compatibility - if no host (or payload) is supplied, default // to shutting down unconditionally @@ -583,7 +593,7 @@ impl HostBridge { } #[instrument(level = "debug", skip_all, fields(actor_id = tracing::field::Empty, provider_id = tracing::field::Empty, contract_id = tracing::field::Empty, link_name = tracing::field::Empty))] - async fn handle_link_put

(&self, msg: async_nats::Message, provider: &P) + async fn handle_link_put

(&self, msg: crate::async_nats::Message, provider: &P) where P: ProviderDispatch + Send + Sync + Clone + 'static, { @@ -718,7 +728,17 @@ impl HostBridge { pub struct ProviderTransport<'send> { pub bridge: &'send HostBridge, pub ld: &'send LinkDefinition, - timeout: StdMutex, + timeout_ms: AtomicU64, +} + +impl<'send> Clone for ProviderTransport<'send> { + fn clone(&self) -> Self { + ProviderTransport { + bridge: self.bridge, + ld: self.ld, + timeout_ms: AtomicU64::new(self.timeout_ms.load(Ordering::Relaxed)), + } + } } impl<'send> ProviderTransport<'send> { @@ -738,14 +758,18 @@ impl<'send> ProviderTransport<'send> { ) -> Self { #[allow(clippy::redundant_closure)] let bridge = bridge.unwrap_or_else(|| crate::provider_main::get_host_bridge()); - let timeout = StdMutex::new(timeout.unwrap_or_else(|| { - bridge + let timeout = match timeout { + Some(d) => d.as_millis() as u64, + None => bridge .host_data .default_rpc_timeout_ms - .map(Duration::from_millis) - .unwrap_or(DEFAULT_RPC_TIMEOUT_MILLIS) - })); - Self { bridge, ld, timeout } + .unwrap_or(DEFAULT_RPC_TIMEOUT_MILLIS.as_millis() as u64), + }; + Self { + bridge, + ld, + timeout_ms: AtomicU64::new(timeout), + } } } @@ -759,19 +783,7 @@ impl<'send> Transport for ProviderTransport<'send> { ) -> RpcResult> { let origin = self.ld.provider_entity(); let target = self.ld.actor_entity(); - let timeout = { - if let Ok(rd) = self.timeout.lock() { - *rd - } else { - // if lock is poisioned - warn!("rpc timeout mutex error - using default value"); - self.bridge - .host_data - .default_rpc_timeout_ms - .map(Duration::from_millis) - .unwrap_or(DEFAULT_RPC_TIMEOUT_MILLIS) - } - }; + let timeout = Duration::from_millis(self.timeout_ms.load(Ordering::Relaxed)); let lattice = &self.bridge.lattice_prefix; self.bridge .rpc_client() @@ -780,10 +792,6 @@ impl<'send> Transport for ProviderTransport<'send> { } fn set_timeout(&self, interval: Duration) { - if let Ok(mut write) = self.timeout.lock() { - *write = interval; - } else { - warn!("rpc timeout mutex error - unchanged") - } + self.timeout_ms.store(interval.as_millis() as u64, Ordering::Relaxed); } } diff --git a/rpc-rs/src/provider_main.rs b/rpc-rs/src/provider_main.rs index d5df7db..cb6d50d 100644 --- a/rpc-rs/src/provider_main.rs +++ b/rpc-rs/src/provider_main.rs @@ -1,5 +1,6 @@ #![cfg(not(target_arch = "wasm32"))] +use crate::async_nats::{AuthError, ConnectOptions}; use std::io::{BufRead, StderrLock, Write}; use std::str::FromStr; @@ -156,13 +157,13 @@ where host_data.lattice_rpc_user_jwt.trim(), host_data.lattice_rpc_user_seed.trim(), ) { - ("", "") => async_nats::ConnectOptions::default(), + ("", "") => ConnectOptions::default(), (rpc_jwt, rpc_seed) => { let key_pair = std::sync::Arc::new(nkeys::KeyPair::from_seed(rpc_seed).unwrap()); let jwt = rpc_jwt.to_owned(); - async_nats::ConnectOptions::with_jwt(jwt, move |nonce| { + ConnectOptions::with_jwt(jwt, move |nonce| { let key_pair = key_pair.clone(); - async move { key_pair.sign(&nonce).map_err(async_nats::AuthError::new) } + async move { key_pair.sign(&nonce).map_err(AuthError::new) } }) } }, diff --git a/rpc-rs/src/rpc_client.rs b/rpc-rs/src/rpc_client.rs index c755da5..c6f21af 100644 --- a/rpc-rs/src/rpc_client.rs +++ b/rpc-rs/src/rpc_client.rs @@ -8,7 +8,7 @@ use std::{ time::Duration, }; -use async_nats::HeaderMap; +use crate::async_nats::{Client, ConnectOptions, HeaderMap}; use futures::Future; #[cfg(feature = "prometheus")] use prometheus::{IntCounter, Opts}; @@ -47,7 +47,7 @@ pub(crate) const CHUNK_RPC_EXTRA_TIME: Duration = Duration::from_secs(13); /// #[derive(Clone)] pub struct RpcClient { - client: async_nats::Client, + client: Client, key: Arc, /// host id (public key) for invocations host_id: String, @@ -119,7 +119,7 @@ impl RpcClient { /// parameters: async nats client, rpc timeout /// secret key for signing messages, host_id, and optional timeout. pub fn new( - nats: async_nats::Client, + nats: Client, host_id: String, timeout: Option, key_pair: Arc, @@ -131,7 +131,7 @@ impl RpcClient { /// parameters: nats client, lattice rpc prefix (usually "default"), /// secret key for signing messages, host_id, and optional timeout. pub(crate) fn new_client( - nats: async_nats::Client, + nats: Client, host_id: String, timeout: Option, key_pair: Arc, @@ -147,7 +147,7 @@ impl RpcClient { } /// convenience method for returning async client - pub fn client(&self) -> async_nats::Client { + pub fn client(&self) -> Client { self.client.clone() } @@ -617,10 +617,8 @@ impl RpcClient { } /// helper method to add logging to a nats connection. Logs disconnection (warn level), reconnection (info level), error (error), slow consumer, and lame duck(warn) events. -pub fn with_connection_event_logging( - opts: async_nats::ConnectOptions, -) -> async_nats::ConnectOptions { - use async_nats::Event; +pub fn with_connection_event_logging(opts: ConnectOptions) -> ConnectOptions { + use crate::async_nats::Event; opts.event_callback(|event| async move { match event { Event::Disconnected => warn!("nats client disconnected"), diff --git a/rpc-rs/src/wasmbus_core.rs b/rpc-rs/src/wasmbus_core.rs index f6c9bb2..831850b 100644 --- a/rpc-rs/src/wasmbus_core.rs +++ b/rpc-rs/src/wasmbus_core.rs @@ -1,4 +1,4 @@ -// This file is @generated by wasmcloud/weld-codegen 0.5.0. +// This file is @generated by wasmcloud/weld-codegen 0.7.0. // It is not intended for manual editing. // namespace: org.wasmcloud.core @@ -51,7 +51,7 @@ pub fn decode_actor_links(d: &mut crate::cbor::Decoder<'_>) -> Result = Vec::with_capacity(n as usize); for _ in 0..(n as usize) { arr.push(decode_link_definition(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#LinkDefinition': {e}") + format!("decoding 'org.wasmcloud.core#LinkDefinition': {}", e) })?) } arr @@ -63,7 +63,7 @@ pub fn decode_actor_links(d: &mut crate::cbor::Decoder<'_>) -> Result break, Ok(crate::cbor::Type::Break) => break, Ok(_) => arr.push(decode_link_definition(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#LinkDefinition': {e}") + format!("decoding 'org.wasmcloud.core#LinkDefinition': {}", e) })?), } } @@ -120,31 +120,30 @@ where pub fn decode_cluster_issuers( d: &mut crate::cbor::Decoder<'_>, ) -> Result { - let __result = - { - if let Some(n) = d.array()? { - let mut arr: Vec = Vec::with_capacity(n as usize); - for _ in 0..(n as usize) { - arr.push(decode_cluster_issuer_key(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#ClusterIssuerKey': {e}") - })?) - } - arr - } else { - // indefinite array - let mut arr: Vec = Vec::new(); - loop { - match d.datatype() { - Err(_) => break, - Ok(crate::cbor::Type::Break) => break, - Ok(_) => arr.push(decode_cluster_issuer_key(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#ClusterIssuerKey': {e}") - })?), - } + let __result = { + if let Some(n) = d.array()? { + let mut arr: Vec = Vec::with_capacity(n as usize); + for _ in 0..(n as usize) { + arr.push(decode_cluster_issuer_key(d).map_err(|e| { + format!("decoding 'org.wasmcloud.core#ClusterIssuerKey': {}", e) + })?) + } + arr + } else { + // indefinite array + let mut arr: Vec = Vec::new(); + loop { + match d.datatype() { + Err(_) => break, + Ok(crate::cbor::Type::Break) => break, + Ok(_) => arr.push(decode_cluster_issuer_key(d).map_err(|e| { + format!("decoding 'org.wasmcloud.core#ClusterIssuerKey': {}", e) + })?), } - arr } - }; + arr + } + }; Ok(__result) } /// health check request parameter @@ -411,18 +410,18 @@ pub fn decode_host_data(d: &mut crate::cbor::Decoder<'_>) -> Result invocation_seed = Some(d.str()?.to_string()), 8 => { env_values = Some(decode_host_env_values(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#HostEnvValues': {e}") + format!("decoding 'org.wasmcloud.core#HostEnvValues': {}", e) })?) } 9 => instance_id = Some(d.str()?.to_string()), 10 => { link_definitions = Some(decode_actor_links(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#ActorLinks': {e}") + format!("decoding 'org.wasmcloud.core#ActorLinks': {}", e) })?) } 11 => { cluster_issuers = Some(decode_cluster_issuers(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#ClusterIssuers': {e}") + format!("decoding 'org.wasmcloud.core#ClusterIssuers': {}", e) })?) } 12 => { @@ -459,18 +458,18 @@ pub fn decode_host_data(d: &mut crate::cbor::Decoder<'_>) -> Result invocation_seed = Some(d.str()?.to_string()), "envValues" => { env_values = Some(decode_host_env_values(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#HostEnvValues': {e}") + format!("decoding 'org.wasmcloud.core#HostEnvValues': {}", e) })?) } "instanceId" => instance_id = Some(d.str()?.to_string()), "linkDefinitions" => { link_definitions = Some(decode_actor_links(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#ActorLinks': {e}") + format!("decoding 'org.wasmcloud.core#ActorLinks': {}", e) })?) } "clusterIssuers" => { cluster_issuers = Some(decode_cluster_issuers(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#ClusterIssuers': {e}") + format!("decoding 'org.wasmcloud.core#ClusterIssuers': {}", e) })?) } "configJson" => { @@ -729,12 +728,12 @@ pub fn decode_invocation(d: &mut crate::cbor::Decoder<'_>) -> Result { origin = Some(decode_wasm_cloud_entity(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {e}") + format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {}", e) })?) } 1 => { target = Some(decode_wasm_cloud_entity(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {e}") + format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {}", e) })?) } 2 => operation = Some(d.str()?.to_string()), @@ -756,7 +755,7 @@ pub fn decode_invocation(d: &mut crate::cbor::Decoder<'_>) -> Result) -> Result { origin = Some(decode_wasm_cloud_entity(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {e}") + format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {}", e) })?) } "target" => { target = Some(decode_wasm_cloud_entity(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {e}") + format!("decoding 'org.wasmcloud.core#WasmCloudEntity': {}", e) })?) } "operation" => operation = Some(d.str()?.to_string()), @@ -797,7 +796,7 @@ pub fn decode_invocation(d: &mut crate::cbor::Decoder<'_>) -> Result contract_id = Some(d.str()?.to_string()), 4 => { values = Some(decode_link_settings(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#LinkSettings': {e}") + format!("decoding 'org.wasmcloud.core#LinkSettings': {}", e) })?) } _ => d.skip()?, @@ -1091,7 +1090,7 @@ pub fn decode_link_definition( "contractId" => contract_id = Some(d.str()?.to_string()), "values" => { values = Some(decode_link_settings(d).map_err(|e| { - format!("decoding 'org.wasmcloud.core#LinkSettings': {e}") + format!("decoding 'org.wasmcloud.core#LinkSettings': {}", e) })?) } _ => d.skip()?, @@ -1337,7 +1336,7 @@ pub trait ActorReceiver: MessageDispatch + Actor { match message.method { "HealthRequest" => { let value: HealthCheckRequest = crate::common::deserialize(&message.arg) - .map_err(|e| RpcError::Deser(format!("'HealthCheckRequest': {e}")))?; + .map_err(|e| RpcError::Deser(format!("'HealthCheckRequest': {}", e)))?; let resp = Actor::health_request(self, ctx, &value).await?; let buf = crate::common::serialize(&resp)?; @@ -1355,7 +1354,7 @@ pub trait ActorReceiver: MessageDispatch + Actor { /// ActorSender sends messages to a Actor service /// Actor service /// client for sending Actor messages -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ActorSender { transport: T, } @@ -1414,7 +1413,7 @@ impl Actor for ActorSender .await?; let value: HealthCheckResponse = crate::common::deserialize(&resp) - .map_err(|e| RpcError::Deser(format!("'{e}': HealthCheckResponse")))?; + .map_err(|e| RpcError::Deser(format!("'{}': HealthCheckResponse", e)))?; Ok(value) } } diff --git a/rpc-rs/src/wasmbus_model.rs b/rpc-rs/src/wasmbus_model.rs index 7a09ca2..32dccaf 100644 --- a/rpc-rs/src/wasmbus_model.rs +++ b/rpc-rs/src/wasmbus_model.rs @@ -1,4 +1,4 @@ -// This file is @generated by wasmcloud/weld-codegen 0.5.0. +// This file is @generated by wasmcloud/weld-codegen 0.7.0. // It is not intended for manual editing. // namespace: org.wasmcloud.model diff --git a/rpc-rs/tests/nats_sub.rs b/rpc-rs/tests/nats_sub.rs index a60712a..06151ee 100644 --- a/rpc-rs/tests/nats_sub.rs +++ b/rpc-rs/tests/nats_sub.rs @@ -7,6 +7,7 @@ use test_log::test; use tracing::{debug, error, info}; use wascap::prelude::KeyPair; use wasmbus_rpc::{ + async_nats::{ConnectOptions, ServerAddr}, error::{RpcError, RpcResult}, rpc_client::{with_connection_event_logging, RpcClient}, }; @@ -28,8 +29,8 @@ fn is_demo() -> bool { /// Parameter is optional RPC timeout async fn make_client(timeout: Option) -> RpcResult { let nats_url = nats_url(); - let server_addr = async_nats::ServerAddr::from_str(&nats_url).unwrap(); - let nc = with_connection_event_logging(async_nats::ConnectOptions::default()) + let server_addr = ServerAddr::from_str(&nats_url).unwrap(); + let nc = with_connection_event_logging(ConnectOptions::default()) .connect(server_addr) .await .map_err(|e| { diff --git a/smithy-bindgen/Cargo.toml b/smithy-bindgen/Cargo.toml new file mode 100644 index 0000000..e2bca35 --- /dev/null +++ b/smithy-bindgen/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "smithy-bindgen" +version = "0.1.0" +edition = "2021" +authors = [ "wasmcloud Team" ] +license = "Apache-2.0" +description = "macros for binding codegeneration from smithy files" +homepage = "https://github.com/wasmcloud/weld" +repository = "https://github.com/wasmcloud/weld" +documentation = "https://docs.rs/smithy-bindgen" +readme = "README.md" + +[lib] +proc-macro = true + +[dependencies] +atelier_json = "0.2" +proc-macro2 = "1.0" +proc-macro-error = "1.0" +quote = "1.0" +serde = { version = "1.0", features = [ "derive" ] } +serde_json = "1.0" +syn = { version = "1.0", features = ["full"] } +tracing = "0.1" +weld-codegen = { version = "0.7.0", path = "../codegen" } + +# these crates are needed by code generated from smithy wasmcloud interfaces +[dev-dependencies] +wasmbus-rpc = { version = "0.12", path = "../rpc-rs" } +async-trait = "0.1" +serde_bytes = "0.11" diff --git a/smithy-bindgen/Makefile b/smithy-bindgen/Makefile new file mode 100644 index 0000000..a6a54d6 --- /dev/null +++ b/smithy-bindgen/Makefile @@ -0,0 +1,18 @@ +# weld/smithy-bindgen/Makefile + +all: build + +build clean update: + cargo $@ + +release: + cargo build --release + +test:: + cargo test --all-features --all-targets -- --nocapture + +rust-check:: + cargo clippy --all-features --all-targets + rustfmt --edition 2021 --check src/*.rs + +.PHONY: all build release clean lint validate test update rust-check diff --git a/smithy-bindgen/README.md b/smithy-bindgen/README.md new file mode 100644 index 0000000..569d68f --- /dev/null +++ b/smithy-bindgen/README.md @@ -0,0 +1,134 @@ +# smithy-bindgen + +This crate provides macros for generating rust bindings from smithy interfaces. +For most uses of smithy interfaces used to generate rust sources, +this can eliminate the need for [`build.rs` and `codegen.toml` files](https://wasmcloud.dev/interfaces/codegen-toml/). +`codegen.toml` is still needed to generate bindings for other languages. + +> ### Feedback welcome! +> This is a first pass at code generation macro, inspired by `wit-bindgen`. If you have used `codegen.toml`, please let us know if this works as a replacement. (one known omission: the `params` setting from codegen.toml is not yet available in this macro) +> Other variations may be introduced in the future: one for actor-specific interfaces, provider-specific interfaces, etc. + +## Quick-start + +We recommend that the `smithy_bindgen!` macro is used inside a `mod` declaration, +or by itself in a separate rust source file, to avoid symbol conflicts with your code. + +If you're using a wasmcloud first-party interface, you can specify its path name +(relative to the wasmcloud/interfaces repo), followed by its namespace: + +``` +// [dependencies] +// smithy-bindgen = "0.1" + +mod httpserver { + smithy_bindgen::smithy_bindgen!("httpserver/httpserver.smithy","org.wasmcloud.interfaces.httpserver"); +} +use httpserver::{HttpRequest,HttpResponse}; + +``` + +If you have a smithy interface defined locally, you can load it + +``` +mod amazing_foo { + smithy_bindgen::smithy_bindgen!( + { path: "./amazing_foo.smithy" }, "org.example.interfaces.foo" + ); +} +use amazing_foo::{Bazinga, Wowza}; +``` + +Additional syntax forms are listed below. + + +> **Note** This doesn't replace all the functionality of wasmCloud's interface crates. A few of the wasmCloud interface crates have hand-generated helper functions that are not part of the smithy files. For example, `wasmcloud-interface-httpserver` contains `impl Default` and a few constructors for `HttpResponse`. Those are not generated by `smithy_bindgen!`. We are planning to move those extra helper functions into a future wasmcloud sdk. + + +## Syntax + +The first parameter of the `smithy_bindgen!` macro can take one of three forms. +The second parameter is the namespace used for code generation. + +- one wasmcloud first-party interface + + The single-file parameter is a path relative to the wasmcloud interfaces git repo `wasmcloud/interfaces` + + ``` + smithy_bindgen!("httpserver/httpserver.smithy", "org.wasmcloud.interfaces.httpserver"); + ```` + + The above is shorthand for the following: + ``` + smithy_bindgen!({ + url: "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces", + files: ["httpserver/httpserver.smithy"] + }, "org.wasmcloud.interfaces.httpserver" ); + ``` + +- one Model Source + + ``` + smithy-bindgen!({ + path: "../interfaces/foo.smithy", + }, "org.example.interfaces.foo" ); + ```` + +- array of Model Sources + + ``` + smithy-bindgen!([ + { path: "../interfaces/foo.smithy" }, + { url: "keyvalue/keyvalue.smithy" }, + ], "org.example.interfaces.foo" ); + ``` + +## Model Source Specification + +A model source contains a `url`, for http(s) downloads, or a `path`, for local fs access, that serves as a base, plus `files`, an optional list of file paths that are appended to the base to build complete url download paths and local file paths. +When joining the sub-paths from the `files` array, '/' is inserted or removed as needed, so that there is exactly one between the base and the sub-path. +`url` must begin with either 'http://' or 'https://'. If `path` is a relative fs path, it is relative to the folder containing `Cargo.toml`. +`files` may be omitted if the `url` or `path` contains the full path to the `.smithy` file. + +All the following are (syntactically) valid model sources: +``` +{ url: "https://example.com/interfaces/foo.smithy" } +{ url: "https://example.com/interfaces", files: [ "foo.smithy", "bar.smithy" ]} +{ path: "../interfaces/foo.smithy" } +{ path: "../interfaces", files: ["foo.smithy", "bar.smithy"]} +``` + +These are all equivalent: +``` +{ path: "/usr/share/interfaces/timer.smithy" } +{ path: "/usr/share/interfaces", files: [ "timer.smithy" ] } +{ path: "/usr/share/interfaces/", files: [ "timer.smithy" ] } +``` + +If a model source structure contains no url base and no path base, +the url for the github wasmcloud interface repo is used: +``` +url: "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces" +``` + +Why would the code generator need to load more than one smithy file? So that interfaces can share common symbols for data structures. Most smithy interfaces already import symbols from the namespace `org.wasmcloud.model`, defined in `wasmcloud-model.smithy`. +The bindgen tool resolves all symbols by assembling an in-memory schema model from all the smithy sources and namespaces, then traversing through the in-memory model, generating code only for the schema elements in the namespace declared in the second parameter of `smithy_bindgen!`. + +## jsdelivr.net urls + +`cdn.jsdelivr.net` mirrors open source github repositories. +The [url syntax](https://www.jsdelivr.com/?docs=gh) can optionally include +a github branch, tag, or commit sha. + +## Common files + +Wasmcloud common model files are always automatically included when compiling models +(If you've used `codegen.toml` files, you may remember that they required all base models +to be specified explicitly.) + +## Namespace + +Models may include symbols defined in other models via the `use` command. +Only the symbols defined in the namespace (`smithy_bindgen!`'s second parameter) +will be included in the generated code. + diff --git a/smithy-bindgen/src/lib.rs b/smithy-bindgen/src/lib.rs new file mode 100644 index 0000000..aefcae4 --- /dev/null +++ b/smithy-bindgen/src/lib.rs @@ -0,0 +1,325 @@ +///! smithy-bindgen macros +///! +use proc_macro2::Span; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, path::PathBuf, str::FromStr}; +use syn::{ + bracketed, parse::Parse, parse::ParseStream, parse::Result, punctuated::Punctuated, + spanned::Spanned, token, Error, LitStr, Token, +}; +use weld_codegen::{ + config::{ModelSource, OutputFile}, + generators::{CodeGen, RustCodeGen}, + render::Renderer, + sources_to_model, + writer::Writer, +}; + +const BASE_MODEL_URL: &str = "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces"; +const CORE_MODEL: &str = "core/wasmcloud-core.smithy"; +const MODEL_MODEL: &str = "core/wasmcloud-model.smithy"; + +/// Generate code from a smithy IDL file. +/// +/// ## Syntax +/// +/// The first parameter of the `smithy_bindgen!` macro can take one of three forms. +/// The second parameter is the namespace used for code generation. +/// +/// - one wasmcloud first-party interface +/// +/// The single-file parameter is a path relative to the wasmcloud interfaces git repo `wasmcloud/interfaces` +/// +/// ``` +/// # use smithy_bindgen::smithy_bindgen; +/// smithy_bindgen!("httpserver/httpserver.smithy", "org.wasmcloud.interfaces.httpserver"); +/// ```` +/// +/// The above is shorthand for the following: +/// ``` +/// # use smithy_bindgen::smithy_bindgen; +/// smithy_bindgen!({ +/// url: "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces", +/// files: ["httpserver/httpserver.smithy"] +/// }, "org.wasmcloud.interfaces.httpserver" ); +/// ``` +/// +/// - one Model Source +/// +/// ``` +/// # use smithy_bindgen::smithy_bindgen; +/// smithy_bindgen!({ +/// path: "./tests/test-bindgen.smithy", +/// }, "org.example.interfaces.foo" ); +/// ```` +/// +/// - array of Model Sources +/// +/// ``` +/// # use smithy_bindgen::smithy_bindgen; +/// smithy_bindgen!([ +/// { path: "./tests/test-bindgen.smithy" }, +/// { url: "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces/factorial/factorial.smithy" }, +/// ], "org.example.interfaces.foo" ); +/// ``` +/// +/// ## Model Source Specification +/// +/// A model source contains a `url`, for http(s) downloads, or a `path`, for local fs access, that serves as a base, plus `files`, an optional list of file paths that are appended to the base to build complete url download paths and local file paths. +/// When joining the sub-paths from the `files` array, '/' is inserted or removed as needed, so that there is exactly one between the base and the sub-path. +/// `url` must begin with either 'http://' or 'https://'. If `path` is a relative fs path, it is relative to the folder containing `Cargo.toml`. +/// `files` may be omitted if the `url` or `path` contains the full path to the `.smithy` file. +/// +/// All the following are (syntactically) valid model sources: +/// ``` +/// { url: "https://example.com/interfaces/foo.smithy" } +/// { url: "https://example.com/interfaces", files: [ "foo.smithy", "bar.smithy" ]} +/// { path: "../interfaces/foo.smithy" } +/// { path: "../interfaces", files: ["foo.smithy", "bar.smithy"]} +/// ``` +/// +/// If a model source structure contains no url base and no path base, +/// the url for the github wasmcloud interface repo is used: +/// ``` +/// url: "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces" +/// ``` +/// +/// Why would the code generator need to load more than one smithy file? So that interfaces can share common symbols for data structures. Most smithy interfaces already import symbols from the namespace `org.wasmcloud.model`, defined in `wasmcloud-model.smithy`. +/// The bindgen tool resolves all symbols by assembling an in-memory schema model from all the smithy sources and namespaces, then traversing through the in-memory model, generating code only for the schema elements in the namespace declared in the second parameter of `smithy_bindgen!`. +/// +/// ## jsdelivr.net urls +/// +/// `cdn.jsdelivr.net` mirrors open source github repositories. +/// The [url syntax](https://www.jsdelivr.com/?docs=gh) can optionally include +/// a github branch, tag, or commit sha. +/// +/// ## Common files +/// +/// Wasmcloud common model files are always automatically included when compiling models +/// (If you've used `codegen.toml` files, you may remember that they required all base models +/// to be specified explicitly.) +/// +/// ## Namespace +/// +/// Models may include symbols defined in other models via the `use` command. +/// Only the symbols defined in the namespace (`smithy_bindgen!`'s second parameter) +/// will be included in the generated code. +#[proc_macro] +pub fn smithy_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let bindgen = syn::parse_macro_input!(input as BindgenConfig); + generate_source(bindgen) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// parse sources into smithy ast model, then write 'namespace' to generated code +fn generate_source(bindgen: BindgenConfig) -> Result { + let call_site = Span::call_site(); + let sources = bindgen + .sources + .into_iter() + .map(SmithySource::into) + .collect::>(); + let mut w = Writer::default(); + let model = sources_to_model(&sources, &PathBuf::new(), 0).map_err(|e| { + Error::new( + call_site.span(), + format!("cannot compile model sources: {}", e), + ) + })?; + let mut rust_gen = RustCodeGen::new(Some(&model)); + let output_config = OutputFile { + namespace: Some(bindgen.namespace), + ..Default::default() + }; + let mut params = BTreeMap::::default(); + params.insert("model".into(), atelier_json::model_to_json(&model)); + let mut renderer = Renderer::default(); + let bytes = rust_gen + .init(Some(&model), &Default::default(), None, &mut renderer) + .and_then(|_| rust_gen.generate_file(&mut w, &model, &output_config, ¶ms)) + .map_err(|e| { + Error::new( + call_site.span(), + format!("cannot generate rust source: {}", e), + ) + })?; + proc_macro2::TokenStream::from_str(&String::from_utf8_lossy(&bytes)).map_err(|e| { + Error::new( + call_site.span(), + format!("cannot parse generated code: {}", e), + ) + }) +} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct SmithySource { + url: Option, + path: Option, + files: Vec, +} + +/// internal struct used by smithy-bindgen +#[derive(Debug, Default, Serialize, Deserialize)] +struct BindgenConfig { + pub sources: Vec, + pub namespace: String, +} + +impl From for ModelSource { + fn from(source: SmithySource) -> Self { + match (source.url, source.path) { + (Some(url), _) => ModelSource::Url { url, files: source.files }, + (_, Some(path)) => ModelSource::Path { path: path.into(), files: source.files }, + _ => unreachable!(), + } + } +} + +mod kw { + syn::custom_keyword!(url); + syn::custom_keyword!(path); + syn::custom_keyword!(files); +} + +enum Opt { + Url(String), + Path(String), + Files(Vec), +} + +impl Parse for Opt { + fn parse(input: ParseStream<'_>) -> Result { + let l = input.lookahead1(); + if l.peek(kw::url) { + input.parse::()?; + input.parse::()?; + Ok(Opt::Url(input.parse::()?.value())) + } else if l.peek(kw::path) { + input.parse::()?; + input.parse::()?; + Ok(Opt::Path(input.parse::()?.value())) + } else if l.peek(kw::files) { + input.parse::()?; + input.parse::()?; + let content; + let _array = bracketed!(content in input); + let files = Punctuated::::parse_terminated(&content)? + .into_iter() + .map(|val| val.value()) + .collect(); + Ok(Opt::Files(files)) + } else { + Err(l.error()) + } + } +} + +impl Parse for SmithySource { + fn parse(input: ParseStream<'_>) -> syn::parse::Result { + let call_site = Span::call_site(); + let mut source = SmithySource::default(); + let content; + syn::braced!(content in input); + let fields = Punctuated::::parse_terminated(&content)?; + for field in fields.into_pairs() { + match field.into_value() { + Opt::Url(s) => { + if source.url.is_some() { + return Err(Error::new(s.span(), "cannot specify second url")); + } + if source.path.is_some() { + return Err(Error::new(s.span(), "cannot specify path and url")); + } + source.url = Some(s) + } + Opt::Path(s) => { + if source.path.is_some() { + return Err(Error::new(s.span(), "cannot specify second path")); + } + if source.url.is_some() { + return Err(Error::new(s.span(), "cannot specify path and url")); + } + source.path = Some(s) + } + Opt::Files(val) => source.files = val, + } + } + if !(!source.files.is_empty() + || (source.url.is_some() && source.url.as_ref().unwrap().ends_with(".smithy")) + || (source.path.is_some() && source.path.as_ref().unwrap().ends_with(".smithy"))) + { + return Err(Error::new( + call_site.span(), + "There must be at least one .smithy file", + )); + } + if source.url.is_none() && source.path.is_none() { + source.url = Some(BASE_MODEL_URL.to_string()); + } + Ok(source) + } +} + +impl Parse for BindgenConfig { + fn parse(input: ParseStream<'_>) -> syn::parse::Result { + let call_site = Span::call_site(); + let mut sources; + + let l = input.lookahead1(); + if l.peek(token::Brace) { + // one source + let source = input.parse::()?; + sources = vec![source]; + } else if l.peek(token::Bracket) { + // list of sources + let content; + syn::bracketed!(content in input); + sources = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + } else if l.peek(LitStr) { + // shorthand for wasmcloud default url + let one_file = input.parse::()?; + sources = vec![SmithySource { + url: Some(BASE_MODEL_URL.into()), + path: None, + files: vec![ + "core/wasmcloud-core.smithy".into(), + "core/wasmcloud-model.smithy".into(), + one_file.value(), + ], + }]; + } else { + return Err(Error::new( + call_site.span(), + "expected quoted path, or model source { url or path: ..., files: ,.. }, or list of model sources [...]" + )); + } + input.parse::()?; + let namespace = input.parse::()?.value(); + + // append base models if either are missing + let has_core = sources.iter().any(|s| { + (s.url.is_some() && s.url.as_ref().unwrap().ends_with(CORE_MODEL)) + || s.files.iter().any(|s| s.ends_with(CORE_MODEL)) + }); + let has_model = sources.iter().any(|s| { + (s.url.is_some() && s.url.as_ref().unwrap().ends_with(MODEL_MODEL)) + || s.files.iter().any(|s| s.ends_with(MODEL_MODEL)) + }); + if !has_core || !has_model { + sources.push(SmithySource { + url: Some(BASE_MODEL_URL.into()), + files: match (has_core, has_model) { + (false, false) => vec![CORE_MODEL.into(), MODEL_MODEL.into()], + (false, true) => vec![CORE_MODEL.into()], + (true, false) => vec![MODEL_MODEL.into()], + _ => unreachable!(), + }, + path: None, + }); + } + Ok(BindgenConfig { sources, namespace }) + } +} diff --git a/smithy-bindgen/tests/bindgen.rs b/smithy-bindgen/tests/bindgen.rs new file mode 100644 index 0000000..8976a48 --- /dev/null +++ b/smithy-bindgen/tests/bindgen.rs @@ -0,0 +1,44 @@ +#[test] +fn test_str() { + mod test { + smithy_bindgen::smithy_bindgen!( + "keyvalue/keyvalue.smithy", + "org.wasmcloud.interface.keyvalue" + ); + } + + use test::ListAddRequest; + use test::StringList; + + let _x = StringList::new(); + let _y = ListAddRequest::default(); + println!("hello"); +} + +#[test] +fn test_path_base() { + mod test { + smithy_bindgen::smithy_bindgen!( + { path: ".", files: [ "tests/test-bindgen.smithy"]}, + "org.wasmcloud.test.bindgen"); + } + + use test::Thing; + + let x = Thing { value: "hello".into() }; + println!("{}", x.value); +} + +#[test] +fn test_path_complete() { + mod test { + smithy_bindgen::smithy_bindgen!( + { path: "./tests/test-bindgen.smithy"}, + "org.wasmcloud.test.bindgen"); + } + + use test::Thing; + + let x = Thing { value: "hello".into() }; + println!("{}", x.value); +} diff --git a/smithy-bindgen/tests/test-bindgen.smithy b/smithy-bindgen/tests/test-bindgen.smithy new file mode 100644 index 0000000..5664950 --- /dev/null +++ b/smithy-bindgen/tests/test-bindgen.smithy @@ -0,0 +1,14 @@ +// bindgen-test.smithy + +metadata package = [ { + namespace: "org.wasmcloud.test.bindgen", + //crate: "wasmcloud_interface_factorial", +} ] + +namespace org.wasmcloud.test.bindgen + +structure Thing { + @required + value: String, +} +