diff --git a/crates/cow-venue/src/client.rs b/crates/cow-venue/src/client.rs index 9dd86963..011b229e 100644 --- a/crates/cow-venue/src/client.rs +++ b/crates/cow-venue/src/client.rs @@ -10,7 +10,7 @@ use alloc::string::String; -use videre_sdk::client::{HostVenues, Venue, VenueClient, VenueId}; +use videre_sdk::client::{HostVenues, Venue, VenueClient}; use videre_sdk::keeper::submission_key; use videre_sdk::{BodyError, IntentBody as _}; @@ -22,10 +22,9 @@ use crate::body::CowIntentBody; #[derive(Clone, Copy, Debug)] pub struct CowVenue; -impl Venue for CowVenue { - const ID: VenueId = VenueId::from_static("cow"); - type Body = CowIntentBody; -} +// The id is held to `module.toml`'s `[module] name` at expansion. +#[videre_sdk::venue(id = "cow", body = CowIntentBody)] +impl Venue for CowVenue {} /// A typed client pre-bound to the CoW venue: callers cannot mis-route /// or submit a foreign body. @@ -49,7 +48,7 @@ mod tests { use std::cell::RefCell; use std::rc::Rc; - use videre_sdk::client::VenueTransport; + use videre_sdk::client::{VenueId, VenueTransport}; use videre_sdk::{IntentStatus, Quotation, SubmitOutcome, VenueFault}; use super::*; diff --git a/crates/nexum-world/src/lib.rs b/crates/nexum-world/src/lib.rs index 4a6aae1e..12269b92 100644 --- a/crates/nexum-world/src/lib.rs +++ b/crates/nexum-world/src/lib.rs @@ -316,6 +316,21 @@ pub fn manifest_capabilities(text: &str) -> Result, String> { Ok(names) } +/// Extract the declared `[module] name` from the manifest text, the id +/// the module registers under. Absent or non-string is an error. +pub fn manifest_name(text: &str) -> Result { + let value: toml::Table = text + .parse() + .map_err(|e| format!("module.toml is not valid TOML: {e}"))?; + value + .get("module") + .and_then(|module| module.get("name")) + .ok_or_else(|| "[module].name is missing".to_string())? + .as_str() + .map(str::to_owned) + .ok_or_else(|| "[module].name must be a string".to_string()) +} + /// Extract the declared `[module] kind` from the manifest text, `None` /// when absent (the runtime defaults an absent kind to the worker). pub fn manifest_kind(text: &str) -> Result, String> { diff --git a/crates/videre-macros/src/lib.rs b/crates/videre-macros/src/lib.rs index a9cb5677..7cb0e1b0 100644 --- a/crates/videre-macros/src/lib.rs +++ b/crates/videre-macros/src/lib.rs @@ -22,6 +22,7 @@ mod intent_body; mod keeper; +mod venue_marker; mod world; use proc_macro::TokenStream; @@ -79,19 +80,26 @@ const VENUE_KIND: &str = "venue-adapter"; /// codegen resolves `Guest`, `exports`, and `export!` there), and the /// consuming crate must declare `wit-bindgen` and `videre-sdk` as /// direct dependencies. +/// +/// # Client marker +/// +/// Given arguments (`#[videre_sdk::venue(id = "cow", body = CowBody)]`) +/// the attribute instead fills a client-side `impl Venue for Marker {}`: +/// it emits the `const ID`/`type Body` from the args and, at expansion, +/// asserts the id equals the crate manifest's `[module] name`. No +/// component world is generated, so a keeper linking the client slice +/// never pulls adapter bindgen. This form expands on host and wasm alike +/// and is opt-in: hand-written `Venue` impls keep compiling. #[proc_macro_attribute] pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as ItemImpl); + if !attr.is_empty() { - return syn::Error::new( - proc_macro2::Span::call_site(), - "#[videre_sdk::venue] takes no arguments", - ) - .to_compile_error() - .into(); + return venue_marker::expand(attr.into(), &input) + .unwrap_or_else(syn::Error::into_compile_error) + .into(); } - let input = syn::parse_macro_input!(item as ItemImpl); - let Some((None, trait_path, _)) = &input.trait_ else { return syn::Error::new_spanned( &input.self_ty, diff --git a/crates/videre-macros/src/venue_marker.rs b/crates/videre-macros/src/venue_marker.rs new file mode 100644 index 00000000..315f9d7f --- /dev/null +++ b/crates/videre-macros/src/venue_marker.rs @@ -0,0 +1,127 @@ +//! The client-side `#[videre_sdk::venue(id = "...", body = Type)]` path: +//! fills a `Venue` marker impl and checks the id against `module.toml` +//! at expansion. No component world, so a keeper linking the client +//! slice never pulls adapter bindgen. + +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{ItemImpl, LitStr, Token, Type}; + +/// `id = "cow", body = CowIntentBody`: the venue id and the body schema +/// the marker binds. +struct Args { + id: LitStr, + body: Type, +} + +impl Parse for Args { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut id = None; + let mut body = None; + while !input.is_empty() { + let key: syn::Ident = input.parse()?; + input.parse::()?; + match key.to_string().as_str() { + "id" => id = Some(input.parse()?), + "body" => body = Some(input.parse()?), + other => { + return Err(syn::Error::new( + key.span(), + format!("unknown argument `{other}`, expected `id` or `body`"), + )); + } + } + if input.peek(Token![,]) { + input.parse::()?; + } + } + Ok(Self { + id: id.ok_or_else(|| syn::Error::new(Span::call_site(), "missing `id = \"...\"`"))?, + body: body + .ok_or_else(|| syn::Error::new(Span::call_site(), "missing `body = Type`"))?, + }) + } +} + +/// Expand `#[videre_sdk::venue(id, body)]` on an `impl Venue for Marker +/// {}` block: inject `const ID`/`type Body` from the args and assert the +/// id equals the crate manifest's `[module] name`. +pub fn expand(attr: TokenStream, input: &ItemImpl) -> Result { + let args: Args = syn::parse2(attr)?; + + let Some((None, trait_path, _)) = &input.trait_ else { + return Err(syn::Error::new_spanned( + &input.self_ty, + "#[videre_sdk::venue(id = ..)] must be applied to an `impl Venue for ...` block", + )); + }; + if trait_path + .segments + .last() + .is_none_or(|segment| segment.ident != "Venue") + { + return Err(syn::Error::new_spanned( + trait_path, + "#[videre_sdk::venue(id = ..)] must be applied to an impl of `videre_sdk::client::Venue`", + )); + } + if !input.items.is_empty() { + return Err(syn::Error::new_spanned( + &input.self_ty, + "#[videre_sdk::venue(id = ..)] fills the impl body; leave it empty", + )); + } + if !input.generics.params.is_empty() { + return Err(syn::Error::new_spanned( + &input.generics, + "#[videre_sdk::venue(id = ..)] must be applied to a non-generic impl", + )); + } + + let self_ty = &input.self_ty; + let id = &args.id; + let body = &args.body; + + // Read the crate manifest at expansion and hold the id to its + // registered name, alloy-`sol!`-style. Any mismatch is a + // compile_error, so the marker and the adapter it types cannot drift. + let manifest_path = manifest_id_check(id)?; + + Ok(quote! { + // Rebuild anchor: an edited `[module] name` re-runs the check. + const _: &[u8] = ::core::include_bytes!(#manifest_path); + + impl #trait_path for #self_ty { + const ID: ::videre_sdk::client::VenueId = + ::videre_sdk::client::VenueId::from_static(#id); + type Body = #body; + } + }) +} + +/// Assert `id` equals the crate manifest's `[module] name`, returning the +/// manifest path for the rebuild anchor. +fn manifest_id_check(id: &LitStr) -> Result { + let err = |msg: String| syn::Error::new(id.span(), msg); + let manifest_path = nexum_world::manifest_dir() + .map_err(&err)? + .join("module.toml"); + let text = std::fs::read_to_string(&manifest_path).map_err(|e| { + err(format!( + "could not read {} ({e}); #[videre_sdk::venue(id = ..)] holds the id to the \ + manifest's [module] name, so the manifest must sit next to Cargo.toml", + manifest_path.display() + )) + })?; + let name = nexum_world::manifest_name(&text) + .map_err(|e| err(format!("{}: {e}", manifest_path.display())))?; + if name != id.value() { + return Err(err(format!( + "{}: venue id {:?} disagrees with [module] name {name:?}", + manifest_path.display(), + id.value(), + ))); + } + Ok(manifest_path.to_string_lossy().into_owned()) +}