From b246ea4179b959ad8b0fdc75403ce41ab1f22aa6 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 12 Dec 2023 15:12:27 -0700 Subject: [PATCH] finish cli --- rpc-toolkit/src/cli.rs | 183 +++++++++++++++++++++++++++++++++++-- rpc-toolkit/src/command.rs | 120 ++++++++---------------- rpc-toolkit/src/context.rs | 58 +----------- rpc-toolkit/src/lib.rs | 9 +- 4 files changed, 222 insertions(+), 148 deletions(-) diff --git a/rpc-toolkit/src/cli.rs b/rpc-toolkit/src/cli.rs index c2d2aa4..eebeae3 100644 --- a/rpc-toolkit/src/cli.rs +++ b/rpc-toolkit/src/cli.rs @@ -1,4 +1,8 @@ -use clap::ArgMatches; +use std::ffi::OsString; + +use clap::{ArgMatches, CommandFactory, FromArgMatches}; +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; use imbl_value::Value; use reqwest::{Client, Method}; use serde::de::DeserializeOwned; @@ -8,11 +12,178 @@ use yajrc::{GenericRpcMethod, Id, RpcError, RpcRequest}; use crate::command::{AsyncCommand, DynCommand, LeafCommand, ParentInfo}; use crate::util::{combine, invalid_params, parse_error}; -use crate::ParentChain; +use crate::{CliBindings, ParentChain}; -pub struct CliApp { - pub(crate) command: DynCommand, - pub(crate) make_ctx: Box Result>, +impl DynCommand { + fn cli_app(&self) -> Option { + if let Some(cli) = &self.cli { + Some( + cli.cmd + .clone() + .name(self.name) + .subcommands(self.subcommands.iter().filter_map(|c| c.cli_app())), + ) + } else { + None + } + } + fn cmd_from_cli_matches( + &self, + matches: &ArgMatches, + parent: ParentInfo, + ) -> Result<(Vec<&'static str>, Value, &DynCommand), RpcError> { + let params = combine( + parent.params, + (self + .cli + .as_ref() + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .parser)(matches)?, + )?; + if let Some((cmd, matches)) = matches.subcommand() { + let mut method = parent.method; + method.push(self.name); + self.subcommands + .iter() + .find(|c| c.name == cmd) + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .cmd_from_cli_matches(matches, ParentInfo { method, params }) + } else { + Ok((parent.method, params, self)) + } + } +} + +struct CliApp { + cli: CliBindings, + commands: Vec>, +} +impl CliApp { + pub fn new( + commands: Vec>, + ) -> Self { + Self { + cli: CliBindings::from_parent::(), + commands, + } + } + fn cmd_from_cli_matches( + &self, + matches: &ArgMatches, + ) -> Result<(Vec<&'static str>, Value, &DynCommand), RpcError> { + if let Some((cmd, matches)) = matches.subcommand() { + Ok(self + .commands + .iter() + .find(|c| c.name == cmd) + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .cmd_from_cli_matches( + matches, + ParentInfo { + method: Vec::new(), + params: Value::Object(Default::default()), + }, + )?) + } else { + Err(yajrc::METHOD_NOT_FOUND_ERROR) + } + } +} + +pub struct CliAppAsync { + app: CliApp, + make_ctx: Box BoxFuture<'static, Result> + Send>, +} +impl CliAppAsync { + pub fn new< + Cmd: FromArgMatches + CommandFactory + Serialize + DeserializeOwned + Send, + F: FnOnce(Cmd) -> Fut + Send + 'static, + Fut: Future> + Send, + >( + make_ctx: F, + commands: Vec>, + ) -> Self { + Self { + app: CliApp::new::(commands), + make_ctx: Box::new(|args| { + async { make_ctx(imbl_value::from_value(args).map_err(parse_error)?).await }.boxed() + }), + } + } + pub async fn run(self, args: Vec) -> Result<(), RpcError> { + let cmd = self + .app + .cli + .cmd + .clone() + .subcommands(self.app.commands.iter().filter_map(|c| c.cli_app())); + let matches = cmd.get_matches_from(args); + let make_ctx_args = (self.app.cli.parser)(&matches)?; + let ctx = (self.make_ctx)(make_ctx_args).await?; + let (parent_method, params, cmd) = self.app.cmd_from_cli_matches(&matches)?; + let display = &cmd + .cli + .as_ref() + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .display; + let res = (cmd + .implementation + .as_ref() + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .async_impl)(ctx, parent_method, params) + .await?; + if let Some(display) = display { + display(res).map_err(parse_error) + } else { + Ok(()) + } + } +} + +pub struct CliAppSync { + app: CliApp, + make_ctx: Box Result + Send>, +} +impl CliAppSync { + pub fn new< + Cmd: FromArgMatches + CommandFactory + Serialize + DeserializeOwned + Send, + F: FnOnce(Cmd) -> Result + Send + 'static, + >( + make_ctx: F, + commands: Vec>, + ) -> Self { + Self { + app: CliApp::new::(commands), + make_ctx: Box::new(|args| make_ctx(imbl_value::from_value(args).map_err(parse_error)?)), + } + } + pub async fn run(self, args: Vec) -> Result<(), RpcError> { + let cmd = self + .app + .cli + .cmd + .clone() + .subcommands(self.app.commands.iter().filter_map(|c| c.cli_app())); + let matches = cmd.get_matches_from(args); + let make_ctx_args = (self.app.cli.parser)(&matches)?; + let ctx = (self.make_ctx)(make_ctx_args)?; + let (parent_method, params, cmd) = self.app.cmd_from_cli_matches(&matches)?; + let display = &cmd + .cli + .as_ref() + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .display; + let res = (cmd + .implementation + .as_ref() + .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? + .sync_impl)(ctx, parent_method, params)?; + if let Some(display) = display { + display(res).map_err(parse_error) + } else { + Ok(()) + } + } } #[async_trait::async_trait] @@ -98,7 +269,7 @@ where &method.join("."), combine( imbl_value::to_value(&self).map_err(invalid_params)?, - imbl_value::to_value(&parent.args).map_err(invalid_params)?, + imbl_value::to_value(&parent.params).map_err(invalid_params)?, )?, ) .await?, diff --git a/rpc-toolkit/src/command.rs b/rpc-toolkit/src/command.rs index e6147cd..fefd057 100644 --- a/rpc-toolkit/src/command.rs +++ b/rpc-toolkit/src/command.rs @@ -1,5 +1,4 @@ use std::marker::PhantomData; -use std::sync::Arc; use clap::{ArgMatches, CommandFactory, FromArgMatches}; use futures::future::BoxFuture; @@ -7,10 +6,9 @@ use futures::FutureExt; use imbl_value::Value; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use tokio::runtime::Runtime; use yajrc::RpcError; -use crate::util::{combine, extract, Flat}; +use crate::util::{extract, Flat}; /// Stores a command's implementation for a given context /// Can be created from anything that implements ParentCommand, AsyncCommand, or SyncCommand @@ -20,68 +18,21 @@ pub struct DynCommand { pub(crate) cli: Option, pub(crate) subcommands: Vec, } -impl DynCommand { - pub(crate) fn cli_app(&self) -> Option { - if let Some(cli) = &self.cli { - Some( - cli.cmd - .clone() - .name(self.name) - .subcommands(self.subcommands.iter().filter_map(|c| c.cli_app())), - ) - } else { - None - } - } - pub(crate) fn impl_from_cli_matches( - &self, - matches: &ArgMatches, - parent: Value, - ) -> Result, RpcError> { - let args = combine( - parent, - (self - .cli - .as_ref() - .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? - .parser)(matches)?, - )?; - if let Some((cmd, matches)) = matches.subcommand() { - self.subcommands - .iter() - .find(|c| c.name == cmd) - .ok_or(yajrc::METHOD_NOT_FOUND_ERROR)? - .impl_from_cli_matches(matches, args) - } else if let Some(implementation) = self.implementation.clone() { - Ok(implementation) - } else { - Err(yajrc::METHOD_NOT_FOUND_ERROR) - } - } -} -struct Implementation { - pub(crate) async_impl: Arc< +pub(crate) struct Implementation { + pub(crate) async_impl: Box< dyn Fn(Context, Vec<&'static str>, Value) -> BoxFuture<'static, Result>, >, - pub(crate) sync_impl: Arc, Value) -> Result>, -} -impl Clone for Implementation { - fn clone(&self) -> Self { - Self { - async_impl: self.async_impl.clone(), - sync_impl: self.sync_impl.clone(), - } - } + pub(crate) sync_impl: Box, Value) -> Result>, } -struct CliBindings { - cmd: clap::Command, - parser: Box Fn(&'a ArgMatches) -> Result + Send + Sync>, - display: Option Result<(), imbl_value::Error> + Send + Sync>>, +pub(crate) struct CliBindings { + pub(crate) cmd: clap::Command, + pub(crate) parser: Box Fn(&'a ArgMatches) -> Result + Send + Sync>, + pub(crate) display: Option Result<(), imbl_value::Error> + Send + Sync>>, } impl CliBindings { - fn from_parent() -> Self { + pub(crate) fn from_parent() -> Self { Self { cmd: Cmd::command(), parser: Box::new(|matches| { @@ -118,13 +69,18 @@ pub trait Command: DeserializeOwned + Sized + Send { /// Arguments are flattened out in the params object, so ensure that there are no collisions between the names of the arguments for your method and its parents pub struct ParentInfo { pub method: Vec<&'static str>, - pub args: T, + pub params: T, } /// This is automatically generated from a command based on its Parents. /// It can be used to generate a proof that one of the parents contains the necessary arguments that a subcommand requires. pub struct ParentChain(PhantomData); pub struct Contains(PhantomData); +impl Contains { + pub fn none() -> Self { + Self(PhantomData) + } +} impl From<(Contains, Contains)> for Contains> { fn from(_: (Contains, Contains)) -> Self { Self(PhantomData) @@ -142,9 +98,6 @@ impl ParentChain where Cmd: Command, { - pub fn none(&self) -> Contains { - Contains(PhantomData) - } pub fn child(&self) -> Contains { Contains(PhantomData) } @@ -198,20 +151,20 @@ pub trait AsyncCommand: LeafCommand { Vec::new() } } -impl Implementation { +impl Implementation { fn for_async>(contains: Contains) -> Self { drop(contains); Self { - async_impl: Arc::new(|ctx, method, params| { + async_impl: Box::new(|ctx, parent_method, params| { async move { - let parent = extract::(¶ms)?; + let parent_params = extract::(¶ms)?; imbl_value::to_value( &extract::(¶ms)? .implementation( ctx, ParentInfo { - method, - args: parent, + method: parent_method, + params: parent_params, }, ) .await @@ -224,11 +177,10 @@ impl Implementation { } .boxed() }), - sync_impl: Arc::new(|ctx, method, params| { - let parent = extract::(¶ms)?; + sync_impl: Box::new(|ctx, parent_method, params| { + let parent_params = extract::(¶ms)?; imbl_value::to_value( - &Runtime::new() - .unwrap() + &ctx.runtime() .block_on( extract::(¶ms) .map_err(|e| RpcError { @@ -238,8 +190,8 @@ impl Implementation { .implementation( ctx, ParentInfo { - method, - args: parent, + method: parent_method, + params: parent_params, }, ), ) @@ -253,7 +205,7 @@ impl Implementation { } } } -impl DynCommand { +impl DynCommand { pub fn from_async + FromArgMatches + CommandFactory + Serialize>( contains: Contains, ) -> Self { @@ -292,9 +244,9 @@ impl Implementation { drop(contains); Self { async_impl: if Cmd::BLOCKING { - Arc::new(|ctx, method, params| { + Box::new(|ctx, parent_method, params| { tokio::task::spawn_blocking(move || { - let parent = extract::(¶ms)?; + let parent_params = extract::(¶ms)?; imbl_value::to_value( &extract::(¶ms) .map_err(|e| RpcError { @@ -304,8 +256,8 @@ impl Implementation { .implementation( ctx, ParentInfo { - method, - args: parent, + method: parent_method, + params: parent_params, }, ) .map_err(|e| e.into())?, @@ -324,9 +276,9 @@ impl Implementation { .boxed() }) } else { - Arc::new(|ctx, method, params| { + Box::new(|ctx, parent_method, params| { async move { - let parent = extract::(¶ms)?; + let parent_params = extract::(¶ms)?; imbl_value::to_value( &extract::(¶ms) .map_err(|e| RpcError { @@ -336,8 +288,8 @@ impl Implementation { .implementation( ctx, ParentInfo { - method, - args: parent, + method: parent_method, + params: parent_params, }, ) .map_err(|e| e.into())?, @@ -350,7 +302,7 @@ impl Implementation { .boxed() }) }, - sync_impl: Arc::new(|ctx, method, params| { + sync_impl: Box::new(|ctx, method, params| { let parent = extract::(¶ms)?; imbl_value::to_value( &extract::(¶ms) @@ -362,7 +314,7 @@ impl Implementation { ctx, ParentInfo { method, - args: parent, + params: parent, }, ) .map_err(|e| e.into())?, diff --git a/rpc-toolkit/src/context.rs b/rpc-toolkit/src/context.rs index 91e9114..57035cb 100644 --- a/rpc-toolkit/src/context.rs +++ b/rpc-toolkit/src/context.rs @@ -1,57 +1,7 @@ -use std::sync::Arc; +use tokio::runtime::Handle; -use lazy_static::lazy_static; -use reqwest::Client; -use tokio::runtime::Runtime; -use url::{Host, Url}; - -lazy_static! { - static ref DEFAULT_CLIENT: Client = Client::new(); -} - -pub trait Context: Send { - fn runtime(&self) -> Arc { - Arc::new(Runtime::new().unwrap()) - } - fn protocol(&self) -> &str { - "http" - } - fn host(&self) -> Host<&str> { - Host::Ipv4([127, 0, 0, 1].into()) - } - fn port(&self) -> u16 { - 8080 - } - fn path(&self) -> &str { - "/" - } - fn url(&self) -> Url { - let mut url: Url = "http://localhost".parse().unwrap(); - url.set_scheme(self.protocol()).expect("protocol"); - url.set_host(Some(&self.host().to_string())).expect("host"); - url.set_port(Some(self.port())).expect("port"); - url.set_path(self.path()); - url - } - fn client(&self) -> &Client { - &*DEFAULT_CLIENT - } -} - -impl Context for () {} - -impl<'a, T: Context + 'a> From for Box { - fn from(ctx: T) -> Self { - Box::new(ctx) - } -} - -impl Context for (T, U) -where - T: Context, - U: Send, -{ - fn runtime(&self) -> Arc { - self.0.runtime() +pub trait Context: Send + 'static { + fn runtime(&self) -> Handle { + Handle::current() } } diff --git a/rpc-toolkit/src/lib.rs b/rpc-toolkit/src/lib.rs index 1613002..e4e47f9 100644 --- a/rpc-toolkit/src/lib.rs +++ b/rpc-toolkit/src/lib.rs @@ -1,5 +1,6 @@ pub use cli::*; pub use command::*; +pub use context::Context; /// `#[command(...)]` /// - `#[command(cli_only)]` -> executed by CLI instead of RPC server (leaf commands only) /// - `#[command(rpc_only)]` -> no CLI bindings (leaf commands only) @@ -24,10 +25,10 @@ pub use command::*; pub use rpc_toolkit_macro::command; pub use {clap, futures, hyper, reqwest, serde, serde_json, tokio, url, yajrc}; -pub(crate) mod cli; -pub(crate) mod command; +mod cli; +mod command; // pub mod command_helpers; -// mod context; +mod context; // mod metadata; // pub mod rpc_server_helpers; -pub(crate) mod util; +mod util;