Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions crates/cow-venue/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _};

Expand All @@ -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.
Expand All @@ -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::*;
Expand Down
15 changes: 15 additions & 0 deletions crates/nexum-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,21 @@ pub fn manifest_capabilities(text: &str) -> Result<Vec<String>, 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<String, String> {
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<Option<String>, String> {
Expand Down
24 changes: 16 additions & 8 deletions crates/videre-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

mod intent_body;
mod keeper;
mod venue_marker;
mod world;

use proc_macro::TokenStream;
Expand Down Expand Up @@ -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,
Expand Down
127 changes: 127 additions & 0 deletions crates/videre-macros/src/venue_marker.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
let mut id = None;
let mut body = None;
while !input.is_empty() {
let key: syn::Ident = input.parse()?;
input.parse::<Token![=]>()?;
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::<Token![,]>()?;
}
}
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<TokenStream, syn::Error> {
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<String, syn::Error> {
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())
}
Loading