mirror of
https://github.com/Start9Labs/rpc-toolkit.git
synced 2026-03-26 02:11:56 +00:00
finish cli
This commit is contained in:
@@ -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<Context> {
|
||||
pub(crate) command: DynCommand<Context>,
|
||||
pub(crate) make_ctx: Box<dyn FnOnce(&ArgMatches) -> Result<Context, RpcError>>,
|
||||
impl<Context> DynCommand<Context> {
|
||||
fn cli_app(&self) -> Option<clap::Command> {
|
||||
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<Value>,
|
||||
) -> Result<(Vec<&'static str>, Value, &DynCommand<Context>), 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<Context> {
|
||||
cli: CliBindings,
|
||||
commands: Vec<DynCommand<Context>>,
|
||||
}
|
||||
impl<Context> CliApp<Context> {
|
||||
pub fn new<Cmd: FromArgMatches + CommandFactory + Serialize>(
|
||||
commands: Vec<DynCommand<Context>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cli: CliBindings::from_parent::<Cmd>(),
|
||||
commands,
|
||||
}
|
||||
}
|
||||
fn cmd_from_cli_matches(
|
||||
&self,
|
||||
matches: &ArgMatches,
|
||||
) -> Result<(Vec<&'static str>, Value, &DynCommand<Context>), 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<Context> {
|
||||
app: CliApp<Context>,
|
||||
make_ctx: Box<dyn FnOnce(Value) -> BoxFuture<'static, Result<Context, RpcError>> + Send>,
|
||||
}
|
||||
impl<Context> CliAppAsync<Context> {
|
||||
pub fn new<
|
||||
Cmd: FromArgMatches + CommandFactory + Serialize + DeserializeOwned + Send,
|
||||
F: FnOnce(Cmd) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = Result<Context, RpcError>> + Send,
|
||||
>(
|
||||
make_ctx: F,
|
||||
commands: Vec<DynCommand<Context>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
app: CliApp::new::<Cmd>(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<OsString>) -> 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<Context> {
|
||||
app: CliApp<Context>,
|
||||
make_ctx: Box<dyn FnOnce(Value) -> Result<Context, RpcError> + Send>,
|
||||
}
|
||||
impl<Context> CliAppSync<Context> {
|
||||
pub fn new<
|
||||
Cmd: FromArgMatches + CommandFactory + Serialize + DeserializeOwned + Send,
|
||||
F: FnOnce(Cmd) -> Result<Context, RpcError> + Send + 'static,
|
||||
>(
|
||||
make_ctx: F,
|
||||
commands: Vec<DynCommand<Context>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
app: CliApp::new::<Cmd>(commands),
|
||||
make_ctx: Box::new(|args| make_ctx(imbl_value::from_value(args).map_err(parse_error)?)),
|
||||
}
|
||||
}
|
||||
pub async fn run(self, args: Vec<OsString>) -> 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?,
|
||||
|
||||
@@ -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<Context> {
|
||||
pub(crate) cli: Option<CliBindings>,
|
||||
pub(crate) subcommands: Vec<Self>,
|
||||
}
|
||||
impl<Context> DynCommand<Context> {
|
||||
pub(crate) fn cli_app(&self) -> Option<clap::Command> {
|
||||
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<Implementation<Context>, 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<Context> {
|
||||
pub(crate) async_impl: Arc<
|
||||
pub(crate) struct Implementation<Context> {
|
||||
pub(crate) async_impl: Box<
|
||||
dyn Fn(Context, Vec<&'static str>, Value) -> BoxFuture<'static, Result<Value, RpcError>>,
|
||||
>,
|
||||
pub(crate) sync_impl: Arc<dyn Fn(Context, Vec<&'static str>, Value) -> Result<Value, RpcError>>,
|
||||
}
|
||||
impl<Context> Clone for Implementation<Context> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
async_impl: self.async_impl.clone(),
|
||||
sync_impl: self.sync_impl.clone(),
|
||||
}
|
||||
}
|
||||
pub(crate) sync_impl: Box<dyn Fn(Context, Vec<&'static str>, Value) -> Result<Value, RpcError>>,
|
||||
}
|
||||
|
||||
struct CliBindings {
|
||||
cmd: clap::Command,
|
||||
parser: Box<dyn for<'a> Fn(&'a ArgMatches) -> Result<Value, RpcError> + Send + Sync>,
|
||||
display: Option<Box<dyn Fn(Value) -> Result<(), imbl_value::Error> + Send + Sync>>,
|
||||
pub(crate) struct CliBindings {
|
||||
pub(crate) cmd: clap::Command,
|
||||
pub(crate) parser: Box<dyn for<'a> Fn(&'a ArgMatches) -> Result<Value, RpcError> + Send + Sync>,
|
||||
pub(crate) display: Option<Box<dyn Fn(Value) -> Result<(), imbl_value::Error> + Send + Sync>>,
|
||||
}
|
||||
impl CliBindings {
|
||||
fn from_parent<Cmd: FromArgMatches + CommandFactory + Serialize>() -> Self {
|
||||
pub(crate) fn from_parent<Cmd: FromArgMatches + CommandFactory + Serialize>() -> 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<T> {
|
||||
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<Cmd: Command>(PhantomData<Cmd>);
|
||||
pub struct Contains<T>(PhantomData<T>);
|
||||
impl Contains<NoParent> {
|
||||
pub fn none() -> Self {
|
||||
Self(PhantomData)
|
||||
}
|
||||
}
|
||||
impl<T, U> From<(Contains<T>, Contains<U>)> for Contains<Flat<T, U>> {
|
||||
fn from(_: (Contains<T>, Contains<U>)) -> Self {
|
||||
Self(PhantomData)
|
||||
@@ -142,9 +98,6 @@ impl<Cmd> ParentChain<Cmd>
|
||||
where
|
||||
Cmd: Command,
|
||||
{
|
||||
pub fn none(&self) -> Contains<NoParent> {
|
||||
Contains(PhantomData)
|
||||
}
|
||||
pub fn child(&self) -> Contains<Cmd> {
|
||||
Contains(PhantomData)
|
||||
}
|
||||
@@ -198,20 +151,20 @@ pub trait AsyncCommand<Context>: LeafCommand {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
impl<Context: Send + 'static> Implementation<Context> {
|
||||
impl<Context: crate::Context> Implementation<Context> {
|
||||
fn for_async<Cmd: AsyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
||||
drop(contains);
|
||||
Self {
|
||||
async_impl: Arc::new(|ctx, method, params| {
|
||||
async_impl: Box::new(|ctx, parent_method, params| {
|
||||
async move {
|
||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||
let parent_params = extract::<Cmd::Parent>(¶ms)?;
|
||||
imbl_value::to_value(
|
||||
&extract::<Cmd>(¶ms)?
|
||||
.implementation(
|
||||
ctx,
|
||||
ParentInfo {
|
||||
method,
|
||||
args: parent,
|
||||
method: parent_method,
|
||||
params: parent_params,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -224,11 +177,10 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
}
|
||||
.boxed()
|
||||
}),
|
||||
sync_impl: Arc::new(|ctx, method, params| {
|
||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||
sync_impl: Box::new(|ctx, parent_method, params| {
|
||||
let parent_params = extract::<Cmd::Parent>(¶ms)?;
|
||||
imbl_value::to_value(
|
||||
&Runtime::new()
|
||||
.unwrap()
|
||||
&ctx.runtime()
|
||||
.block_on(
|
||||
extract::<Cmd>(¶ms)
|
||||
.map_err(|e| RpcError {
|
||||
@@ -238,8 +190,8 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
.implementation(
|
||||
ctx,
|
||||
ParentInfo {
|
||||
method,
|
||||
args: parent,
|
||||
method: parent_method,
|
||||
params: parent_params,
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -253,7 +205,7 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Context: Send + 'static> DynCommand<Context> {
|
||||
impl<Context: crate::Context> DynCommand<Context> {
|
||||
pub fn from_async<Cmd: AsyncCommand<Context> + FromArgMatches + CommandFactory + Serialize>(
|
||||
contains: Contains<Cmd::Parent>,
|
||||
) -> Self {
|
||||
@@ -292,9 +244,9 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
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::<Cmd::Parent>(¶ms)?;
|
||||
let parent_params = extract::<Cmd::Parent>(¶ms)?;
|
||||
imbl_value::to_value(
|
||||
&extract::<Cmd>(¶ms)
|
||||
.map_err(|e| RpcError {
|
||||
@@ -304,8 +256,8 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
.implementation(
|
||||
ctx,
|
||||
ParentInfo {
|
||||
method,
|
||||
args: parent,
|
||||
method: parent_method,
|
||||
params: parent_params,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.into())?,
|
||||
@@ -324,9 +276,9 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
.boxed()
|
||||
})
|
||||
} else {
|
||||
Arc::new(|ctx, method, params| {
|
||||
Box::new(|ctx, parent_method, params| {
|
||||
async move {
|
||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||
let parent_params = extract::<Cmd::Parent>(¶ms)?;
|
||||
imbl_value::to_value(
|
||||
&extract::<Cmd>(¶ms)
|
||||
.map_err(|e| RpcError {
|
||||
@@ -336,8 +288,8 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
.implementation(
|
||||
ctx,
|
||||
ParentInfo {
|
||||
method,
|
||||
args: parent,
|
||||
method: parent_method,
|
||||
params: parent_params,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.into())?,
|
||||
@@ -350,7 +302,7 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
.boxed()
|
||||
})
|
||||
},
|
||||
sync_impl: Arc::new(|ctx, method, params| {
|
||||
sync_impl: Box::new(|ctx, method, params| {
|
||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||
imbl_value::to_value(
|
||||
&extract::<Cmd>(¶ms)
|
||||
@@ -362,7 +314,7 @@ impl<Context: Send + 'static> Implementation<Context> {
|
||||
ctx,
|
||||
ParentInfo {
|
||||
method,
|
||||
args: parent,
|
||||
params: parent,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.into())?,
|
||||
|
||||
@@ -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<Runtime> {
|
||||
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<T> for Box<dyn Context + 'a> {
|
||||
fn from(ctx: T) -> Self {
|
||||
Box::new(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Context for (T, U)
|
||||
where
|
||||
T: Context,
|
||||
U: Send,
|
||||
{
|
||||
fn runtime(&self) -> Arc<Runtime> {
|
||||
self.0.runtime()
|
||||
pub trait Context: Send + 'static {
|
||||
fn runtime(&self) -> Handle {
|
||||
Handle::current()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user