mirror of
https://github.com/Start9Labs/rpc-toolkit.git
synced 2026-03-26 02:11:56 +00:00
create rpc toolkit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
1201
Cargo.lock
generated
Normal file
1201
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
Cargo.toml
Normal file
2
Cargo.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["rpc-toolkit", "rpc-toolkit-macro", "rpc-toolkit-macro-internals"]
|
||||||
2
rpc-toolkit-macro-internals/.gitignore
vendored
Normal file
2
rpc-toolkit-macro-internals/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
12
rpc-toolkit-macro-internals/Cargo.toml
Normal file
12
rpc-toolkit-macro-internals/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||||
|
edition = "2018"
|
||||||
|
name = "rpc-toolkit-macro-internals"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0.26"
|
||||||
|
quote = "1.0.9"
|
||||||
|
syn = { version = "1.0.68", features = ["fold"] }
|
||||||
824
rpc-toolkit-macro-internals/src/command/build.rs
Normal file
824
rpc-toolkit-macro-internals/src/command/build.rs
Normal file
@@ -0,0 +1,824 @@
|
|||||||
|
use super::parse::*;
|
||||||
|
use super::*;
|
||||||
|
use proc_macro2::*;
|
||||||
|
use quote::*;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use syn::{
|
||||||
|
fold::Fold,
|
||||||
|
punctuated::Punctuated,
|
||||||
|
spanned::Spanned,
|
||||||
|
token::{Comma, Where},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn build_app(name: LitStr, opt: &mut Options, params: &mut [ParamType]) -> TokenStream {
|
||||||
|
let about = opt.common().about.clone().into_iter();
|
||||||
|
let (subcommand, subcommand_required) = if let Options::Parent(opt) = opt {
|
||||||
|
(
|
||||||
|
opt.subcommands
|
||||||
|
.iter()
|
||||||
|
.map(|subcmd| {
|
||||||
|
let mut path = subcmd.clone();
|
||||||
|
path.segments.last_mut().unwrap().arguments = PathArguments::None;
|
||||||
|
path
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
opt.self_impl.is_none(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), false)
|
||||||
|
};
|
||||||
|
let arg = params
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|param| {
|
||||||
|
if let ParamType::Arg(arg) = param {
|
||||||
|
if arg.stdin {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = arg.name.clone().unwrap();
|
||||||
|
let name_str = LitStr::new(&name.to_string(), name.span());
|
||||||
|
let help = arg.help.clone().into_iter();
|
||||||
|
let short = arg.short.clone().into_iter();
|
||||||
|
let long = arg.long.clone().into_iter();
|
||||||
|
let mut modifications = TokenStream::default();
|
||||||
|
let ty_span = arg.ty.span();
|
||||||
|
if let Type::Path(p) = &mut arg.ty {
|
||||||
|
if p.path.is_ident("bool")
|
||||||
|
&& arg.parse.is_none()
|
||||||
|
&& (arg.short.is_some() || arg.long.is_some())
|
||||||
|
{
|
||||||
|
arg.check_is_present = true;
|
||||||
|
modifications.extend(quote_spanned! { ty_span =>
|
||||||
|
arg = arg.takes_value(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
modifications.extend(quote_spanned! { ty_span =>
|
||||||
|
arg = arg.takes_value(true);
|
||||||
|
});
|
||||||
|
if p.path.segments.last().unwrap().ident != "Option" {
|
||||||
|
modifications.extend(quote_spanned! { ty_span =>
|
||||||
|
arg = arg.required(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
arg.optional = true;
|
||||||
|
modifications.extend(quote_spanned! { ty_span =>
|
||||||
|
arg = arg.required(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(quote! {
|
||||||
|
{
|
||||||
|
let mut arg = rpc_toolkit_prelude::Arg::with_name(#name_str);
|
||||||
|
#(
|
||||||
|
arg = arg.help(#help);
|
||||||
|
)*
|
||||||
|
#(
|
||||||
|
arg = arg.short(#short);
|
||||||
|
)*
|
||||||
|
#(
|
||||||
|
arg = arg.long(#long);
|
||||||
|
)*
|
||||||
|
#modifications
|
||||||
|
|
||||||
|
arg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let required = LitBool::new(subcommand_required, Span::call_site());
|
||||||
|
quote! {
|
||||||
|
pub fn build_app() -> rpc_toolkit_prelude::App<'static, 'static> {
|
||||||
|
let mut app = rpc_toolkit_prelude::App::new(#name);
|
||||||
|
#(
|
||||||
|
app = app.about(#about);
|
||||||
|
)*
|
||||||
|
#(
|
||||||
|
app = app.arg(#arg);
|
||||||
|
)*
|
||||||
|
#(
|
||||||
|
app = app.subcommand(#subcommand::build_app());
|
||||||
|
)*
|
||||||
|
if #required {
|
||||||
|
app = app.setting(rpc_toolkit_prelude::AppSettings::SubcommandRequired);
|
||||||
|
}
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GenericFilter<'a> {
|
||||||
|
src: &'a Generics,
|
||||||
|
lifetimes: HashSet<Lifetime>,
|
||||||
|
types: HashSet<Ident>,
|
||||||
|
}
|
||||||
|
impl<'a> GenericFilter<'a> {
|
||||||
|
fn new(src: &'a Generics) -> Self {
|
||||||
|
GenericFilter {
|
||||||
|
src,
|
||||||
|
lifetimes: HashSet::new(),
|
||||||
|
types: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn finish(self) -> Generics {
|
||||||
|
let mut params: Punctuated<GenericParam, Comma> = Default::default();
|
||||||
|
let mut where_clause = self
|
||||||
|
.src
|
||||||
|
.where_clause
|
||||||
|
.as_ref()
|
||||||
|
.map(|wc| WhereClause {
|
||||||
|
where_token: wc.where_token,
|
||||||
|
predicates: Default::default(),
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| WhereClause {
|
||||||
|
where_token: Where(Span::call_site()),
|
||||||
|
predicates: Default::default(),
|
||||||
|
});
|
||||||
|
for src_param in &self.src.params {
|
||||||
|
match src_param {
|
||||||
|
GenericParam::Lifetime(l) if self.lifetimes.contains(&l.lifetime) => {
|
||||||
|
params.push(src_param.clone())
|
||||||
|
}
|
||||||
|
GenericParam::Type(t) if self.types.contains(&t.ident) => {
|
||||||
|
params.push(src_param.clone())
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for src_predicate in self.src.where_clause.iter().flat_map(|wc| &wc.predicates) {
|
||||||
|
match src_predicate {
|
||||||
|
WherePredicate::Lifetime(l) if self.lifetimes.contains(&l.lifetime) => {
|
||||||
|
where_clause.predicates.push(src_predicate.clone())
|
||||||
|
}
|
||||||
|
WherePredicate::Type(PredicateType {
|
||||||
|
bounded_ty: Type::Path(t),
|
||||||
|
..
|
||||||
|
}) if self.types.contains(&t.path.segments.last().unwrap().ident) => {
|
||||||
|
where_clause.predicates.push(src_predicate.clone())
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Generics {
|
||||||
|
lt_token: if params.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.src.lt_token.clone()
|
||||||
|
},
|
||||||
|
gt_token: if params.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.src.gt_token.clone()
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
where_clause: if where_clause.predicates.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(where_clause)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Fold for GenericFilter<'a> {
|
||||||
|
fn fold_lifetime(&mut self, i: Lifetime) -> Lifetime {
|
||||||
|
self.lifetimes
|
||||||
|
.extend(self.src.params.iter().filter_map(|param| match param {
|
||||||
|
GenericParam::Lifetime(l) if l.lifetime == i => Some(l.lifetime.clone()),
|
||||||
|
_ => None,
|
||||||
|
}));
|
||||||
|
i
|
||||||
|
}
|
||||||
|
fn fold_type(&mut self, i: Type) -> Type {
|
||||||
|
self.types.extend(
|
||||||
|
self.src
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.filter_map(|param| match (param, &i) {
|
||||||
|
(GenericParam::Type(t), Type::Path(i)) if i.path.is_ident(&t.ident) => {
|
||||||
|
Some(t.ident.clone())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_handler(
|
||||||
|
fn_name: &Ident,
|
||||||
|
fn_generics: &Generics,
|
||||||
|
opt: &Options,
|
||||||
|
params: &[ParamType],
|
||||||
|
) -> TokenStream {
|
||||||
|
let mut param_def = Vec::new();
|
||||||
|
let mut ctx_ty = quote! { () };
|
||||||
|
for param in params {
|
||||||
|
match param {
|
||||||
|
ParamType::Arg(arg) => {
|
||||||
|
let name = arg.name.clone().unwrap();
|
||||||
|
let rename = LitStr::new(&name.to_string(), name.span());
|
||||||
|
let field_name = Ident::new(&format!("arg_{}", name), name.span());
|
||||||
|
let ty = arg.ty.clone();
|
||||||
|
param_def.push(quote! {
|
||||||
|
#[serde(rename = #rename)]
|
||||||
|
#field_name: #ty,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ParamType::Context(ctx) => {
|
||||||
|
ctx_ty = quote! { #ctx };
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (_, fn_type_generics, _) = fn_generics.split_for_impl();
|
||||||
|
let fn_turbofish = fn_type_generics.as_turbofish();
|
||||||
|
let fn_path: Path = macro_try!(syn::parse2(quote! { super::#fn_name#fn_turbofish }));
|
||||||
|
let mut param_generics_filter = GenericFilter::new(fn_generics);
|
||||||
|
for param in params {
|
||||||
|
if let ParamType::Arg(a) = param {
|
||||||
|
param_generics_filter.fold_type(a.ty.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let param_generics = param_generics_filter.finish();
|
||||||
|
let (_, param_ty_generics, _) = param_generics.split_for_impl();
|
||||||
|
let param_struct_def = quote! {
|
||||||
|
#[derive(rpc_toolkit_prelude::Deserialize)]
|
||||||
|
pub struct Params#param_ty_generics {
|
||||||
|
#(
|
||||||
|
#param_def
|
||||||
|
)*
|
||||||
|
#[serde(flatten)]
|
||||||
|
#[serde(default)]
|
||||||
|
rest: rpc_toolkit_prelude::Value,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let param = params.iter().map(|param| match param {
|
||||||
|
ParamType::Arg(arg) => {
|
||||||
|
let name = arg.name.clone().unwrap();
|
||||||
|
let field_name = Ident::new(&format!("arg_{}", name), name.span());
|
||||||
|
quote! { args.#field_name }
|
||||||
|
}
|
||||||
|
ParamType::Context(_) => quote! { ctx },
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
match opt {
|
||||||
|
Options::Leaf(opt) if matches!(opt.exec_ctx, ExecutionContext::LocalOnly(_)) => quote! {
|
||||||
|
#param_struct_def
|
||||||
|
|
||||||
|
pub async fn rpc_handler#fn_generics(
|
||||||
|
_ctx: #ctx_ty,
|
||||||
|
method: &str,
|
||||||
|
_args: Params#param_ty_generics,
|
||||||
|
) -> Result<rpc_toolkit_prelude::Value, rpc_toolkit_prelude::RpcError> {
|
||||||
|
Err(RpcError {
|
||||||
|
data: Some(method.into()),
|
||||||
|
..rpc_toolkit_prelude::yajrc::METHOD_NOT_FOUND_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Options::Leaf(opt) => {
|
||||||
|
let invocation = if opt.is_async {
|
||||||
|
quote! {
|
||||||
|
#fn_path(#(#param),*).await?
|
||||||
|
}
|
||||||
|
} else if opt.blocking.is_some() {
|
||||||
|
quote! {
|
||||||
|
rpc_toolkit_prelude::spawn_blocking(move || #fn_path(#(#param),*)).await?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#fn_path(#(#param),*)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
#param_struct_def
|
||||||
|
|
||||||
|
pub async fn rpc_handler#fn_generics(
|
||||||
|
ctx: #ctx_ty,
|
||||||
|
method: &str,
|
||||||
|
args: Params#param_ty_generics,
|
||||||
|
) -> Result<rpc_toolkit_prelude::Value, rpc_toolkit_prelude::RpcError> {
|
||||||
|
Ok(rpc_toolkit_prelude::to_value(#invocation)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Options::Parent(ParentOptions {
|
||||||
|
common,
|
||||||
|
subcommands,
|
||||||
|
self_impl,
|
||||||
|
}) => {
|
||||||
|
let cmd_preprocess = if common.is_async {
|
||||||
|
quote! {
|
||||||
|
let ctx = #fn_path(#(#param),*).await?;
|
||||||
|
}
|
||||||
|
} else if common.blocking.is_some() {
|
||||||
|
quote! {
|
||||||
|
let ctx = rpc_toolkit_prelude::spawn_blocking(move || #fn_path(#(#param),*)).await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
let ctx = #fn_path(#(#param),*)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let subcmd_impl = subcommands.iter().map(|subcommand| {
|
||||||
|
let mut subcommand = subcommand.clone();
|
||||||
|
let rpc_handler = PathSegment {
|
||||||
|
ident: Ident::new("rpc_handler", Span::call_site()),
|
||||||
|
arguments: std::mem::replace(
|
||||||
|
&mut subcommand.segments.last_mut().unwrap().arguments,
|
||||||
|
PathArguments::None,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
quote_spanned!{ subcommand.span() =>
|
||||||
|
[#subcommand::NAME, rest] => #subcommand::#rpc_handler(ctx, rest, rpc_toolkit_prelude::from_value(args.rest)?).await
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let subcmd_impl = quote! {
|
||||||
|
match method.splitn(2, ".").chain(std::iter::repeat("")).take(2).collect::<Vec<_>>().as_slice() {
|
||||||
|
#(
|
||||||
|
#subcmd_impl,
|
||||||
|
)*
|
||||||
|
_ => Err(RpcError {
|
||||||
|
data: Some(method.into()),
|
||||||
|
..rpc_toolkit_prelude::yajrc::METHOD_NOT_FOUND_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match self_impl {
|
||||||
|
Some(self_impl) if !matches!(common.exec_ctx, ExecutionContext::LocalOnly(_)) => {
|
||||||
|
let self_impl_fn = &self_impl.path;
|
||||||
|
let self_impl = if self_impl.is_async {
|
||||||
|
quote_spanned! { self_impl_fn.span() =>
|
||||||
|
#self_impl_fn(ctx).await?
|
||||||
|
}
|
||||||
|
} else if self_impl.blocking {
|
||||||
|
quote_spanned! { self_impl_fn.span() =>
|
||||||
|
rpc_toolkit_prelude::spawn_blocking(move || #self_impl_fn(ctx)).await?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote_spanned! { self_impl_fn.span() =>
|
||||||
|
#self_impl_fn(ctx)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
#param_struct_def
|
||||||
|
|
||||||
|
pub async fn rpc_handler#fn_generics(
|
||||||
|
ctx: #ctx_ty,
|
||||||
|
method: &str,
|
||||||
|
args: Params#param_ty_generics,
|
||||||
|
) -> Result<rpc_toolkit_prelude::Value, rpc_toolkit_prelude::RpcError> {
|
||||||
|
#cmd_preprocess
|
||||||
|
|
||||||
|
if method.is_empty() {
|
||||||
|
Ok(rpc_toolkit_prelude::to_value(&#self_impl)?)
|
||||||
|
} else {
|
||||||
|
#subcmd_impl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
quote! {
|
||||||
|
#param_struct_def
|
||||||
|
|
||||||
|
pub async fn rpc_handler#fn_generics(
|
||||||
|
ctx: #ctx_ty,
|
||||||
|
method: &str,
|
||||||
|
args: Params#param_ty_generics,
|
||||||
|
) -> Result<rpc_toolkit_prelude::Value, rpc_toolkit_prelude::RpcError> {
|
||||||
|
#cmd_preprocess
|
||||||
|
|
||||||
|
#subcmd_impl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_handler(
|
||||||
|
fn_name: &Ident,
|
||||||
|
fn_generics: &Generics,
|
||||||
|
opt: &mut Options,
|
||||||
|
params: &[ParamType],
|
||||||
|
) -> TokenStream {
|
||||||
|
let mut ctx_ty = quote! { () };
|
||||||
|
for param in params {
|
||||||
|
match param {
|
||||||
|
ParamType::Context(ctx) => {
|
||||||
|
ctx_ty = quote! { #ctx };
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut generics = fn_generics.clone();
|
||||||
|
generics.params.push(macro_try!(syn::parse2(
|
||||||
|
quote! { ParentParams: rpc_toolkit_prelude::Serialize }
|
||||||
|
)));
|
||||||
|
if generics.lt_token.is_none() {
|
||||||
|
generics.lt_token = Some(Default::default());
|
||||||
|
}
|
||||||
|
if generics.gt_token.is_none() {
|
||||||
|
generics.gt_token = Some(Default::default());
|
||||||
|
}
|
||||||
|
let (_, fn_type_generics, _) = fn_generics.split_for_impl();
|
||||||
|
let fn_turbofish = fn_type_generics.as_turbofish();
|
||||||
|
let fn_path: Path = macro_try!(syn::parse2(quote! { super::#fn_name#fn_turbofish }));
|
||||||
|
let param = params.iter().map(|param| match param {
|
||||||
|
ParamType::Arg(arg) => {
|
||||||
|
let name = arg.name.clone().unwrap();
|
||||||
|
let field_name = Ident::new(&format!("arg_{}", name), name.span());
|
||||||
|
quote! { params.#field_name.clone() }
|
||||||
|
}
|
||||||
|
ParamType::Context(_) => quote! { ctx },
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
let mut param_generics_filter = GenericFilter::new(fn_generics);
|
||||||
|
for param in params {
|
||||||
|
if let ParamType::Arg(a) = param {
|
||||||
|
param_generics_filter.fold_type(a.ty.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut param_generics = param_generics_filter.finish();
|
||||||
|
param_generics.params.push(macro_try!(syn::parse2(quote! {
|
||||||
|
ParentParams: rpc_toolkit_prelude::Serialize
|
||||||
|
})));
|
||||||
|
if param_generics.lt_token.is_none() {
|
||||||
|
generics.lt_token = Some(Default::default());
|
||||||
|
}
|
||||||
|
if param_generics.gt_token.is_none() {
|
||||||
|
generics.gt_token = Some(Default::default());
|
||||||
|
}
|
||||||
|
let (_, param_ty_generics, _) = param_generics.split_for_impl();
|
||||||
|
let mut arg_def = Vec::new();
|
||||||
|
for param in params {
|
||||||
|
match param {
|
||||||
|
ParamType::Arg(arg) => {
|
||||||
|
let name = arg.name.clone().unwrap();
|
||||||
|
let rename = LitStr::new(&name.to_string(), name.span());
|
||||||
|
let field_name = Ident::new(&format!("arg_{}", name), name.span());
|
||||||
|
let ty = arg.ty.clone();
|
||||||
|
arg_def.push(quote! {
|
||||||
|
#[serde(rename = #rename)]
|
||||||
|
#field_name: #ty,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let arg = params
|
||||||
|
.iter()
|
||||||
|
.filter_map(|param| {
|
||||||
|
if let ParamType::Arg(a) = param {
|
||||||
|
Some(a)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|arg| {
|
||||||
|
let name = arg.name.clone().unwrap();
|
||||||
|
let arg_name = LitStr::new(&name.to_string(), name.span());
|
||||||
|
let field_name = Ident::new(&format!("arg_{}", name), name.span());
|
||||||
|
if arg.stdin {
|
||||||
|
quote! {
|
||||||
|
#field_name: rpc_toolkit_prelude::default_stdin_parser(&mut std::io::stdin(), matches)?,
|
||||||
|
}
|
||||||
|
} else if arg.check_is_present {
|
||||||
|
quote! {
|
||||||
|
#field_name: matches.is_present(#arg_name),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let parse_val = if let Some(parse) = &arg.parse {
|
||||||
|
quote! {
|
||||||
|
#parse(arg_val, matches)?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
rpc_toolkit_prelude::default_arg_parser(arg_val, matches)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if arg.optional {
|
||||||
|
quote! {
|
||||||
|
#field_name: if let Some(arg_val) = matches.value_of(#arg_name) {
|
||||||
|
Some(#parse_val)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#field_name: {
|
||||||
|
let arg_val = matches.value_of(#arg_name).unwrap();
|
||||||
|
#parse_val
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let param_struct_def = quote! {
|
||||||
|
#[derive(rpc_toolkit_prelude::Serialize)]
|
||||||
|
struct Params#param_ty_generics {
|
||||||
|
#(
|
||||||
|
#arg_def
|
||||||
|
)*
|
||||||
|
#[serde(flatten)]
|
||||||
|
rest: ParentParams,
|
||||||
|
}
|
||||||
|
let params: Params#param_ty_generics = Params {
|
||||||
|
#(
|
||||||
|
#arg
|
||||||
|
)*
|
||||||
|
rest: parent_params,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let create_rt = quote! {
|
||||||
|
let rt_ref = if let Some(rt) = rt.as_mut() {
|
||||||
|
&*rt
|
||||||
|
} else {
|
||||||
|
rt = Some(rpc_toolkit_prelude::Runtime::new().map_err(|e| RpcError {
|
||||||
|
data: Some(format!("{}", e).into()),
|
||||||
|
..rpc_toolkit_prelude::yajrc::INTERNAL_ERROR
|
||||||
|
})?);
|
||||||
|
rt.as_ref().unwrap()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let display = if let Some(display) = &opt.common().display {
|
||||||
|
quote! { #display }
|
||||||
|
} else {
|
||||||
|
quote! { rpc_toolkit_prelude::default_display }
|
||||||
|
};
|
||||||
|
match opt {
|
||||||
|
Options::Leaf(opt) if matches!(opt.exec_ctx, ExecutionContext::RemoteOnly(_)) => quote! {
|
||||||
|
pub fn cli_handler#generics(
|
||||||
|
_ctx: #ctx_ty,
|
||||||
|
_rt: Option<rpc_toolkit_prelude::Runtime>,
|
||||||
|
_matches: &rpc_toolkit_prelude::ArgMatches<'_>,
|
||||||
|
method: rpc_toolkit_prelude::Cow<'_, str>,
|
||||||
|
_parent_params: ParentParams,
|
||||||
|
) -> Result<(), rpc_toolkit_prelude::RpcError> {
|
||||||
|
Err(RpcError {
|
||||||
|
data: Some(method.into()),
|
||||||
|
..rpc_toolkit_prelude::yajrc::METHOD_NOT_FOUND_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Options::Leaf(opt) if matches!(opt.exec_ctx, ExecutionContext::LocalOnly(_)) => {
|
||||||
|
let invocation = if opt.is_async {
|
||||||
|
quote! {
|
||||||
|
rt_ref.block_on(#fn_path(#(#param),*))?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#fn_path(#(#param),*)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let display_res = if let Some(display_fn) = &opt.display {
|
||||||
|
quote! {
|
||||||
|
#display_fn(#invocation)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
rpc_toolkit_prelude::default_display(#invocation)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let rt_action = if opt.is_async {
|
||||||
|
create_rt
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
drop(rt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
pub fn cli_handler#generics(
|
||||||
|
ctx: #ctx_ty,
|
||||||
|
mut rt: Option<rpc_toolkit_prelude::Runtime>,
|
||||||
|
matches: &rpc_toolkit_prelude::ArgMatches<'_>,
|
||||||
|
_method: rpc_toolkit_prelude::Cow<'_, str>,
|
||||||
|
_parent_params: ParentParams
|
||||||
|
) -> Result<(), rpc_toolkit_prelude::RpcError> {
|
||||||
|
#rt_action
|
||||||
|
Ok(#display_res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Options::Leaf(opt) => {
|
||||||
|
let param = param.map(|_| quote! { unreachable!() });
|
||||||
|
let invocation = if opt.is_async {
|
||||||
|
quote! {
|
||||||
|
rt_ref.block_on(#fn_path(#(#param),*))?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#fn_path(#(#param),*)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
pub fn cli_handler#generics(
|
||||||
|
ctx: #ctx_ty,
|
||||||
|
mut rt: Option<rpc_toolkit_prelude::Runtime>,
|
||||||
|
matches: &rpc_toolkit_prelude::ArgMatches<'_>,
|
||||||
|
method: rpc_toolkit_prelude::Cow<'_, str>,
|
||||||
|
parent_params: ParentParams,
|
||||||
|
) -> Result<(), rpc_toolkit_prelude::RpcError> {
|
||||||
|
#param_struct_def
|
||||||
|
|
||||||
|
#create_rt
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
let return_ty = if true {
|
||||||
|
rpc_toolkit_prelude::PhantomData
|
||||||
|
} else {
|
||||||
|
rpc_toolkit_prelude::make_phantom(#invocation)
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = rt_ref.block_on(rpc_toolkit_prelude::call_remote(ctx, method.as_ref(), params, return_ty))?;
|
||||||
|
Ok(#display(res.result?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Options::Parent(ParentOptions {
|
||||||
|
common,
|
||||||
|
subcommands,
|
||||||
|
self_impl,
|
||||||
|
}) => {
|
||||||
|
let cmd_preprocess = if common.is_async {
|
||||||
|
quote! {
|
||||||
|
#create_rt
|
||||||
|
let ctx = rt_ref.block_on(#fn_path(#(#param),*))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
let ctx = #fn_path(#(#param),*)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let subcmd_impl = subcommands.iter().map(|subcommand| {
|
||||||
|
let mut subcommand = subcommand.clone();
|
||||||
|
let mut cli_handler = PathSegment {
|
||||||
|
ident: Ident::new("cli_handler", Span::call_site()),
|
||||||
|
arguments: std::mem::replace(
|
||||||
|
&mut subcommand.segments.last_mut().unwrap().arguments,
|
||||||
|
PathArguments::None,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
cli_handler.arguments = match cli_handler.arguments {
|
||||||
|
PathArguments::None => PathArguments::AngleBracketed(
|
||||||
|
syn::parse2(quote! { ::<Params#param_ty_generics> }).unwrap(),
|
||||||
|
),
|
||||||
|
PathArguments::AngleBracketed(mut a) => {
|
||||||
|
a.args
|
||||||
|
.push(syn::parse2(quote! { Params#param_ty_generics }).unwrap());
|
||||||
|
PathArguments::AngleBracketed(a)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
quote_spanned! { subcommand.span() =>
|
||||||
|
(#subcommand::NAME, Some(sub_m)) => {
|
||||||
|
let method = if method.is_empty() {
|
||||||
|
#subcommand::NAME.into()
|
||||||
|
} else {
|
||||||
|
method + "." + #subcommand::NAME
|
||||||
|
};
|
||||||
|
#subcommand::#cli_handler(ctx, rt, sub_m, method, params)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let self_impl = match (self_impl, &common.exec_ctx) {
|
||||||
|
(Some(self_impl), ExecutionContext::LocalOnly(_)) => {
|
||||||
|
let self_impl_fn = &self_impl.path;
|
||||||
|
let create_rt = if common.is_async {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(create_rt)
|
||||||
|
};
|
||||||
|
let self_impl = if self_impl.is_async {
|
||||||
|
quote_spanned! { self_impl_fn.span() =>
|
||||||
|
#create_rt
|
||||||
|
rt_ref.block_on(#self_impl_fn(ctx))?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote_spanned! { self_impl_fn.span() =>
|
||||||
|
#self_impl_fn(ctx)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
Ok(#display(#self_impl)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(self_impl), ExecutionContext::Standard) => {
|
||||||
|
let self_impl_fn = &self_impl.path;
|
||||||
|
let self_impl = if self_impl.is_async {
|
||||||
|
quote! {
|
||||||
|
rt_ref.block_on(#self_impl_fn(ctx))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#self_impl_fn(ctx)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let create_rt = if common.is_async {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(create_rt)
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
#create_rt
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
let return_ty = if true {
|
||||||
|
rpc_toolkit_prelude::PhantomData
|
||||||
|
} else {
|
||||||
|
let ctx_new = unreachable!();
|
||||||
|
rpc_toolkit_prelude::match_types(&ctx, &ctx_new);
|
||||||
|
let ctx = ctx_new;
|
||||||
|
rpc_toolkit_prelude::make_phantom(#self_impl?)
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = rt_ref.block_on(rpc_toolkit_prelude::call_remote(ctx, method.as_ref(), params, return_ty))?;
|
||||||
|
Ok(#display(res.result?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => quote! {
|
||||||
|
Err(RpcError {
|
||||||
|
data: Some(method.into()),
|
||||||
|
..rpc_toolkit_prelude::yajrc::METHOD_NOT_FOUND_ERROR
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
pub fn cli_handler#generics(
|
||||||
|
ctx: #ctx_ty,
|
||||||
|
mut rt: Option<rpc_toolkit_prelude::Runtime>,
|
||||||
|
matches: &rpc_toolkit_prelude::ArgMatches<'_>,
|
||||||
|
method: rpc_toolkit_prelude::Cow<'_, str>,
|
||||||
|
parent_params: ParentParams,
|
||||||
|
) -> Result<(), rpc_toolkit_prelude::RpcError> {
|
||||||
|
#param_struct_def
|
||||||
|
|
||||||
|
#cmd_preprocess
|
||||||
|
|
||||||
|
match matches.subcommand() {
|
||||||
|
#(
|
||||||
|
#subcmd_impl
|
||||||
|
)*
|
||||||
|
_ => #self_impl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(args: AttributeArgs, mut item: ItemFn) -> TokenStream {
|
||||||
|
let mut params = macro_try!(parse_param_attrs(&mut item));
|
||||||
|
let mut opt = macro_try!(parse_command_attr(args));
|
||||||
|
if let Some(a) = &opt.common().blocking {
|
||||||
|
if item.sig.asyncness.is_some() {
|
||||||
|
return Error::new(a.span(), "cannot use `blocking` on an async fn").to_compile_error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opt.common().is_async = item.sig.asyncness.is_some();
|
||||||
|
let fn_vis = &item.vis;
|
||||||
|
let fn_name = &item.sig.ident;
|
||||||
|
let fn_generics = &item.sig.generics;
|
||||||
|
let command_name = opt
|
||||||
|
.common()
|
||||||
|
.rename
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| fn_name.clone());
|
||||||
|
let command_name_str = LitStr::new(&command_name.to_string(), command_name.span());
|
||||||
|
let is_async = LitBool::new(
|
||||||
|
opt.common().is_async,
|
||||||
|
item.sig
|
||||||
|
.asyncness
|
||||||
|
.map(|a| a.span())
|
||||||
|
.unwrap_or_else(Span::call_site),
|
||||||
|
);
|
||||||
|
let build_app = build_app(command_name_str.clone(), &mut opt, &mut params);
|
||||||
|
let rpc_handler = rpc_handler(fn_name, fn_generics, &opt, ¶ms);
|
||||||
|
let cli_handler = cli_handler(fn_name, fn_generics, &mut opt, ¶ms);
|
||||||
|
|
||||||
|
let res = quote! {
|
||||||
|
#item
|
||||||
|
#fn_vis mod #fn_name {
|
||||||
|
use super::*;
|
||||||
|
use rpc_toolkit::command_helpers::prelude as rpc_toolkit_prelude;
|
||||||
|
|
||||||
|
pub const NAME: &'static str = #command_name_str;
|
||||||
|
pub const ASYNC: bool = #is_async;
|
||||||
|
|
||||||
|
#build_app
|
||||||
|
|
||||||
|
#rpc_handler
|
||||||
|
|
||||||
|
#cli_handler
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// panic!("{}", res);
|
||||||
|
res
|
||||||
|
}
|
||||||
85
rpc-toolkit-macro-internals/src/command/mod.rs
Normal file
85
rpc-toolkit-macro-internals/src/command/mod.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use syn::*;
|
||||||
|
|
||||||
|
pub mod build;
|
||||||
|
mod parse;
|
||||||
|
|
||||||
|
pub enum ExecutionContext {
|
||||||
|
Standard,
|
||||||
|
LocalOnly(Path),
|
||||||
|
RemoteOnly(Path),
|
||||||
|
}
|
||||||
|
impl Default for ExecutionContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
ExecutionContext::Standard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LeafOptions {
|
||||||
|
blocking: Option<Path>,
|
||||||
|
is_async: bool,
|
||||||
|
about: Option<LitStr>,
|
||||||
|
rename: Option<Ident>,
|
||||||
|
exec_ctx: ExecutionContext,
|
||||||
|
display: Option<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SelfImplInfo {
|
||||||
|
path: Path,
|
||||||
|
is_async: bool,
|
||||||
|
blocking: bool,
|
||||||
|
}
|
||||||
|
pub struct ParentOptions {
|
||||||
|
common: LeafOptions,
|
||||||
|
subcommands: Vec<Path>,
|
||||||
|
self_impl: Option<SelfImplInfo>,
|
||||||
|
}
|
||||||
|
impl From<LeafOptions> for ParentOptions {
|
||||||
|
fn from(opt: LeafOptions) -> Self {
|
||||||
|
ParentOptions {
|
||||||
|
common: opt,
|
||||||
|
subcommands: Default::default(),
|
||||||
|
self_impl: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Options {
|
||||||
|
Leaf(LeafOptions),
|
||||||
|
Parent(ParentOptions),
|
||||||
|
}
|
||||||
|
impl Options {
|
||||||
|
fn to_parent(&mut self) -> Result<&mut ParentOptions> {
|
||||||
|
if let Options::Leaf(opt) = self {
|
||||||
|
*self = Options::Parent(std::mem::replace(opt, Default::default()).into());
|
||||||
|
}
|
||||||
|
Ok(match self {
|
||||||
|
Options::Parent(a) => a,
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn common(&mut self) -> &mut LeafOptions {
|
||||||
|
match self {
|
||||||
|
Options::Leaf(ref mut opt) => opt,
|
||||||
|
Options::Parent(opt) => &mut opt.common,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArgOptions {
|
||||||
|
ty: Type,
|
||||||
|
optional: bool,
|
||||||
|
check_is_present: bool,
|
||||||
|
help: Option<LitStr>,
|
||||||
|
name: Option<Ident>,
|
||||||
|
short: Option<LitStr>,
|
||||||
|
long: Option<LitStr>,
|
||||||
|
parse: Option<Path>,
|
||||||
|
stdin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ParamType {
|
||||||
|
None,
|
||||||
|
Arg(ArgOptions),
|
||||||
|
Context(Type),
|
||||||
|
}
|
||||||
537
rpc-toolkit-macro-internals/src/command/parse.rs
Normal file
537
rpc-toolkit-macro-internals/src/command/parse.rs
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
use super::*;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
pub fn parse_command_attr(args: AttributeArgs) -> Result<Options> {
|
||||||
|
let mut opt = Options::Leaf(Default::default());
|
||||||
|
for arg in args {
|
||||||
|
match arg {
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("subcommands") => {
|
||||||
|
let inner = opt.to_parent()?;
|
||||||
|
if !inner.subcommands.is_empty() {
|
||||||
|
return Err(Error::new(list.span(), "duplicate argument `subcommands`"));
|
||||||
|
}
|
||||||
|
for subcmd in list.nested {
|
||||||
|
match subcmd {
|
||||||
|
NestedMeta::Meta(Meta::Path(subcmd)) => inner.subcommands.push(subcmd),
|
||||||
|
NestedMeta::Lit(Lit::Str(s)) => {
|
||||||
|
inner.subcommands.push(syn::parse_str(&s.value())?)
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(mut self_impl))
|
||||||
|
if self_impl.path.is_ident("self") =>
|
||||||
|
{
|
||||||
|
if self_impl.nested.len() == 1 {
|
||||||
|
match self_impl.nested.pop().unwrap().into_value() {
|
||||||
|
NestedMeta::Meta(Meta::Path(self_impl)) => {
|
||||||
|
if inner.self_impl.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
self_impl.span(),
|
||||||
|
"duplicate argument `self`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
inner.self_impl = Some(SelfImplInfo {
|
||||||
|
path: self_impl,
|
||||||
|
is_async: false,
|
||||||
|
blocking: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(l)) if l.nested.len() == 1 => {
|
||||||
|
if inner.self_impl.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
self_impl.span(),
|
||||||
|
"duplicate argument `self`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let blocking = match l.nested.first().unwrap() {
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("blocking") => true,
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("async") => false,
|
||||||
|
arg => return Err(Error::new(arg.span(), "unknown argument")),
|
||||||
|
};
|
||||||
|
inner.self_impl = Some(SelfImplInfo {
|
||||||
|
path: l.path,
|
||||||
|
is_async: !blocking,
|
||||||
|
blocking,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
a => {
|
||||||
|
return Err(Error::new(
|
||||||
|
a.span(),
|
||||||
|
"`self` implementation must be a path, or a list with 1 argument",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(
|
||||||
|
self_impl.nested.span(),
|
||||||
|
"`self` can only have one implementation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arg => {
|
||||||
|
return Err(Error::new(arg.span(), "unknown argument to `subcommands`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inner.subcommands.is_empty() {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`subcommands` requires at least 1 argument",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("subcommands") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"`subcommands` requires at least 1 argument",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("subcommands") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
nv.path.span(),
|
||||||
|
"`subcommands` cannot be assigned to",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("display") => {
|
||||||
|
if list.nested.len() == 1 {
|
||||||
|
match &list.nested[0] {
|
||||||
|
NestedMeta::Meta(Meta::Path(display_impl)) => {
|
||||||
|
if opt.common().display.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
display_impl.span(),
|
||||||
|
"duplicate argument `display`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.common().display = Some(display_impl.clone())
|
||||||
|
}
|
||||||
|
a => {
|
||||||
|
return Err(Error::new(
|
||||||
|
a.span(),
|
||||||
|
"`display` implementation must be a path",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.nested.span(),
|
||||||
|
"`display` can only have one implementation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("display") => {
|
||||||
|
return Err(Error::new(p.span(), "`display` requires an argument"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("display") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
nv.path.span(),
|
||||||
|
"`display` cannot be assigned to",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("local") => {
|
||||||
|
match &opt.common().exec_ctx {
|
||||||
|
ExecutionContext::Standard => {
|
||||||
|
opt.common().exec_ctx = ExecutionContext::LocalOnly(p)
|
||||||
|
}
|
||||||
|
ExecutionContext::LocalOnly(_) => {
|
||||||
|
return Err(Error::new(p.span(), "duplicate argument: `local`"))
|
||||||
|
}
|
||||||
|
ExecutionContext::RemoteOnly(_) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"`local` and `remote` are mutually exclusive",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("local") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`local` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("local") => {
|
||||||
|
return Err(Error::new(nv.path.span(), "`local` cannot be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("remote") => {
|
||||||
|
match &opt.common().exec_ctx {
|
||||||
|
ExecutionContext::Standard => {
|
||||||
|
opt.common().exec_ctx = ExecutionContext::RemoteOnly(p)
|
||||||
|
}
|
||||||
|
ExecutionContext::LocalOnly(_) => {
|
||||||
|
return Err(Error::new(p.span(), "duplicate argument: `remote`"))
|
||||||
|
}
|
||||||
|
ExecutionContext::RemoteOnly(_) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"`local` and `remote` are mutually exclusive",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("remote") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`remote` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("remote") => {
|
||||||
|
return Err(Error::new(nv.path.span(), "`remote` cannot be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("blocking") => {
|
||||||
|
if opt.common().blocking.is_some() {
|
||||||
|
return Err(Error::new(p.span(), "duplicate argument `blocking`"));
|
||||||
|
}
|
||||||
|
opt.common().blocking = Some(p);
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("blocking") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`blocking` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("blocking") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
nv.path.span(),
|
||||||
|
"`blocking` cannot be assigned to",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("about") => {
|
||||||
|
if let Lit::Str(about) = nv.lit {
|
||||||
|
if opt.common().about.is_some() {
|
||||||
|
return Err(Error::new(about.span(), "duplicate argument `about`"));
|
||||||
|
}
|
||||||
|
opt.common().about = Some(about);
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(nv.lit.span(), "about message must be a string"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("about") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`about` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("about") => {
|
||||||
|
return Err(Error::new(p.span(), "`about` must be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("rename") => {
|
||||||
|
if let Lit::Str(rename) = nv.lit {
|
||||||
|
if opt.common().rename.is_some() {
|
||||||
|
return Err(Error::new(rename.span(), "duplicate argument `rename`"));
|
||||||
|
}
|
||||||
|
opt.common().rename = Some(
|
||||||
|
syn::parse_str(&rename.value())
|
||||||
|
.map_err(|e| Error::new(rename.span(), format!("{}", e)))?,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(nv.lit.span(), "`rename` must be a string"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("rename") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`rename` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("rename") => {
|
||||||
|
return Err(Error::new(p.span(), "`rename` must be assigned to"));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::new(arg.span(), "unknown argument"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Options::Parent(opt) = &opt {
|
||||||
|
if opt.self_impl.is_none() {
|
||||||
|
if let Some(display) = &opt.common.display {
|
||||||
|
return Err(Error::new(
|
||||||
|
display.span(),
|
||||||
|
"cannot define `display` for a command without an implementation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match &opt.common.exec_ctx {
|
||||||
|
ExecutionContext::LocalOnly(local) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
local.span(),
|
||||||
|
"cannot define `local` for a command without an implementation",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
ExecutionContext::RemoteOnly(remote) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
remote.span(),
|
||||||
|
"cannot define `remote` for a command without an implementation",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_arg_attr(attr: Attribute, arg: PatType, has_stdin: &mut bool) -> Result<ArgOptions> {
|
||||||
|
let arg_span = arg.span();
|
||||||
|
let mut opt = ArgOptions {
|
||||||
|
ty: *arg.ty,
|
||||||
|
optional: false,
|
||||||
|
check_is_present: false,
|
||||||
|
help: None,
|
||||||
|
name: match *arg.pat {
|
||||||
|
Pat::Ident(i) => Some(i.ident),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
short: None,
|
||||||
|
long: None,
|
||||||
|
parse: None,
|
||||||
|
stdin: false,
|
||||||
|
};
|
||||||
|
match attr.parse_meta()? {
|
||||||
|
Meta::List(list) => {
|
||||||
|
for arg in list.nested {
|
||||||
|
match arg {
|
||||||
|
NestedMeta::Meta(Meta::List(mut list)) if list.path.is_ident("parse") => {
|
||||||
|
if list.nested.len() == 1 {
|
||||||
|
match list.nested.pop().unwrap().into_value() {
|
||||||
|
NestedMeta::Meta(Meta::Path(parse_impl)) => {
|
||||||
|
if opt.parse.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.span(),
|
||||||
|
"duplicate argument `parse`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.parse = Some(parse_impl)
|
||||||
|
}
|
||||||
|
a => {
|
||||||
|
return Err(Error::new(
|
||||||
|
a.span(),
|
||||||
|
"`parse` implementation must be a path",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.nested.span(),
|
||||||
|
"`parse` can only have one implementation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("parse") => {
|
||||||
|
return Err(Error::new(p.span(), "`parse` requires an argument"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("parse") => {
|
||||||
|
return Err(Error::new(nv.path.span(), "`parse` cannot be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("stdin") => {
|
||||||
|
if *has_stdin {
|
||||||
|
return Err(Error::new(p.span(), "duplicate argument `stdin`"));
|
||||||
|
}
|
||||||
|
if opt.short.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"`stdin` and `short` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if opt.long.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"`stdin` and `long` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if opt.help.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"`stdin` and `help` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.stdin = true;
|
||||||
|
*has_stdin = true;
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("stdin") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`stdin` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("stdin") => {
|
||||||
|
return Err(Error::new(nv.path.span(), "`stdin` cannot be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("help") => {
|
||||||
|
if let Lit::Str(help) = nv.lit {
|
||||||
|
if opt.help.is_some() {
|
||||||
|
return Err(Error::new(help.span(), "duplicate argument `help`"));
|
||||||
|
}
|
||||||
|
if opt.stdin {
|
||||||
|
return Err(Error::new(
|
||||||
|
help.span(),
|
||||||
|
"`stdin` and `help` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.help = Some(help);
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(nv.lit.span(), "help message must be a string"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("help") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`help` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("help") => {
|
||||||
|
return Err(Error::new(p.span(), "`help` must be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("rename") => {
|
||||||
|
if let Lit::Str(rename) = nv.lit {
|
||||||
|
if opt.name.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
rename.span(),
|
||||||
|
"duplicate argument `rename`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.name = Some(
|
||||||
|
syn::parse_str(&rename.value())
|
||||||
|
.map_err(|e| Error::new(rename.span(), format!("{}", e)))?,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(nv.lit.span(), "`rename` must be a string"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("rename") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`rename` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("rename") => {
|
||||||
|
return Err(Error::new(p.span(), "`rename` must be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("short") => {
|
||||||
|
if let Lit::Str(short) = nv.lit {
|
||||||
|
if short.value().len() != 1 {
|
||||||
|
return Err(Error::new(
|
||||||
|
short.span(),
|
||||||
|
"`short` value must be 1 character",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if opt.short.is_some() {
|
||||||
|
return Err(Error::new(short.span(), "duplicate argument `short`"));
|
||||||
|
}
|
||||||
|
if opt.stdin {
|
||||||
|
return Err(Error::new(
|
||||||
|
short.span(),
|
||||||
|
"`stdin` and `short` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.short = Some(short);
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(nv.lit.span(), "`short` must be a string"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("short") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`short` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("short") => {
|
||||||
|
return Err(Error::new(p.span(), "`short` must be assigned to"));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("long") => {
|
||||||
|
if let Lit::Str(long) = nv.lit {
|
||||||
|
if opt.long.is_some() {
|
||||||
|
return Err(Error::new(long.span(), "duplicate argument `long`"));
|
||||||
|
}
|
||||||
|
if opt.stdin {
|
||||||
|
return Err(Error::new(
|
||||||
|
long.span(),
|
||||||
|
"`stdin` and `long` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
opt.long = Some(long);
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(nv.lit.span(), "`long` must be a string"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("long") => {
|
||||||
|
return Err(Error::new(
|
||||||
|
list.path.span(),
|
||||||
|
"`long` does not take any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("long") => {
|
||||||
|
return Err(Error::new(p.span(), "`long` must be assigned to"));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::new(arg.span(), "unknown argument"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Meta::Path(_) => (),
|
||||||
|
Meta::NameValue(nv) => return Err(Error::new(nv.span(), "`arg` cannot be assigned to")),
|
||||||
|
}
|
||||||
|
if opt.name.is_none() {
|
||||||
|
return Err(Error::new(
|
||||||
|
arg_span,
|
||||||
|
"cannot infer name for pattern argument",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_param_attrs(item: &mut ItemFn) -> Result<Vec<ParamType>> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
let mut has_stdin = false;
|
||||||
|
for param in item.sig.inputs.iter_mut() {
|
||||||
|
if let FnArg::Typed(param) = param {
|
||||||
|
let mut ty = ParamType::None;
|
||||||
|
let mut i = 0;
|
||||||
|
while i != param.attrs.len() {
|
||||||
|
if param.attrs[i].path.is_ident("arg") {
|
||||||
|
let attr = param.attrs.remove(i);
|
||||||
|
if matches!(ty, ParamType::None) {
|
||||||
|
ty = ParamType::Arg(parse_arg_attr(attr, param.clone(), &mut has_stdin)?);
|
||||||
|
} else if matches!(ty, ParamType::Arg(_)) {
|
||||||
|
return Err(Error::new(
|
||||||
|
attr.span(),
|
||||||
|
"`arg` attribute may only be specified once",
|
||||||
|
));
|
||||||
|
} else if matches!(ty, ParamType::Context(_)) {
|
||||||
|
return Err(Error::new(
|
||||||
|
attr.span(),
|
||||||
|
"`arg` and `context` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if param.attrs[i].path.is_ident("context") {
|
||||||
|
let attr = param.attrs.remove(i);
|
||||||
|
if matches!(ty, ParamType::None) {
|
||||||
|
ty = ParamType::Context(*param.ty.clone());
|
||||||
|
} else if matches!(ty, ParamType::Context(_)) {
|
||||||
|
return Err(Error::new(
|
||||||
|
attr.span(),
|
||||||
|
"`context` attribute may only be specified once",
|
||||||
|
));
|
||||||
|
} else if matches!(ty, ParamType::Arg(_)) {
|
||||||
|
return Err(Error::new(
|
||||||
|
attr.span(),
|
||||||
|
"`arg` and `context` are mutually exclusive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches!(ty, ParamType::None) {
|
||||||
|
return Err(Error::new(
|
||||||
|
param.span(),
|
||||||
|
"must specify either `arg` or `context` attributes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
params.push(ty)
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(
|
||||||
|
param.span(),
|
||||||
|
"commands may not take `self` as an argument",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
18
rpc-toolkit-macro-internals/src/lib.rs
Normal file
18
rpc-toolkit-macro-internals/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
macro_rules! macro_try {
|
||||||
|
($x:expr) => {
|
||||||
|
match $x {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => return e.to_compile_error(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mod command;
|
||||||
|
mod rpc_server;
|
||||||
|
mod run_cli;
|
||||||
|
|
||||||
|
pub use command::build::build as build_command;
|
||||||
|
pub use rpc_server::build::build as build_rpc_server;
|
||||||
|
pub use rpc_server::RpcServerArgs;
|
||||||
|
pub use run_cli::build::build as build_run_cli;
|
||||||
|
pub use run_cli::RunCliArgs;
|
||||||
55
rpc-toolkit-macro-internals/src/rpc_server/build.rs
Normal file
55
rpc-toolkit-macro-internals/src/rpc_server/build.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use super::*;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
pub fn build(args: RpcServerArgs) -> TokenStream {
|
||||||
|
let mut command = args.command;
|
||||||
|
let arguments = std::mem::replace(
|
||||||
|
&mut command.segments.last_mut().unwrap().arguments,
|
||||||
|
PathArguments::None,
|
||||||
|
);
|
||||||
|
command.segments.push(PathSegment {
|
||||||
|
ident: Ident::new("rpc_handler", command.span()),
|
||||||
|
arguments,
|
||||||
|
});
|
||||||
|
let seed = args.seed;
|
||||||
|
let status_fn = args
|
||||||
|
.status_fn
|
||||||
|
.unwrap_or_else(|| syn::parse2(quote! { |_| rpc_toolkit::hyper::StatusCode::OK }).unwrap());
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
let seed = #seed;
|
||||||
|
let status_fn = #status_fn;
|
||||||
|
let (builder, ctx_phantom) = rpc_toolkit::rpc_server_helpers::make_builder(seed.clone());
|
||||||
|
let make_svc = rpc_toolkit::hyper::service::make_service_fn(move |_| {
|
||||||
|
let seed = seed.clone();
|
||||||
|
async move {
|
||||||
|
Ok::<_, hyper::Error>(rpc_toolkit::hyper::service::service_fn(move |mut req| {
|
||||||
|
let seed = seed.clone();
|
||||||
|
async move {
|
||||||
|
let rpc_req = rpc_toolkit::rpc_server_helpers::make_request(&mut req).await;
|
||||||
|
rpc_toolkit::rpc_server_helpers::to_response(
|
||||||
|
&req,
|
||||||
|
match rpc_req {
|
||||||
|
Ok(rpc_req) => Ok((
|
||||||
|
rpc_req.id,
|
||||||
|
#command(
|
||||||
|
rpc_toolkit::rpc_server_helpers::bind_type(ctx_phantom, rpc_toolkit::SeedableContext::new(seed)),
|
||||||
|
rpc_toolkit::yajrc::RpcMethod::as_str(&rpc_req.method),
|
||||||
|
rpc_req.params,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
)),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
},
|
||||||
|
status_fn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.serve(make_svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
rpc-toolkit-macro-internals/src/rpc_server/mod.rs
Normal file
10
rpc-toolkit-macro-internals/src/rpc_server/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use syn::*;
|
||||||
|
|
||||||
|
pub struct RpcServerArgs {
|
||||||
|
command: Path,
|
||||||
|
seed: Expr,
|
||||||
|
status_fn: Option<Expr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod build;
|
||||||
|
mod parse;
|
||||||
23
rpc-toolkit-macro-internals/src/rpc_server/parse.rs
Normal file
23
rpc-toolkit-macro-internals/src/rpc_server/parse.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use super::*;
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
|
||||||
|
impl Parse for RpcServerArgs {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
let command = input.parse()?;
|
||||||
|
let _: token::Comma = input.parse()?;
|
||||||
|
let seed = input.parse()?;
|
||||||
|
if !input.is_empty() {
|
||||||
|
let _: token::Comma = input.parse()?;
|
||||||
|
}
|
||||||
|
let status_fn = if !input.is_empty() {
|
||||||
|
Some(input.parse()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(RpcServerArgs {
|
||||||
|
command,
|
||||||
|
seed,
|
||||||
|
status_fn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
71
rpc-toolkit-macro-internals/src/run_cli/build.rs
Normal file
71
rpc-toolkit-macro-internals/src/run_cli/build.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use super::*;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
pub fn build(args: RunCliArgs) -> TokenStream {
|
||||||
|
let mut command_handler = args.command.clone();
|
||||||
|
let mut arguments = std::mem::replace(
|
||||||
|
&mut command_handler.segments.last_mut().unwrap().arguments,
|
||||||
|
PathArguments::None,
|
||||||
|
);
|
||||||
|
let command = command_handler.clone();
|
||||||
|
if let PathArguments::AngleBracketed(a) = &mut arguments {
|
||||||
|
a.args.push(syn::parse2(quote! { () }).unwrap());
|
||||||
|
}
|
||||||
|
command_handler.segments.push(PathSegment {
|
||||||
|
ident: Ident::new("cli_handler", command.span()),
|
||||||
|
arguments,
|
||||||
|
});
|
||||||
|
let app = if let Some(mut_app) = args.mut_app {
|
||||||
|
let ident = mut_app.app_ident;
|
||||||
|
let body = mut_app.body;
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
let #ident = #command::build_app();
|
||||||
|
#body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! { #command::build_app() }
|
||||||
|
};
|
||||||
|
let make_ctx = if let Some(make_seed) = args.make_seed {
|
||||||
|
let ident = make_seed.matches_ident;
|
||||||
|
let body = make_seed.body;
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
let #ident = &rpc_toolkit_matches;
|
||||||
|
rpc_toolkit::SeedableContext::new(#body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! { rpc_toolkit::SeedableContext::new(&rpc_toolkit_matches) }
|
||||||
|
};
|
||||||
|
let exit_fn = args
|
||||||
|
.exit_fn
|
||||||
|
.unwrap_or_else(|| syn::parse2(quote! { |code| code }).unwrap());
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
let rpc_toolkit_matches = #app.get_matches();
|
||||||
|
let rpc_toolkit_ctx = #make_ctx;
|
||||||
|
if let Err(e) = #command_handler(
|
||||||
|
rpc_toolkit_ctx,
|
||||||
|
None,
|
||||||
|
&rpc_toolkit_matches,
|
||||||
|
"".into(),
|
||||||
|
(),
|
||||||
|
) {
|
||||||
|
eprintln!("{}", e.message);
|
||||||
|
if let Some(data) = e.data {
|
||||||
|
eprintln!("{:?}", data);
|
||||||
|
}
|
||||||
|
let exit_fn = #exit_fn;
|
||||||
|
drop(rpc_toolkit_matches);
|
||||||
|
std::process::exit(exit_fn(e.code))
|
||||||
|
} else {
|
||||||
|
drop(rpc_toolkit_matches);
|
||||||
|
std::process::exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
rpc-toolkit-macro-internals/src/run_cli/mod.rs
Normal file
21
rpc-toolkit-macro-internals/src/run_cli/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use syn::*;
|
||||||
|
|
||||||
|
pub struct MakeSeed {
|
||||||
|
matches_ident: Ident,
|
||||||
|
body: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MutApp {
|
||||||
|
app_ident: Ident,
|
||||||
|
body: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RunCliArgs {
|
||||||
|
command: Path,
|
||||||
|
mut_app: Option<MutApp>,
|
||||||
|
make_seed: Option<MakeSeed>,
|
||||||
|
exit_fn: Option<Expr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod build;
|
||||||
|
mod parse;
|
||||||
59
rpc-toolkit-macro-internals/src/run_cli/parse.rs
Normal file
59
rpc-toolkit-macro-internals/src/run_cli/parse.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use super::*;
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
|
||||||
|
impl Parse for MakeSeed {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
let matches_ident = input.parse()?;
|
||||||
|
let _: token::FatArrow = input.parse()?;
|
||||||
|
let body = input.parse()?;
|
||||||
|
Ok(MakeSeed {
|
||||||
|
matches_ident,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for MutApp {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
let app_ident = input.parse()?;
|
||||||
|
let _: token::FatArrow = input.parse()?;
|
||||||
|
let body = input.parse()?;
|
||||||
|
Ok(MutApp { app_ident, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for RunCliArgs {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
let command = input.parse()?;
|
||||||
|
if !input.is_empty() {
|
||||||
|
let _: token::Comma = input.parse()?;
|
||||||
|
}
|
||||||
|
let mut_app = if !input.is_empty() {
|
||||||
|
Some(input.parse()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if !input.is_empty() {
|
||||||
|
let _: token::Comma = input.parse()?;
|
||||||
|
}
|
||||||
|
let make_seed = if !input.is_empty() {
|
||||||
|
Some(input.parse()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if !input.is_empty() {
|
||||||
|
let _: token::Comma = input.parse()?;
|
||||||
|
}
|
||||||
|
let exit_fn = if !input.is_empty() {
|
||||||
|
Some(input.parse()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(RunCliArgs {
|
||||||
|
command,
|
||||||
|
mut_app,
|
||||||
|
make_seed,
|
||||||
|
exit_fn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
2
rpc-toolkit-macro/.gitignore
vendored
Normal file
2
rpc-toolkit-macro/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
15
rpc-toolkit-macro/Cargo.toml
Normal file
15
rpc-toolkit-macro/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||||
|
edition = "2018"
|
||||||
|
name = "rpc-toolkit-macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0.1"
|
||||||
|
rpc-toolkit-macro-internals = { path = "../rpc-toolkit-macro-internals" }
|
||||||
|
syn = "1.0.62"
|
||||||
52
rpc-toolkit-macro/src/lib.rs
Normal file
52
rpc-toolkit-macro/src/lib.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use proc_macro::{Span, TokenStream};
|
||||||
|
use rpc_toolkit_macro_internals::*;
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn command(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let args = syn::parse_macro_input!(args as syn::AttributeArgs);
|
||||||
|
let item = syn::parse_macro_input!(item as syn::ItemFn);
|
||||||
|
build_command(args, item).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `#[arg(...)]` -> Take this argument as a parameter
|
||||||
|
/// - `#[arg(help = "Help text")]` -> Set help text for the arg
|
||||||
|
/// - `#[arg(rename = "new_name")]` -> Set the name of the arg to `new_name` in the RPC and CLI
|
||||||
|
/// - `#[arg(short = "a")]` -> Set the "short" representation of the arg to `-a` on the CLI
|
||||||
|
/// - `#[arg(long = "arg")]` -> Set the "long" representation of the arg to `--arg` on the CLI
|
||||||
|
/// - `#[arg(parse(custom_parse_fn))]` -> Use the function `custom_parse_fn` to parse the arg from the CLI
|
||||||
|
/// - `custom_parse_fn :: Into<RpcError> err => (&str, &ArgMatches<'_>) -> Result<arg, err>`
|
||||||
|
/// - note: `arg` is the type of the argument
|
||||||
|
/// - `#[arg(stdin)]` -> Parse the argument from stdin when using the CLI
|
||||||
|
/// - `custom_parse_fn :: Into<RpcError> err => (&[u8], &ArgMatches<'_>) -> Result<arg, err>`
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn arg(_: TokenStream, _: TokenStream) -> TokenStream {
|
||||||
|
syn::Error::new(
|
||||||
|
Span::call_site().into(),
|
||||||
|
"`arg` is only allowed on arguments of a function with the `command` attribute",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - `#[context]` -> Passes the application context into this parameter
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn context(_: TokenStream, _: TokenStream) -> TokenStream {
|
||||||
|
syn::Error::new(
|
||||||
|
Span::call_site().into(),
|
||||||
|
"`context` is only allowed on arguments of a function with the `command` attribute",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn rpc_server(item: TokenStream) -> TokenStream {
|
||||||
|
let item = syn::parse_macro_input!(item as RpcServerArgs);
|
||||||
|
build_rpc_server(item).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn run_cli(item: TokenStream) -> TokenStream {
|
||||||
|
let item = syn::parse_macro_input!(item as RunCliArgs);
|
||||||
|
build_run_cli(item).into()
|
||||||
|
}
|
||||||
2
rpc-toolkit/.gitignore
vendored
Normal file
2
rpc-toolkit/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
24
rpc-toolkit/Cargo.toml
Normal file
24
rpc-toolkit/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||||
|
edition = "2018"
|
||||||
|
name = "rpc-toolkit"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[features]
|
||||||
|
cbor = ["serde_cbor"]
|
||||||
|
default = ["cbor"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = "2.33.3"
|
||||||
|
hyper = { version = "0.14.5", features = ["server", "http1", "http2", "tcp", "stream", "client"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
reqwest = { version = "0.11.2" }
|
||||||
|
rpc-toolkit-macro = { path = "../rpc-toolkit-macro" }
|
||||||
|
serde = { version = "1.0.125", features = ["derive"] }
|
||||||
|
serde_cbor = { version = "0.11.1", optional = true }
|
||||||
|
serde_json = "1.0.64"
|
||||||
|
thiserror = "1.0.24"
|
||||||
|
tokio = { version = "1.4.0", features = ["full"] }
|
||||||
|
url = "2.2.1"
|
||||||
|
yajrc = { path = "../../yajrc" }
|
||||||
123
rpc-toolkit/src/command_helpers.rs
Normal file
123
rpc-toolkit/src/command_helpers.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
use std::{fmt::Display, io::Stdin, marker::PhantomData};
|
||||||
|
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use hyper::Method;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use yajrc::{GenericRpcMethod, Id, RpcError, RpcRequest, RpcResponse};
|
||||||
|
|
||||||
|
use crate::Context;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::{
|
||||||
|
call_remote, default_arg_parser, default_display, default_stdin_parser, make_phantom,
|
||||||
|
match_types,
|
||||||
|
};
|
||||||
|
pub use crate::Context;
|
||||||
|
pub use clap::{App, AppSettings, Arg, ArgMatches};
|
||||||
|
pub use serde::{Deserialize, Serialize};
|
||||||
|
pub use serde_json::{from_value, to_value, Value};
|
||||||
|
pub use std::borrow::Cow;
|
||||||
|
pub use std::marker::PhantomData;
|
||||||
|
pub use tokio::{runtime::Runtime, task::spawn_blocking};
|
||||||
|
pub use yajrc::{self, RpcError};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RequestError {
|
||||||
|
#[error("JSON Error: {0}")]
|
||||||
|
JSON(#[from] serde_json::Error),
|
||||||
|
#[cfg(feature = "cbor")]
|
||||||
|
#[error("CBOR Error: {0}")]
|
||||||
|
CBOR(#[from] serde_cbor::Error),
|
||||||
|
#[error("HTTP Error: {0}")]
|
||||||
|
HTTP(#[from] reqwest::Error),
|
||||||
|
#[error("Missing Content-Type")]
|
||||||
|
MissingContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_phantom<T>(_actual: T) -> PhantomData<T> {
|
||||||
|
PhantomData
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match_types<T>(_: &T, _: &T) {}
|
||||||
|
|
||||||
|
pub async fn call_remote<Ctx: Context, Params: Serialize, Res: for<'de> Deserialize<'de>>(
|
||||||
|
ctx: Ctx,
|
||||||
|
method: &str,
|
||||||
|
params: Params,
|
||||||
|
_return_ty: PhantomData<Res>,
|
||||||
|
) -> Result<RpcResponse<GenericRpcMethod<&str, Params, Res>>, RequestError> {
|
||||||
|
let rpc_req: RpcRequest<GenericRpcMethod<&str, Params, Res>> = RpcRequest {
|
||||||
|
id: Some(Id::Number(0.into())),
|
||||||
|
method: GenericRpcMethod::new(method),
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
let mut req = ctx.client().request(Method::POST, ctx.url());
|
||||||
|
let body;
|
||||||
|
#[cfg(feature = "cbor")]
|
||||||
|
{
|
||||||
|
req = req.header("content-type", "application/cbor");
|
||||||
|
req = req.header("accept", "application/cbor, application/json");
|
||||||
|
body = serde_cbor::to_vec(&rpc_req)?;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "cbor"))]
|
||||||
|
{
|
||||||
|
req = req.header("content-type", "application/json");
|
||||||
|
req = req.header("accept", "application/json");
|
||||||
|
body = serde_json::to_vec(&req)?;
|
||||||
|
}
|
||||||
|
let res = req
|
||||||
|
.header("content-length", body.len())
|
||||||
|
.body(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(
|
||||||
|
match res
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
{
|
||||||
|
Some("application/json") => serde_json::from_slice(&*res.bytes().await?)?,
|
||||||
|
#[cfg(feature = "cbor")]
|
||||||
|
Some("application/cbor") => serde_cbor::from_slice(&*res.bytes().await?)?,
|
||||||
|
_ => return Err(RequestError::MissingContentType),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_arg_parser<T: FromStr<Err = E>, E: Display>(
|
||||||
|
arg: &str,
|
||||||
|
_: &ArgMatches<'_>,
|
||||||
|
) -> Result<T, RpcError> {
|
||||||
|
arg.parse().map_err(|e| RpcError {
|
||||||
|
data: Some(format!("{}", e).into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_stdin_parser<T: FromStr<Err = E>, E: Display>(
|
||||||
|
stdin: &mut Stdin,
|
||||||
|
_: &ArgMatches<'_>,
|
||||||
|
) -> Result<T, RpcError> {
|
||||||
|
let mut s = String::new();
|
||||||
|
stdin.read_line(&mut s).map_err(|e| RpcError {
|
||||||
|
data: Some(format!("{}", e).into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
})?;
|
||||||
|
if let Some(s) = s.strip_suffix("\n") {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
&s
|
||||||
|
}
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| RpcError {
|
||||||
|
data: Some(format!("{}", e).into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_display<T: Display>(t: T) {
|
||||||
|
println!("{}", t)
|
||||||
|
}
|
||||||
39
rpc-toolkit/src/context.rs
Normal file
39
rpc-toolkit/src/context.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use reqwest::Client;
|
||||||
|
use url::{Host, Url};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref DEFAULT_CLIENT: Client = Client::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Context {
|
||||||
|
fn host(&self) -> Host<&str> {
|
||||||
|
Host::Ipv4([127, 0, 0, 1].into())
|
||||||
|
}
|
||||||
|
fn port(&self) -> u16 {
|
||||||
|
8080
|
||||||
|
}
|
||||||
|
fn protocol(&self) -> &str {
|
||||||
|
"http"
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
format!("{}://{}:{}", self.protocol(), self.host(), self.port())
|
||||||
|
.parse()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
fn client(&self) -> &Client {
|
||||||
|
&*DEFAULT_CLIENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SeedableContext<T: Clone>: Context {
|
||||||
|
fn new(seed: T) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for () {}
|
||||||
|
|
||||||
|
impl<T: Copy> SeedableContext<T> for () {
|
||||||
|
fn new(_: T) -> Self {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
57
rpc-toolkit/src/lib.rs
Normal file
57
rpc-toolkit/src/lib.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
pub use crate::context::{Context, SeedableContext};
|
||||||
|
pub use clap;
|
||||||
|
pub use hyper;
|
||||||
|
pub use serde;
|
||||||
|
pub use serde_json;
|
||||||
|
pub use tokio;
|
||||||
|
pub use yajrc;
|
||||||
|
|
||||||
|
/// `#[command(...)]`
|
||||||
|
/// - `#[command(local)]` -> executed by CLI instead of RPC server (leaf commands only)
|
||||||
|
/// - `#[command(remote)]` -> no CLI bindings (leaf commands only)
|
||||||
|
/// - `#[command(blocking)]` -> run with [spawn_blocking](tokio::task::spawn_blocking) if in an async context
|
||||||
|
/// - `#[command(about = "About text")]` -> Set about text for the command
|
||||||
|
/// - `#[command(rename = "new_name")]` -> Set the name of the command to `new_name` in the RPC and CLI
|
||||||
|
/// - `#[command(subcommands(...))]` -> Set this as the parent command for the listed subcommands
|
||||||
|
/// - note: the return type of the decorated function must be the [Context] required by its subcommands, and all args must implement [Clone](std::clone::Clone).
|
||||||
|
/// - `#[command(subcommands(self(self_command_impl)))]` -> If no subcommand is provided, call this function
|
||||||
|
/// - `self_command_impl :: Context ctx, Display res, Into<RpcError> err => ctx -> Result<res, err>`
|
||||||
|
/// - note: [Display](std::fmt::Display) is not required for `res` if it has a custom display function that will take it as input
|
||||||
|
/// - if `self_command_impl` is async, write `self(self_command_impl(async))`
|
||||||
|
/// - if `self_command_impl` is blocking, write `self(self_command_impl(blocking))`
|
||||||
|
/// - default: require a subcommand if subcommands are specified
|
||||||
|
/// - `#[command(display(custom_display_fn))]` -> Use the function `custom_display_fn` to display the command's result (leaf commands only)
|
||||||
|
/// - `custom_display_fn :: res -> ()`
|
||||||
|
/// - note: `res` is the type of the decorated command's output
|
||||||
|
/// - default: `default_display`
|
||||||
|
///
|
||||||
|
/// See also: [arg](rpc_toolkit_macro::arg), [context](rpc_toolkit_macro::context)
|
||||||
|
pub use rpc_toolkit_macro::command;
|
||||||
|
|
||||||
|
/// `rpc_server!(command, seed, status_fn)`
|
||||||
|
/// - returns: [Server](hyper::Server)
|
||||||
|
/// - `command`: path to an rpc command (with the `#[command]` attribute)
|
||||||
|
/// - `seed`: A seed for the [SeedableContext] of the rpc command.
|
||||||
|
/// - `status_fn` (optional): a function that takes a JSON RPC error code (`i32`) and returns a [StatusCode](hyper::StatusCode)
|
||||||
|
/// - default: `|_| StatusCode::OK`
|
||||||
|
pub use rpc_toolkit_macro::rpc_server;
|
||||||
|
|
||||||
|
/// `run_cli!(command, app_mutator, make_seed, exit_fn)`
|
||||||
|
/// - this function does not return
|
||||||
|
/// - `command`: path to an rpc command (with the `#[command]` attribute)
|
||||||
|
/// - `app_mutator` (optional): an expression that returns a mutated app.
|
||||||
|
/// - example: `app => app.arg(Arg::with_name("port").long("port"))`
|
||||||
|
/// - default: `app => app`
|
||||||
|
/// - `make_seed` (optional): an expression that takes [&ArgMatches](clap::ArgMatches) and returns a seed.
|
||||||
|
/// - example: `matches => matches.value_of("port")`
|
||||||
|
/// - default: `matches => matches`
|
||||||
|
/// - `exit_fn` (optional): a function that takes a JSON RPC error code (`i32`) and returns an Exit code (`i32`)
|
||||||
|
/// - default: `|code| code`
|
||||||
|
pub use rpc_toolkit_macro::run_cli;
|
||||||
|
|
||||||
|
pub mod command_helpers;
|
||||||
|
mod context;
|
||||||
|
pub mod rpc_server_helpers;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
105
rpc-toolkit/src/rpc_server_helpers.rs
Normal file
105
rpc-toolkit/src/rpc_server_helpers.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use crate::SeedableContext;
|
||||||
|
use hyper::{
|
||||||
|
body::Buf,
|
||||||
|
server::{conn::AddrIncoming, Builder, Server},
|
||||||
|
Body, Request, Response, StatusCode,
|
||||||
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
use url::Host;
|
||||||
|
use yajrc::{AnyRpcMethod, GenericRpcMethod, Id, RpcError, RpcRequest, RpcResponse};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
#[cfg(feature = "cbor")]
|
||||||
|
static ref CBOR_INTERNAL_ERROR: Vec<u8> =
|
||||||
|
serde_cbor::to_vec(&RpcResponse::<AnyRpcMethod<'static>>::from(yajrc::INTERNAL_ERROR)).unwrap();
|
||||||
|
static ref JSON_INTERNAL_ERROR: Vec<u8> =
|
||||||
|
serde_json::to_vec(&RpcResponse::<AnyRpcMethod<'static>>::from(yajrc::INTERNAL_ERROR)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_builder<Ctx: SeedableContext<Seed>, Seed: Clone>(
|
||||||
|
seed: Seed,
|
||||||
|
) -> (Builder<AddrIncoming>, PhantomData<Ctx>) {
|
||||||
|
let ctx = Ctx::new(seed);
|
||||||
|
let addr = match ctx.host() {
|
||||||
|
Host::Ipv4(ip) => (ip, ctx.port()).into(),
|
||||||
|
Host::Ipv6(ip) => (ip, ctx.port()).into(),
|
||||||
|
Host::Domain(localhost) if localhost == "localhost" => ([127, 0, 0, 1], ctx.port()).into(),
|
||||||
|
_ => ([0, 0, 0, 0], ctx.port()).into(),
|
||||||
|
};
|
||||||
|
(Server::bind(&addr), PhantomData)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind_type<T>(_phantom: PhantomData<T>, actual: T) -> T {
|
||||||
|
actual
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn make_request<Params: for<'de> Deserialize<'de> + 'static>(
|
||||||
|
req: &mut Request<Body>,
|
||||||
|
) -> Result<RpcRequest<GenericRpcMethod<String, Params>>, RpcError> {
|
||||||
|
let body = hyper::body::aggregate(std::mem::replace(req.body_mut(), Body::empty()))
|
||||||
|
.await?
|
||||||
|
.reader();
|
||||||
|
let rpc_req: RpcRequest<GenericRpcMethod<String, Params>>;
|
||||||
|
#[cfg(feature = "cbor")]
|
||||||
|
if req
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
== Some("application/cbor")
|
||||||
|
{
|
||||||
|
rpc_req = serde_cbor::from_reader(body)?;
|
||||||
|
} else {
|
||||||
|
rpc_req = serde_json::from_reader(body)?;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "cbor"))]
|
||||||
|
{
|
||||||
|
rpc_req = serde_json::from_reader(body)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rpc_req)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_response<F: Fn(i32) -> StatusCode>(
|
||||||
|
req: &Request<Body>,
|
||||||
|
res: Result<(Option<Id>, Result<Value, RpcError>), RpcError>,
|
||||||
|
status_code_fn: F,
|
||||||
|
) -> Result<Response<Body>, hyper::http::Error> {
|
||||||
|
let rpc_res: RpcResponse = match res {
|
||||||
|
Ok((id, result)) => RpcResponse { id, result },
|
||||||
|
Err(e) => e.into(),
|
||||||
|
};
|
||||||
|
let body;
|
||||||
|
let mut res = Response::builder();
|
||||||
|
#[cfg(feature = "cbor")]
|
||||||
|
if req
|
||||||
|
.headers()
|
||||||
|
.get("accept")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.iter()
|
||||||
|
.flat_map(|s| s.split(","))
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.any(|s| s == "application/cbor")
|
||||||
|
// prefer cbor if accepted
|
||||||
|
{
|
||||||
|
res = res.header("content-type", "application/cbor");
|
||||||
|
body = serde_cbor::to_vec(&rpc_res).unwrap_or_else(|_| CBOR_INTERNAL_ERROR.clone());
|
||||||
|
} else {
|
||||||
|
res = res.header("content-type", "application/json");
|
||||||
|
body = serde_json::to_vec(&rpc_res).unwrap_or_else(|_| JSON_INTERNAL_ERROR.clone());
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "cbor"))]
|
||||||
|
{
|
||||||
|
res.header("content-type", "application/json");
|
||||||
|
body = serde_json::to_vec(&rpc_res).unwrap_or_else(|_| JSON_INTERNAL_ERROR.clone());
|
||||||
|
}
|
||||||
|
res = res.header("content-length", body.len());
|
||||||
|
res = res.status(match &rpc_res.result {
|
||||||
|
Ok(_) => StatusCode::OK,
|
||||||
|
Err(e) => status_code_fn(e.code),
|
||||||
|
});
|
||||||
|
res.body(Body::from(body))
|
||||||
|
}
|
||||||
182
rpc-toolkit/src/test.rs
Normal file
182
rpc-toolkit/src/test.rs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
pub use crate as rpc_toolkit;
|
||||||
|
use crate::{command, rpc_server, Context, SeedableContext};
|
||||||
|
use clap::Arg;
|
||||||
|
use rpc_toolkit_macro::run_cli;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
use url::Host;
|
||||||
|
use yajrc::RpcError;
|
||||||
|
|
||||||
|
pub struct AppState<T, U> {
|
||||||
|
seed: T,
|
||||||
|
data: U,
|
||||||
|
}
|
||||||
|
impl<T, U> AppState<T, U> {
|
||||||
|
pub fn map<F: FnOnce(U) -> V, V>(self, f: F) -> AppState<T, V> {
|
||||||
|
AppState {
|
||||||
|
seed: self.seed,
|
||||||
|
data: f(self.data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConfigSeed {
|
||||||
|
host: Host,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SeedableContext<Arc<ConfigSeed>> for AppState<Arc<ConfigSeed>, ()> {
|
||||||
|
fn new(seed: Arc<ConfigSeed>) -> Self {
|
||||||
|
AppState {
|
||||||
|
seed: seed.clone(),
|
||||||
|
data: (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Context for AppState<Arc<ConfigSeed>, T> {
|
||||||
|
fn host(&self) -> Host<&str> {
|
||||||
|
match &self.seed.host {
|
||||||
|
Host::Domain(s) => Host::Domain(s.as_str()),
|
||||||
|
Host::Ipv4(i) => Host::Ipv4(*i),
|
||||||
|
Host::Ipv6(i) => Host::Ipv6(*i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn port(&self) -> u16 {
|
||||||
|
self.seed.port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(
|
||||||
|
about = "Does the thing",
|
||||||
|
subcommands("dothething2::<U, E>", self(dothething_impl(async)))
|
||||||
|
)]
|
||||||
|
async fn dothething<
|
||||||
|
U: Serialize + for<'a> Deserialize<'a> + FromStr<Err = E> + Clone,
|
||||||
|
E: Display,
|
||||||
|
>(
|
||||||
|
#[context] ctx: AppState<Arc<ConfigSeed>, ()>,
|
||||||
|
#[arg(short = "a")] arg1: Option<String>,
|
||||||
|
#[arg(short = "b")] val: String,
|
||||||
|
#[arg(short = "c", help = "I am the flag `c`!")] arg3: bool,
|
||||||
|
#[arg(stdin)] structured: U,
|
||||||
|
) -> Result<AppState<Arc<ConfigSeed>, (Option<String>, String, bool, U)>, RpcError> {
|
||||||
|
Ok(ctx.map(|_| (arg1, val, arg3, structured)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dothething_impl<U: Serialize>(
|
||||||
|
ctx: AppState<Arc<ConfigSeed>, (Option<String>, String, bool, U)>,
|
||||||
|
) -> Result<String, RpcError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{:?}, {}, {}, {}",
|
||||||
|
ctx.data.0,
|
||||||
|
ctx.data.1,
|
||||||
|
ctx.data.2,
|
||||||
|
serde_json::to_string_pretty(&ctx.data.3)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(about = "Does the thing")]
|
||||||
|
fn dothething2<U: Serialize + for<'a> Deserialize<'a> + FromStr<Err = E>, E: Display>(
|
||||||
|
#[context] ctx: AppState<Arc<ConfigSeed>, (Option<String>, String, bool, U)>,
|
||||||
|
#[arg(stdin)] structured2: U,
|
||||||
|
) -> Result<String, RpcError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{:?}, {}, {}, {}, {}",
|
||||||
|
ctx.data.0,
|
||||||
|
ctx.data.1,
|
||||||
|
ctx.data.2,
|
||||||
|
serde_json::to_string_pretty(&ctx.data.3)?,
|
||||||
|
serde_json::to_string_pretty(&structured2)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test() {
|
||||||
|
use crate as rpc_toolkit;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
let seed = Arc::new(ConfigSeed {
|
||||||
|
host: Host::parse("localhost").unwrap(),
|
||||||
|
port: 8000,
|
||||||
|
});
|
||||||
|
let server = rpc_server!(dothething::<String, _>, seed);
|
||||||
|
let handle = tokio::spawn(server);
|
||||||
|
let mut cmd = tokio::process::Command::new("cargo")
|
||||||
|
.arg("test")
|
||||||
|
.arg("--package")
|
||||||
|
.arg("rpc-toolkit")
|
||||||
|
.arg("--lib")
|
||||||
|
.arg("--")
|
||||||
|
.arg("test::cli_test")
|
||||||
|
.arg("--exact")
|
||||||
|
.arg("--nocapture")
|
||||||
|
.arg("--ignored")
|
||||||
|
.arg("--")
|
||||||
|
.arg("-b")
|
||||||
|
.arg("test")
|
||||||
|
.arg("dothething2")
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
cmd.stdin
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.write_all(b"TEST\nHAHA")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let out = cmd.wait_with_output().await.unwrap();
|
||||||
|
assert!(out.status.success());
|
||||||
|
assert!(std::str::from_utf8(&out.stdout)
|
||||||
|
.unwrap()
|
||||||
|
.contains("\nNone, test, false, \"TEST\", \"HAHA\"\n"));
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_test() {
|
||||||
|
let app = dothething::build_app();
|
||||||
|
let mut skip = true;
|
||||||
|
let args = std::iter::once(std::ffi::OsString::from("cli_test"))
|
||||||
|
.chain(std::env::args_os().into_iter().skip_while(|a| {
|
||||||
|
if a == "--" {
|
||||||
|
skip = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
skip
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if skip {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let matches = app.get_matches_from(args);
|
||||||
|
let seed = Arc::new(ConfigSeed {
|
||||||
|
host: Host::parse("localhost").unwrap(),
|
||||||
|
port: 8000,
|
||||||
|
});
|
||||||
|
dothething::cli_handler::<String, _, _>(
|
||||||
|
SeedableContext::new(seed),
|
||||||
|
None,
|
||||||
|
&matches,
|
||||||
|
"".into(),
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn cli_example() {
|
||||||
|
run_cli!(
|
||||||
|
dothething::<String, _>,
|
||||||
|
app => app
|
||||||
|
.arg(Arg::with_name("host").long("host").short("h").takes_value(true))
|
||||||
|
.arg(Arg::with_name("port").long("port").short("p").takes_value(true)),
|
||||||
|
matches => Arc::new(ConfigSeed {
|
||||||
|
host: Host::parse(matches.value_of("host").unwrap_or("localhost")).unwrap(),
|
||||||
|
port: matches.value_of("port").unwrap_or("8000").parse().unwrap(),
|
||||||
|
}),
|
||||||
|
|code| if code < 0 { 1 } else { code }
|
||||||
|
)
|
||||||
|
}
|
||||||
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Module"
|
||||||
Reference in New Issue
Block a user