diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6207c455..c5d2fa81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ env: # Dockerfile build, and the local host sccache — one silo, zero egress cost. # Set at the workflow env level (not the composite) because composite actions # cannot read the `secrets` context. Keys are content-addressed on the full - # compiler input, and the sole build.rs (cow-venue) + nexum-macros are + # compiler input, and the sole build.rs (cow-venue) + the macro crates are # deterministic, so PR builds writing to the shared bucket write correct objects # under correct keys (no poisoning); bound storage with an R2 lifecycle-expiry # rule on the bucket (sccache does not evict cloud backends itself). diff --git a/Cargo.lock b/Cargo.lock index 56f41150..8aa31b32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3592,7 +3592,7 @@ dependencies = [ ] [[package]] -name = "nexum-macros" +name = "nexum-module-macros" version = "0.1.0" dependencies = [ "nexum-world", @@ -3654,7 +3654,7 @@ dependencies = [ "alloy-sol-types", "alloy-transport", "http", - "nexum-macros", + "nexum-module-macros", "nexum-sdk-test", "nexum-status-body", "proptest", @@ -6060,16 +6060,26 @@ dependencies = [ "wasmtime", ] +[[package]] +name = "videre-macros" +version = "0.1.0" +dependencies = [ + "nexum-world", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "videre-sdk" version = "0.1.0" dependencies = [ "borsh", - "nexum-macros", "nexum-sdk", "nexum-sdk-test", "strum", "thiserror 2.0.18", + "videre-macros", "wit-bindgen 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index 13067951..d8e1be24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "crates/cow-venue", "crates/nexum-cli", "crates/nexum-launch", - "crates/nexum-macros", + "crates/nexum-module-macros", "crates/nexum-runtime", "crates/nexum-sdk", "crates/nexum-sdk-test", @@ -18,6 +18,7 @@ members = [ "crates/shepherd-sdk", "crates/shepherd-sdk-test", "crates/videre-host", + "crates/videre-macros", "crates/videre-sdk", "modules/ethflow-watcher", "modules/example", @@ -141,9 +142,9 @@ http-body = "1" http-body-util = "0.1" bytes = "1" -# Proc-macro toolkit backing `nexum-macros`. Host-side only: the -# proc-macro crate always builds for the host, even when the module -# consuming it targets wasm. +# Proc-macro toolkit backing `nexum-module-macros` and `videre-macros`. +# Host-side only: a proc-macro crate always builds for the host, even +# when the module consuming it targets wasm. proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } diff --git a/crates/nexum-macros/Cargo.toml b/crates/nexum-module-macros/Cargo.toml similarity index 81% rename from crates/nexum-macros/Cargo.toml rename to crates/nexum-module-macros/Cargo.toml index ec7785bd..4bef46f5 100644 --- a/crates/nexum-macros/Cargo.toml +++ b/crates/nexum-module-macros/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "nexum-macros" +name = "nexum-module-macros" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Proc-macro glue for nexum runtime modules: #[module] emits the per-cdylib wit-bindgen, host adapter, event dispatch, and export; derive(IntentBody) emits the venue SDK's versioned body codec." +description = "Proc-macro glue for nexum runtime modules: #[module] emits the per-cdylib wit-bindgen, host adapter, event dispatch, and export." [lib] proc-macro = true diff --git a/crates/nexum-macros/src/lib.rs b/crates/nexum-module-macros/src/lib.rs similarity index 50% rename from crates/nexum-macros/src/lib.rs rename to crates/nexum-module-macros/src/lib.rs index ec26d462..bb90eb72 100644 --- a/crates/nexum-macros/src/lib.rs +++ b/crates/nexum-module-macros/src/lib.rs @@ -7,44 +7,15 @@ //! `nexum_sdk::bind_host_via_wit_bindgen!`), the `Guest` implementation //! whose `on-event` dispatches to the handlers present, and `export!`. //! -//! [`venue`] is the adapter counterpart: it emits the same per-cdylib -//! wit-bindgen and `export!`, but for a per-component venue-adapter -//! world exporting the `videre:venue/adapter` face and importing only -//! the manifest's declared scoped transport. +//! The venue-side macros (`#[venue]`, `derive(IntentBody)`) live in +//! `videre-macros`. //! -//! [`derive@IntentBody`] implements the venue SDK's versioned body codec -//! over a per-venue version enum. -//! -//! Consumers reach these through the SDK re-exports (`nexum_sdk::module`, -//! `videre_sdk::venue`, `videre_sdk::IntentBody`) rather than -//! depending on this crate directly. - -mod intent_body; -mod world; +//! Consumers reach this through the SDK re-export (`nexum_sdk::module`) +//! rather than depending on this crate directly. use proc_macro::TokenStream; use quote::quote; -use syn::{DeriveInput, ImplItem, ItemImpl, Type}; - -/// Derive the venue SDK's `IntentBody` codec on the outer per-venue -/// version enum: one newtype variant per published body version, each -/// payload a borsh type. -/// -/// The wire form is the borsh enum layout (a one-byte tag, the variant's -/// declaration index, then the borsh payload), so the tag order is the -/// schema: append new versions, never reorder. Decoding an unknown tag -/// fails typedly as `BodyError::UnknownVersion`. -/// -/// Generated code resolves the SDK by crate path, so use the -/// `videre_sdk::IntentBody` re-export with `videre-sdk` as a -/// direct dependency. -#[proc_macro_derive(IntentBody)] -pub fn derive_intent_body(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - intent_body::expand(&input) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} +use syn::{ImplItem, ItemImpl, Type}; /// The handler names recognised on a `#[module]` impl. Any method not in /// this set is left untouched on the type, except that names starting @@ -271,251 +242,12 @@ pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -/// The associated functions the `videre:venue/adapter` face mandates. A -/// venue adapter must define all five; `init` is separate (a no-op when -/// absent, exactly as in a module). -const VENUE_EXPORTS: [&str; 5] = ["derive_header", "quote", "submit", "status", "cancel"]; - -/// Generate the per-cdylib glue for a venue adapter. -/// -/// Apply to an inherent `impl` block whose associated functions are the -/// adapter face: `derive_header`, `quote`, `submit`, `status`, `cancel` -/// (all required, from `videre:venue/adapter`), plus an optional `init` -/// (absent means a no-op) and an optional `body_versions` (absent -/// declares none). Each takes and returns the per-cdylib -/// wit-bindgen payloads for its signature. The macro reads the crate's -/// `module.toml`, synthesizes a per-component world exporting the -/// adapter face and importing exactly the manifest's declared scoped -/// transport, then emits `wit_bindgen::generate!`, the `Guest` impls -/// wiring the world to the adapter's functions, and `export!` around the -/// untouched impl. So the built component imports what the manifest -/// declares and nothing else, retiring the toolchain-elision dependency -/// on the venue side. -/// -/// A venue's capabilities are scoped transport only: an undeclared -/// capability's bindings do not exist (using one is a compile error), -/// and a capability outside the venue-permitted set (`chain`, -/// `messaging`, `http`) is rejected at expansion. -/// -/// The same crate-root resolution invariants as [`macro@module`] apply: -/// the wit-bindgen output lands at the module crate root (so the emitted -/// glue resolves `Guest`, `Fault`, and the `nexum::*`/`videre::*` type modules -/// there), the consuming crate must declare `wit-bindgen` as a direct -/// dependency, and the crate root must not shadow std prelude names. -#[proc_macro_attribute] -pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream { - if !attr.is_empty() { - return syn::Error::new( - proc_macro2::Span::call_site(), - "#[videre_sdk::venue] takes no arguments", - ) - .to_compile_error() - .into(); - } - - let input = syn::parse_macro_input!(item as ItemImpl); - - let self_ty = &input.self_ty; - if !is_plain_type(self_ty) { - return syn::Error::new_spanned( - self_ty, - "#[videre_sdk::venue] must be applied to an inherent impl of a named type", - ) - .to_compile_error() - .into(); - } - if let Some((_, trait_path, _)) = &input.trait_ { - return syn::Error::new_spanned( - trait_path, - "#[videre_sdk::venue] must be applied to an inherent impl, not a trait impl", - ) - .to_compile_error() - .into(); - } - if !input.generics.params.is_empty() { - return syn::Error::new_spanned( - &input.generics, - "#[videre_sdk::venue] must be applied to a non-generic impl", - ) - .to_compile_error() - .into(); - } - - let defines = |name: &str| { - input - .items - .iter() - .any(|item| matches!(item, ImplItem::Fn(f) if f.sig.ident == name)) - }; - let missing: Vec<&str> = VENUE_EXPORTS - .into_iter() - .filter(|name| !defines(name)) - .collect(); - if !missing.is_empty() { - return syn::Error::new_spanned( - self_ty, - format!( - "#[videre_sdk::venue] requires the adapter face; this impl is missing {:?}. \ - Define all of `derive_header`, `quote`, `submit`, `status`, `cancel` (plus an \ - optional `init`)", - missing - ), - ) - .to_compile_error() - .into(); - } - - let (manifest_path, venue_world) = match derive_venue_world() { - Ok(parts) => parts, - Err(msg) => { - return syn::Error::new(proc_macro2::Span::call_site(), msg) - .to_compile_error() - .into(); - } - }; - let wit_paths = match resolve_wit_packages(&venue_world.packages) { - Ok(paths) => paths, - Err(msg) => { - return syn::Error::new(proc_macro2::Span::call_site(), msg) - .to_compile_error() - .into(); - } - }; - let inline_world = &venue_world.wit; - - // `body-versions` is a required adapter export; when the adapter - // omits it, declare none. Install asserts the export equals the - // manifest `[venue] body_versions` set. - let body_versions_impl = if defines("body_versions") { - quote! { - fn body_versions() -> ::std::vec::Vec { - <#self_ty>::body_versions() - } - } - } else { - quote! { - fn body_versions() -> ::std::vec::Vec { - ::std::vec::Vec::new() - } - } - }; - - // `init` is a required world export; when the adapter omits it the - // config is bound but unused, so drop it to stay warning-clean. - let init_impl = if defines("init") { - quote! { - fn init( - config: ::std::vec::Vec<(::std::string::String, ::std::string::String)>, - ) -> ::core::result::Result<(), Fault> { - <#self_ty>::init(config) - } - } - } else { - quote! { - fn init( - _config: ::std::vec::Vec<(::std::string::String, ::std::string::String)>, - ) -> ::core::result::Result<(), Fault> { - ::core::result::Result::Ok(()) - } - } - }; - - quote! { - // Anchor a rebuild on the manifest: the emitted world is derived - // from it, so an edited [capabilities] must recompile the adapter. - const _: &[u8] = ::core::include_bytes!(#manifest_path); - - wit_bindgen::generate!({ - inline: #inline_world, - path: [#(#wit_paths),*], - world: "nexum:venue-world/venue-adapter", - generate_all, - }); - - #input - - #[doc(hidden)] - struct __NexumVenueAdapterExport; - - impl Guest for __NexumVenueAdapterExport { - #init_impl - } - - impl exports::videre::venue::adapter::Guest for __NexumVenueAdapterExport { - #body_versions_impl - - fn derive_header( - body: ::std::vec::Vec, - ) -> ::core::result::Result< - videre::types::types::IntentHeader, - videre::types::types::VenueError, - > { - <#self_ty>::derive_header(body) - } - - fn quote( - body: ::std::vec::Vec, - ) -> ::core::result::Result< - videre::types::types::Quotation, - videre::types::types::VenueError, - > { - <#self_ty>::quote(body) - } - - fn submit( - body: ::std::vec::Vec, - ) -> ::core::result::Result< - videre::types::types::SubmitOutcome, - videre::types::types::VenueError, - > { - <#self_ty>::submit(body) - } - - fn status( - receipt: ::std::vec::Vec, - ) -> ::core::result::Result< - videre::types::types::IntentStatus, - videre::types::types::VenueError, - > { - <#self_ty>::status(receipt) - } - - fn cancel( - receipt: ::std::vec::Vec, - ) -> ::core::result::Result<(), videre::types::types::VenueError> { - <#self_ty>::cancel(receipt) - } - } - - export!(__NexumVenueAdapterExport); - } - .into() -} - /// Whether a type is a plain named path (`Foo`), the only shape a module /// export type may take. fn is_plain_type(ty: &Type) -> bool { matches!(ty, Type::Path(tp) if tp.qself.is_none()) } -/// Read the consuming crate's `module.toml` and return its declared -/// capability names alongside the manifest path (for the rebuild -/// anchor). Shared by the module and venue worlds, which differ only in -/// how they turn the declarations into a world. -fn read_manifest_capabilities(attribute: &str) -> Result<(String, Vec), String> { - let manifest_path = manifest_dir()?.join("module.toml"); - let text = std::fs::read_to_string(&manifest_path).map_err(|e| { - format!( - "could not read {} ({e}); {attribute} derives the component's WIT world from the \ - manifest's [capabilities] section, so the manifest must sit next to Cargo.toml", - manifest_path.display() - ) - })?; - let declared = world::manifest_capabilities(&text) - .map_err(|e| format!("{}: {e}", manifest_path.display()))?; - Ok((manifest_path.to_string_lossy().into_owned(), declared)) -} - /// The consuming crate's manifest directory, the root every crate-local /// lookup starts from. fn manifest_dir() -> Result { @@ -529,36 +261,37 @@ fn manifest_dir() -> Result { /// extension rows registered in the nearest ancestor `extensions.toml`. /// Returns the rebuild anchor paths (the manifest, then the registry /// when one exists) alongside the world. -fn derive_module_world() -> Result<(Vec, world::ModuleWorld), String> { - let (manifest_path, declared) = read_manifest_capabilities("#[nexum_sdk::module]")?; +fn derive_module_world() -> Result<(Vec, nexum_world::ModuleWorld), String> { + let manifest_path = manifest_dir()?.join("module.toml"); + let text = std::fs::read_to_string(&manifest_path).map_err(|e| { + format!( + "could not read {} ({e}); #[nexum_sdk::module] derives the component's WIT world \ + from the manifest's [capabilities] section, so the manifest must sit next to \ + Cargo.toml", + manifest_path.display() + ) + })?; + let declared = nexum_world::manifest_capabilities(&text) + .map_err(|e| format!("{}: {e}", manifest_path.display()))?; + let manifest_path = manifest_path.to_string_lossy().into_owned(); + let mut anchors = vec![manifest_path.clone()]; - let extensions = match world::find_extensions_manifest(&manifest_dir()?) { + let extensions = match nexum_world::find_extensions_manifest(&manifest_dir()?) { None => Vec::new(), Some(registry) => { let text = std::fs::read_to_string(®istry) .map_err(|e| format!("could not read {}: {e}", registry.display()))?; - let rows = world::manifest_extensions(&text) + let rows = nexum_world::manifest_extensions(&text) .map_err(|e| format!("{}: {e}", registry.display()))?; anchors.push(registry.to_string_lossy().into_owned()); rows } }; - let module_world = - world::synthesize(&declared, &extensions).map_err(|e| format!("{manifest_path}: {e}"))?; + let module_world = nexum_world::synthesize(&declared, &extensions) + .map_err(|e| format!("{manifest_path}: {e}"))?; Ok((anchors, module_world)) } -/// Read the consuming crate's `module.toml` and synthesize the -/// per-component venue-adapter world from its `[capabilities]` -/// declarations. Returns the manifest path (for the rebuild anchor) -/// alongside the world. -fn derive_venue_world() -> Result<(String, world::ModuleWorld), String> { - let (manifest_path, declared) = read_manifest_capabilities("#[videre_sdk::venue]")?; - let venue_world = - world::synthesize_venue(&declared).map_err(|e| format!("{manifest_path}: {e}"))?; - Ok((manifest_path, venue_world)) -} - /// Resolve each needed WIT package directory crate-locally (vendored /// `wit/deps/`, then own `wit/`), falling back through /// ancestors for the transitional monorepo layout. diff --git a/crates/nexum-sdk/Cargo.toml b/crates/nexum-sdk/Cargo.toml index 620883bf..a5c53524 100644 --- a/crates/nexum-sdk/Cargo.toml +++ b/crates/nexum-sdk/Cargo.toml @@ -22,7 +22,7 @@ stderr-echo = [] # Re-exported as `nexum_sdk::module`; the proc-macro emits glue that # calls back into this crate (`bind_host_via_wit_bindgen!`, the host # trait seam, the tracing facade). -nexum-macros = { path = "../nexum-macros" } +nexum-module-macros = { path = "../nexum-module-macros" } # Decoder for the opaque status body an `intent-status` event carries; # re-exported as `nexum_sdk::status_body`. nexum-status-body = { path = "../nexum-status-body" } diff --git a/crates/nexum-sdk/src/lib.rs b/crates/nexum-sdk/src/lib.rs index fd487c02..cce15425 100644 --- a/crates/nexum-sdk/src/lib.rs +++ b/crates/nexum-sdk/src/lib.rs @@ -127,8 +127,8 @@ /// Generate the per-cdylib module glue (wit-bindgen, host adapter, /// `Guest`/`on-event` dispatch, `export!`) from an `impl` block of named -/// handlers. See [`nexum_macros::module`]. -pub use nexum_macros::module; +/// handlers. See [`nexum_module_macros::module`]. +pub use nexum_module_macros::module; pub mod address; pub mod chain; diff --git a/crates/videre-macros/Cargo.toml b/crates/videre-macros/Cargo.toml new file mode 100644 index 00000000..0b65cbde --- /dev/null +++ b/crates/videre-macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "videre-macros" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Proc-macro glue for videre venue adapters: #[venue] emits the per-cdylib wit-bindgen and adapter export; derive(IntentBody) emits the versioned body codec." + +[lib] +proc-macro = true + +[lints] +workspace = true + +[dependencies] +nexum-world = { path = "../nexum-world" } +proc-macro2.workspace = true +quote.workspace = true +syn = { workspace = true, features = ["full"] } diff --git a/crates/nexum-macros/src/intent_body.rs b/crates/videre-macros/src/intent_body.rs similarity index 100% rename from crates/nexum-macros/src/intent_body.rs rename to crates/videre-macros/src/intent_body.rs diff --git a/crates/videre-macros/src/lib.rs b/crates/videre-macros/src/lib.rs new file mode 100644 index 00000000..feb1b620 --- /dev/null +++ b/crates/videre-macros/src/lib.rs @@ -0,0 +1,311 @@ +//! Proc-macro glue for videre venue adapters. +//! +//! [`venue`] emits the per-cdylib wit-bindgen and `export!` for a +//! per-component venue-adapter world exporting the +//! `videre:venue/adapter` face and importing only the manifest's +//! declared scoped transport. +//! +//! [`derive@IntentBody`] implements the venue SDK's versioned body codec +//! over a per-venue version enum. +//! +//! The module-side macro (`#[module]`) lives in `nexum-module-macros`. +//! +//! Consumers reach these through the SDK re-exports +//! (`videre_sdk::venue`, `videre_sdk::IntentBody`) rather than +//! depending on this crate directly. + +mod intent_body; +mod world; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, ImplItem, ItemImpl, Type}; + +/// Derive the venue SDK's `IntentBody` codec on the outer per-venue +/// version enum: one newtype variant per published body version, each +/// payload a borsh type. +/// +/// The wire form is the borsh enum layout (a one-byte tag, the variant's +/// declaration index, then the borsh payload), so the tag order is the +/// schema: append new versions, never reorder. Decoding an unknown tag +/// fails typedly as `BodyError::UnknownVersion`. +/// +/// Generated code resolves the SDK by crate path, so use the +/// `videre_sdk::IntentBody` re-export with `videre-sdk` as a +/// direct dependency. +#[proc_macro_derive(IntentBody)] +pub fn derive_intent_body(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + intent_body::expand(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// The associated functions the `videre:venue/adapter` face mandates. A +/// venue adapter must define all five; `init` is separate (a no-op when +/// absent, exactly as in a module). +const VENUE_EXPORTS: [&str; 5] = ["derive_header", "quote", "submit", "status", "cancel"]; + +/// Generate the per-cdylib glue for a venue adapter. +/// +/// Apply to an inherent `impl` block whose associated functions are the +/// adapter face: `derive_header`, `quote`, `submit`, `status`, `cancel` +/// (all required, from `videre:venue/adapter`), plus an optional `init` +/// (absent means a no-op) and an optional `body_versions` (absent +/// declares none). Each takes and returns the per-cdylib +/// wit-bindgen payloads for its signature. The macro reads the crate's +/// `module.toml`, synthesizes a per-component world exporting the +/// adapter face and importing exactly the manifest's declared scoped +/// transport, then emits `wit_bindgen::generate!`, the `Guest` impls +/// wiring the world to the adapter's functions, and `export!` around the +/// untouched impl. So the built component imports what the manifest +/// declares and nothing else, retiring the toolchain-elision dependency +/// on the venue side. +/// +/// A venue's capabilities are scoped transport only: an undeclared +/// capability's bindings do not exist (using one is a compile error), +/// and a capability outside the venue-permitted set (`chain`, +/// `messaging`, `http`) is rejected at expansion. +/// +/// The same crate-root resolution invariants as `#[module]` apply: the +/// wit-bindgen output lands at the module crate root (so the emitted +/// glue resolves `Guest`, `Fault`, and the `nexum::*`/`videre::*` type modules +/// there), the consuming crate must declare `wit-bindgen` as a direct +/// dependency, and the crate root must not shadow std prelude names. +#[proc_macro_attribute] +pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + return syn::Error::new( + proc_macro2::Span::call_site(), + "#[videre_sdk::venue] takes no arguments", + ) + .to_compile_error() + .into(); + } + + let input = syn::parse_macro_input!(item as ItemImpl); + + let self_ty = &input.self_ty; + if !is_plain_type(self_ty) { + return syn::Error::new_spanned( + self_ty, + "#[videre_sdk::venue] must be applied to an inherent impl of a named type", + ) + .to_compile_error() + .into(); + } + if let Some((_, trait_path, _)) = &input.trait_ { + return syn::Error::new_spanned( + trait_path, + "#[videre_sdk::venue] must be applied to an inherent impl, not a trait impl", + ) + .to_compile_error() + .into(); + } + if !input.generics.params.is_empty() { + return syn::Error::new_spanned( + &input.generics, + "#[videre_sdk::venue] must be applied to a non-generic impl", + ) + .to_compile_error() + .into(); + } + + let defines = |name: &str| { + input + .items + .iter() + .any(|item| matches!(item, ImplItem::Fn(f) if f.sig.ident == name)) + }; + let missing: Vec<&str> = VENUE_EXPORTS + .into_iter() + .filter(|name| !defines(name)) + .collect(); + if !missing.is_empty() { + return syn::Error::new_spanned( + self_ty, + format!( + "#[videre_sdk::venue] requires the adapter face; this impl is missing {:?}. \ + Define all of `derive_header`, `quote`, `submit`, `status`, `cancel` (plus an \ + optional `init`)", + missing + ), + ) + .to_compile_error() + .into(); + } + + let (manifest_path, venue_world) = match derive_venue_world() { + Ok(parts) => parts, + Err(msg) => { + return syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into(); + } + }; + let wit_paths = match resolve_wit_packages(&venue_world.packages) { + Ok(paths) => paths, + Err(msg) => { + return syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into(); + } + }; + let inline_world = &venue_world.wit; + + // `body-versions` is a required adapter export; when the adapter + // omits it, declare none. Install asserts the export equals the + // manifest `[venue] body_versions` set. + let body_versions_impl = if defines("body_versions") { + quote! { + fn body_versions() -> ::std::vec::Vec { + <#self_ty>::body_versions() + } + } + } else { + quote! { + fn body_versions() -> ::std::vec::Vec { + ::std::vec::Vec::new() + } + } + }; + + // `init` is a required world export; when the adapter omits it the + // config is bound but unused, so drop it to stay warning-clean. + let init_impl = if defines("init") { + quote! { + fn init( + config: ::std::vec::Vec<(::std::string::String, ::std::string::String)>, + ) -> ::core::result::Result<(), Fault> { + <#self_ty>::init(config) + } + } + } else { + quote! { + fn init( + _config: ::std::vec::Vec<(::std::string::String, ::std::string::String)>, + ) -> ::core::result::Result<(), Fault> { + ::core::result::Result::Ok(()) + } + } + }; + + quote! { + // Anchor a rebuild on the manifest: the emitted world is derived + // from it, so an edited [capabilities] must recompile the adapter. + const _: &[u8] = ::core::include_bytes!(#manifest_path); + + wit_bindgen::generate!({ + inline: #inline_world, + path: [#(#wit_paths),*], + world: "nexum:venue-world/venue-adapter", + generate_all, + }); + + #input + + #[doc(hidden)] + struct __NexumVenueAdapterExport; + + impl Guest for __NexumVenueAdapterExport { + #init_impl + } + + impl exports::videre::venue::adapter::Guest for __NexumVenueAdapterExport { + #body_versions_impl + + fn derive_header( + body: ::std::vec::Vec, + ) -> ::core::result::Result< + videre::types::types::IntentHeader, + videre::types::types::VenueError, + > { + <#self_ty>::derive_header(body) + } + + fn quote( + body: ::std::vec::Vec, + ) -> ::core::result::Result< + videre::types::types::Quotation, + videre::types::types::VenueError, + > { + <#self_ty>::quote(body) + } + + fn submit( + body: ::std::vec::Vec, + ) -> ::core::result::Result< + videre::types::types::SubmitOutcome, + videre::types::types::VenueError, + > { + <#self_ty>::submit(body) + } + + fn status( + receipt: ::std::vec::Vec, + ) -> ::core::result::Result< + videre::types::types::IntentStatus, + videre::types::types::VenueError, + > { + <#self_ty>::status(receipt) + } + + fn cancel( + receipt: ::std::vec::Vec, + ) -> ::core::result::Result<(), videre::types::types::VenueError> { + <#self_ty>::cancel(receipt) + } + } + + export!(__NexumVenueAdapterExport); + } + .into() +} + +/// Whether a type is a plain named path (`Foo`), the only shape a module +/// export type may take. +fn is_plain_type(ty: &Type) -> bool { + matches!(ty, Type::Path(tp) if tp.qself.is_none()) +} + +/// The consuming crate's manifest directory, the root every crate-local +/// lookup starts from. +fn manifest_dir() -> Result { + std::env::var("CARGO_MANIFEST_DIR") + .map(std::path::PathBuf::from) + .map_err(|_| "CARGO_MANIFEST_DIR is not set".to_string()) +} + +/// Read the consuming crate's `module.toml` and synthesize the +/// per-component venue-adapter world from its `[capabilities]` +/// declarations. Returns the manifest path (for the rebuild anchor) +/// alongside the world. +fn derive_venue_world() -> Result<(String, nexum_world::ModuleWorld), String> { + let manifest_path = manifest_dir()?.join("module.toml"); + let text = std::fs::read_to_string(&manifest_path).map_err(|e| { + format!( + "could not read {} ({e}); #[videre_sdk::venue] derives the component's WIT world \ + from the manifest's [capabilities] section, so the manifest must sit next to \ + Cargo.toml", + manifest_path.display() + ) + })?; + let declared = nexum_world::manifest_capabilities(&text) + .map_err(|e| format!("{}: {e}", manifest_path.display()))?; + let manifest_path = manifest_path.to_string_lossy().into_owned(); + let venue_world = + world::synthesize_venue(&declared).map_err(|e| format!("{manifest_path}: {e}"))?; + Ok((manifest_path, venue_world)) +} + +/// Resolve each needed WIT package directory crate-locally (vendored +/// `wit/deps/`, then own `wit/`), falling back through +/// ancestors for the transitional monorepo layout. +fn resolve_wit_packages(packages: &[String]) -> Result, String> { + Ok( + nexum_world::resolve_wit_packages(&manifest_dir()?, packages)? + .into_iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect(), + ) +} diff --git a/crates/nexum-macros/src/world.rs b/crates/videre-macros/src/world.rs similarity index 93% rename from crates/nexum-macros/src/world.rs rename to crates/videre-macros/src/world.rs index 82de0717..30a059ad 100644 --- a/crates/nexum-macros/src/world.rs +++ b/crates/videre-macros/src/world.rs @@ -1,11 +1,8 @@ -//! World wiring for the macros: the venue-adapter world synthesis. The -//! module world synthesis, the core capability table, and the extension -//! registry parsing (`extensions.toml`, the composition root's data) -//! live in `nexum-world`, so no crate here carries a downstream name. +//! World wiring for the venue macro: the venue-adapter world synthesis. +//! The module world synthesis, the core capability table, and the +//! extension registry parsing live in `nexum-world`. -pub use nexum_world::{ - ModuleWorld, find_extensions_manifest, manifest_capabilities, manifest_extensions, synthesize, -}; +pub use nexum_world::ModuleWorld; /// Capabilities a venue adapter may import. A venue speaks one venue's /// protocol over scoped transport and nothing else: chain RPC, diff --git a/crates/videre-sdk/Cargo.toml b/crates/videre-sdk/Cargo.toml index 674804aa..47b77c88 100644 --- a/crates/videre-sdk/Cargo.toml +++ b/crates/videre-sdk/Cargo.toml @@ -21,9 +21,9 @@ workspace = true # the derive's generated code so an adapter crate needs no direct borsh # declaration unless its payloads derive the borsh traits themselves. borsh.workspace = true -# Source of the `IntentBody` derive, re-exported at the crate root next -# to the trait it implements. -nexum-macros = { path = "../nexum-macros" } +# Source of `#[venue]` and the `IntentBody` derive, re-exported at the +# crate root next to the trait they implement against. +videre-macros = { path = "../videre-macros" } # Host-neutral SDK layer this crate builds on: the `ChainHost` seam the # chain wrapper implements, the shared `Fault` vocabulary, and the # wasi:http `fetch` surface re-exported as `transport::http`. diff --git a/crates/videre-sdk/src/lib.rs b/crates/videre-sdk/src/lib.rs index c268e7ca..e4d1da82 100644 --- a/crates/videre-sdk/src/lib.rs +++ b/crates/videre-sdk/src/lib.rs @@ -71,20 +71,20 @@ pub use client::{ClientError, IntentClient, Quoted, VenueClient, VenueId}; pub use faults::VenueFault; pub use keeper::{Keeper, Sweep, SweepReport}; /// Derive [`IntentBody`] on the outer per-venue version enum. See -/// [`nexum_macros::IntentBody`]. -pub use nexum_macros::IntentBody; +/// [`videre_macros::IntentBody`]. +pub use videre_macros::IntentBody; /// Emit the per-cdylib export glue and per-component world for a venue /// adapter. Apply to an inherent `impl` of the adapter face /// (`derive_header`, `quote`, `submit`, `status`, `cancel`, plus an /// optional `init`); the built component imports exactly the manifest's declared -/// scoped transport. See [`nexum_macros::venue`]. +/// scoped transport. See [`videre_macros::venue`]. /// /// The self-contained per-cdylib alternative to /// [`export_venue_adapter!`]: that macro exports through this crate's /// shared blanket-world bindgen (chain and messaging always imported, /// relying on toolchain elision), whereas `#[venue]` derives a narrowed /// world from the manifest and generates its own bindings. -pub use nexum_macros::venue; +pub use videre_macros::venue; /// The intent ontology at its plain spellings: the types the /// [`VenueAdapter`] face and the client core speak. diff --git a/docs/05-sdk-design.md b/docs/05-sdk-design.md index 52ed08d0..1722ca1a 100755 --- a/docs/05-sdk-design.md +++ b/docs/05-sdk-design.md @@ -12,7 +12,8 @@ For the architectural decision behind the host-trait seam that the module-author persona builds on, see [ADR-0009](adr/0009-host-trait-surface.md). For the rustdoc-level API reference (the source of truth once you are writing module code), see [`sdk.md`](sdk.md) and the rustdoc under -`crates/nexum-sdk/`, `crates/shepherd-sdk/`, and `crates/nexum-macros/`. +`crates/nexum-sdk/`, `crates/shepherd-sdk/`, and +`crates/nexum-module-macros/`. ## The two personas @@ -38,7 +39,8 @@ things from the SDK: tree yet. See [Venue-adapter persona (planned)](#venue-adapter-persona-planned) below for the shape of the plan. -Both personas share one proc-macro crate, `nexum-macros`, and the +Each persona has its own proc-macro crate (`nexum-module-macros` for +modules, `videre-macros` for venue adapters) and both share the same host-trait philosophy: guest code is written against small Rust traits that mirror the WIT interfaces one-for-one, so strategy logic can be unit-tested against an in-memory mock without a `wasm32-wasip2` @@ -52,7 +54,7 @@ toolchain or a running wasmtime instance. nexum-sdk/ ├── Cargo.toml └── src/ - ├── lib.rs # crate docs, `pub use nexum_macros::module` + ├── lib.rs # crate docs, `pub use nexum_module_macros::module` ├── prelude.rs # alloy primitive re-exports (Address, B256, Bytes, U256, keccak256) ├── host.rs # ChainHost / LocalStoreHost / LoggingHost + supertrait Host; Fault, ChainError, RpcError ├── wit_bindgen_macro.rs # bind_host_via_wit_bindgen! - generates WitBindgenHost + converters @@ -65,7 +67,7 @@ nexum-sdk/ ├── tracing.rs # guest tracing facade + panic hook over a LogSink seam └── proptests.rs # cfg(test) property tests (not part of the public surface) -nexum-macros/ +nexum-module-macros/ ├── Cargo.toml # proc-macro = true └── src/ └── lib.rs # #[module] attribute macro @@ -153,7 +155,7 @@ layers the `CowApiHost` impl on top of the same `WitBindgenHost` type. ### The `#[nexum::module]` macro -`nexum-macros` ships one attribute macro, re-exported as +`nexum-module-macros` ships one attribute macro, re-exported as `nexum_sdk::module`. Apply it to an inherent `impl` block whose methods are named event handlers - `init`, `on_block`, `on_chain_logs`, `on_tick`, `on_message` - and the macro reads the @@ -268,7 +270,7 @@ The planned shape: CoW Protocol's intent-body codec and become the eventual home for the CoW helpers `shepherd-sdk::cow` carries today, once the clean break happens. -- **`#[nexum::venue]`** - a second attribute macro in `nexum-macros`, +- **`#[nexum::venue]`** - a second attribute macro, in `videre-macros`, parallel to `#[nexum::module]`: it would emit the per-cdylib export glue for an adapter and a per-component world matching the manifest's declared capabilities (retiring the import-elision diff --git a/docs/sdk.md b/docs/sdk.md index b27045af..c4f07f2f 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -13,13 +13,13 @@ site under `target/doc/nexum_sdk/` and `target/doc/shepherd_sdk/`, generated by: ```sh -RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk -p nexum-sdk -p nexum-macros --no-deps --open +RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk -p nexum-sdk -p nexum-module-macros --no-deps --open ``` ## Authoring a module Modules are authored with the `#[nexum_sdk::module]` attribute -(re-exported from `nexum-macros`). Apply it to an inherent `impl` +(re-exported from `nexum-module-macros`). Apply it to an inherent `impl` block whose associated functions are the named event handlers - `init`, `on_block`, `on_chain_logs`, `on_tick`, `on_message` - each taking its wit-bindgen payload and returning `Result<(), Fault>`. @@ -28,7 +28,7 @@ The macro generates the rest of the per-cdylib glue: the adapter, a `Guest` impl whose `on_event` dispatches to whichever handlers are present (absent handlers no-op), and `export!`. See [doc 05](05-sdk-design.md#the-nexummodule-macro) for a worked -example and the `nexum-macros` rustdoc for the fine print. +example and the `nexum-module-macros` rustdoc for the fine print. ## Supported host capabilities