sausage-plugin part 2: import! with typed clients (sausage_interface::client_rs), TCP endpoints with rpc.endpoint, examples
Part 1 fixes: complete stdout writes, buffered stdin, Rust type names in schemas, enum variant docs, -32603 for serde errors in handlers, crate docs and modules. End-to-end check of a generated consumer through a fake core.
This commit is contained in:
@@ -8,6 +8,8 @@ description = "Macros of the Sausage plugin SDK. Use them through `sausage-plugi
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
sausage-interface = { path = "../../interface" }
|
||||
serde_json = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
|
||||
+72
-3
@@ -47,6 +47,7 @@ fn serde_subset(attrs: &[Attribute]) -> syn::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Derive serde and Draft 7 schemas for a documented type in the supported subset.
|
||||
#[proc_macro_attribute]
|
||||
pub fn schema(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(input as Item);
|
||||
@@ -110,12 +111,14 @@ pub fn schema(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
})();
|
||||
result.unwrap_or_else(syn::Error::into_compile_error).into()
|
||||
}
|
||||
/// Generate interface dispatch and discovery from documented methods.
|
||||
#[proc_macro_attribute]
|
||||
pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut item = parse_macro_input!(input as ItemImpl);
|
||||
let args = proc_macro2::TokenStream::from(args);
|
||||
let result = (|| -> syn::Result<proc_macro2::TokenStream> {
|
||||
let mut grace = None;
|
||||
let mut endpoints: Vec<(syn::Ident, syn::Type)> = vec![];
|
||||
let parser = syn::meta::parser(|m| {
|
||||
if m.path.is_ident("grace_seconds") {
|
||||
grace = Some(m.value()?.parse::<syn::LitInt>()?);
|
||||
@@ -123,7 +126,15 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
} else if m.path.is_ident("endpoints") {
|
||||
let content;
|
||||
syn::parenthesized!(content in m.input);
|
||||
let _ = content.parse::<proc_macro2::TokenStream>()?;
|
||||
while !content.is_empty() {
|
||||
let name = content.parse()?;
|
||||
let _: syn::Token![=] = content.parse()?;
|
||||
let ty = content.parse()?;
|
||||
endpoints.push((name, ty));
|
||||
if !content.is_empty() {
|
||||
let _: syn::Token![,] = content.parse()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(m.error("unknown RPC option"))
|
||||
@@ -179,7 +190,10 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
));
|
||||
}
|
||||
param = Some(p.ty.as_ref());
|
||||
args.push(quote!(::sausage_plugin::serde_json::from_value(params)?));
|
||||
args.push(quote!(
|
||||
::sausage_plugin::serde_json::from_value(params)
|
||||
.map_err(|e| ::sausage_plugin::Error::new(-32602, e.to_string()))?
|
||||
));
|
||||
}
|
||||
}
|
||||
let await_ = f.sig.asyncness.map(|_| quote!(.await));
|
||||
@@ -225,22 +239,77 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
}
|
||||
let grace =
|
||||
grace.map(|n| quote!(doc["x-sausage-grace"]=::sausage_plugin::serde_json::json!(#n);));
|
||||
let names: Vec<_> = endpoints.iter().map(|(n, _)| n.to_string()).collect();
|
||||
let types: Vec<_> = endpoints.iter().map(|(_, t)| t).collect();
|
||||
Ok(quote! {#item impl ::sausage_plugin::Interface for #ty {
|
||||
fn discover(&self)->::sausage_plugin::serde_json::Value {let mut doc=::sausage_plugin::document(env!("CARGO_PKG_NAME"),env!("CARGO_PKG_VERSION")); #(#entries)* #grace doc}
|
||||
fn endpoint_types(&self)->Vec<(::std::any::TypeId,String)>{vec![#((::std::any::TypeId::of::<#types>(),#names.into())),*]}
|
||||
fn methods_document()->::sausage_plugin::serde_json::Value {let mut doc=::sausage_plugin::document(env!("CARGO_PKG_NAME"),env!("CARGO_PKG_VERSION")); #(#entries)* #grace ::sausage_plugin::finish_document(doc)}
|
||||
fn describe()->::sausage_plugin::serde_json::Value {let mut doc=<Self as ::sausage_plugin::Interface>::methods_document(); #(let endpoint=<#types as ::sausage_plugin::Interface>::methods_document();doc["x-sausage-endpoints"][#names]=::sausage_plugin::serde_json::json!({"methods":endpoint["methods"]});doc["components"]["schemas"].as_object_mut().unwrap().extend(endpoint["components"]["schemas"].as_object().unwrap().clone());)* doc}
|
||||
fn discover(&self)->::sausage_plugin::serde_json::Value {<Self as ::sausage_plugin::Interface>::describe()}
|
||||
fn dispatch<'a>(&'a self, method:&'a str, params: ::sausage_plugin::serde_json::Value,ctx:&'a ::sausage_plugin::Ctx)->::std::pin::Pin<Box<dyn ::std::future::Future<Output=::sausage_plugin::Result<::sausage_plugin::serde_json::Value>>+'a>> {Box::pin(async move {if !params.is_object() {return Err(::sausage_plugin::Error::new(-32602,"params must be an object"));} match method {#(#arms,)* _=>Err(::sausage_plugin::Error::new(-32601,"method not found"))}})}
|
||||
#stop
|
||||
}})
|
||||
})();
|
||||
result.unwrap_or_else(syn::Error::into_compile_error).into()
|
||||
}
|
||||
/// Generate a private client from `imports/<provider>.json`.
|
||||
#[proc_macro]
|
||||
pub fn import(input: TokenStream) -> TokenStream {
|
||||
let name = parse_macro_input!(input as syn::LitStr);
|
||||
let result = (|| -> Result<proc_macro2::TokenStream, String> {
|
||||
let path = std::path::PathBuf::from(
|
||||
std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?,
|
||||
)
|
||||
.join("imports")
|
||||
.join(format!("{}.json", name.value()));
|
||||
let contents =
|
||||
std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let doc = serde_json::from_str(&contents).map_err(|e| e.to_string())?;
|
||||
let code = sausage_interface::client_rs(&name.value(), &doc)?;
|
||||
let generated: proc_macro2::TokenStream = code
|
||||
.parse()
|
||||
.map_err(|e: proc_macro2::LexError| e.to_string())?;
|
||||
fn source_spans(
|
||||
tokens: proc_macro2::TokenStream,
|
||||
span: proc_macro2::Span,
|
||||
) -> proc_macro2::TokenStream {
|
||||
tokens
|
||||
.into_iter()
|
||||
.map(|mut token| {
|
||||
if let proc_macro2::TokenTree::Group(group) = &token {
|
||||
let mut replacement = proc_macro2::Group::new(
|
||||
group.delimiter(),
|
||||
source_spans(group.stream(), span),
|
||||
);
|
||||
replacement.set_span(span);
|
||||
token = proc_macro2::TokenTree::Group(replacement);
|
||||
}
|
||||
token.set_span(span);
|
||||
token
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let generated = source_spans(generated, name.span());
|
||||
let path = path.to_str().ok_or("non-UTF8 import path")?;
|
||||
Ok(
|
||||
quote! {#generated ::sausage_plugin::inventory::submit! {::sausage_plugin::Import {name:#name,document:include_str!(#path)}}},
|
||||
)
|
||||
})();
|
||||
result
|
||||
.unwrap_or_else(|e| syn::Error::new(name.span(), e).into_compile_error())
|
||||
.into()
|
||||
}
|
||||
/// Mark the method that handles `rpc.retired` after an upgrade.
|
||||
#[proc_macro_attribute]
|
||||
pub fn on_retired(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
input
|
||||
}
|
||||
/// Mark the hook that runs at stdin EOF without a live call transport.
|
||||
#[proc_macro_attribute]
|
||||
pub fn on_stop(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
input
|
||||
}
|
||||
/// Run an async test with the SDK runtime and the available core transport.
|
||||
#[proc_macro_attribute]
|
||||
pub fn test(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut f = parse_macro_input!(input as syn::ItemFn);
|
||||
|
||||
Reference in New Issue
Block a user