create rpc toolkit

This commit is contained in:
Aiden McClelland
2021-04-01 18:00:16 -06:00
commit dd22dfacc2
26 changed files with 3524 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
/target
Cargo.lock

View 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"] }

View 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, &params);
let cli_handler = cli_handler(fn_name, fn_generics, &mut opt, &params);
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
}

View 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),
}

View 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)
}

View 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;

View 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)
}
}
}

View File

@@ -0,0 +1,10 @@
use syn::*;
pub struct RpcServerArgs {
command: Path,
seed: Expr,
status_fn: Option<Expr>,
}
pub mod build;
mod parse;

View 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,
})
}
}

View 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)
}
}
}
}

View 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;

View 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,
})
}
}