mirror of
https://github.com/Start9Labs/rpc-toolkit.git
synced 2026-03-26 02:11:56 +00:00
clean up, add docs
This commit is contained in:
108
rpc-toolkit/src/cli.rs
Normal file
108
rpc-toolkit/src/cli.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use clap::ArgMatches;
|
||||||
|
use imbl_value::Value;
|
||||||
|
use reqwest::{Client, Method};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
use url::Url;
|
||||||
|
use yajrc::{GenericRpcMethod, Id, RpcError, RpcRequest};
|
||||||
|
|
||||||
|
use crate::command::{AsyncCommand, DynCommand, LeafCommand, ParentInfo};
|
||||||
|
use crate::util::{combine, invalid_params, parse_error};
|
||||||
|
use crate::ParentChain;
|
||||||
|
|
||||||
|
pub struct CliApp<Context> {
|
||||||
|
pub(crate) command: DynCommand<Context>,
|
||||||
|
pub(crate) make_ctx: Box<dyn FnOnce(&ArgMatches) -> Result<Context, RpcError>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait CliContext {
|
||||||
|
async fn call_remote(&self, method: &str, params: Value) -> Result<Value, RpcError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CliContextHttp {
|
||||||
|
fn client(&self) -> &Client;
|
||||||
|
fn url(&self) -> Url;
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T: CliContextHttp + Sync> CliContext for T {
|
||||||
|
async fn call_remote(&self, method: &str, params: Value) -> Result<Value, RpcError> {
|
||||||
|
let rpc_req: RpcRequest<GenericRpcMethod<&str, Value, Value>> = RpcRequest {
|
||||||
|
id: Some(Id::Number(0.into())),
|
||||||
|
method: GenericRpcMethod::new(method),
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
let mut req = self.client().request(Method::POST, self.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(RpcError {
|
||||||
|
data: Some("missing content type".into()),
|
||||||
|
..yajrc::INTERNAL_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RemoteCommand<Context: CliContext>: LeafCommand {
|
||||||
|
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>> {
|
||||||
|
drop(chain);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T, Context> AsyncCommand<Context> for T
|
||||||
|
where
|
||||||
|
T: RemoteCommand<Context> + Send + Serialize,
|
||||||
|
T::Parent: Serialize,
|
||||||
|
T::Ok: DeserializeOwned,
|
||||||
|
T::Err: From<RpcError>,
|
||||||
|
Context: CliContext + Send + 'static,
|
||||||
|
{
|
||||||
|
async fn implementation(
|
||||||
|
self,
|
||||||
|
ctx: Context,
|
||||||
|
parent: ParentInfo<Self::Parent>,
|
||||||
|
) -> Result<Self::Ok, Self::Err> {
|
||||||
|
let mut method = parent.method;
|
||||||
|
method.push(Self::NAME);
|
||||||
|
Ok(imbl_value::from_value(
|
||||||
|
ctx.call_remote(
|
||||||
|
&method.join("."),
|
||||||
|
combine(
|
||||||
|
imbl_value::to_value(&self).map_err(invalid_params)?,
|
||||||
|
imbl_value::to_value(&parent.args).map_err(invalid_params)?,
|
||||||
|
)?,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
.map_err(parse_error)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,17 +10,22 @@ use serde::ser::Serialize;
|
|||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use yajrc::RpcError;
|
use yajrc::RpcError;
|
||||||
|
|
||||||
|
use crate::util::{combine, extract, Flat};
|
||||||
|
|
||||||
|
/// Stores a command's implementation for a given context
|
||||||
|
/// Can be created from anything that implements ParentCommand, AsyncCommand, or SyncCommand
|
||||||
pub struct DynCommand<Context> {
|
pub struct DynCommand<Context> {
|
||||||
name: &'static str,
|
pub(crate) name: &'static str,
|
||||||
implementation: Option<Implementation<Context>>,
|
pub(crate) implementation: Option<Implementation<Context>>,
|
||||||
cli: Option<CliBindings>,
|
pub(crate) cli: Option<CliBindings>,
|
||||||
subcommands: Vec<Self>,
|
pub(crate) subcommands: Vec<Self>,
|
||||||
}
|
}
|
||||||
impl<Context> DynCommand<Context> {
|
impl<Context> DynCommand<Context> {
|
||||||
fn cli_app(&self) -> Option<clap::Command> {
|
pub(crate) fn cli_app(&self) -> Option<clap::Command> {
|
||||||
if let Some(cli) = &self.cli {
|
if let Some(cli) = &self.cli {
|
||||||
Some(
|
Some(
|
||||||
cli.cmd
|
cli.cmd
|
||||||
|
.clone()
|
||||||
.name(self.name)
|
.name(self.name)
|
||||||
.subcommands(self.subcommands.iter().filter_map(|c| c.cli_app())),
|
.subcommands(self.subcommands.iter().filter_map(|c| c.cli_app())),
|
||||||
)
|
)
|
||||||
@@ -28,7 +33,7 @@ impl<Context> DynCommand<Context> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn impl_from_cli_matches(
|
pub(crate) fn impl_from_cli_matches(
|
||||||
&self,
|
&self,
|
||||||
matches: &ArgMatches,
|
matches: &ArgMatches,
|
||||||
parent: Value,
|
parent: Value,
|
||||||
@@ -53,12 +58,13 @@ impl<Context> DynCommand<Context> {
|
|||||||
Err(yajrc::METHOD_NOT_FOUND_ERROR)
|
Err(yajrc::METHOD_NOT_FOUND_ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn run_cli(ctx: Context) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Implementation<Context> {
|
struct Implementation<Context> {
|
||||||
async_impl: Arc<dyn Fn(Context, Value) -> BoxFuture<'static, Result<Value, RpcError>>>,
|
pub(crate) async_impl: Arc<
|
||||||
sync_impl: Arc<dyn Fn(Context, Value) -> Result<Value, RpcError>>,
|
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> {
|
impl<Context> Clone for Implementation<Context> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
@@ -72,7 +78,7 @@ impl<Context> Clone for Implementation<Context> {
|
|||||||
struct CliBindings {
|
struct CliBindings {
|
||||||
cmd: clap::Command,
|
cmd: clap::Command,
|
||||||
parser: Box<dyn for<'a> Fn(&'a ArgMatches) -> Result<Value, RpcError> + Send + Sync>,
|
parser: Box<dyn for<'a> Fn(&'a ArgMatches) -> Result<Value, RpcError> + Send + Sync>,
|
||||||
display: Option<Box<dyn Fn(Value) + Send + Sync>>,
|
display: Option<Box<dyn Fn(Value) -> Result<(), imbl_value::Error> + Send + Sync>>,
|
||||||
}
|
}
|
||||||
impl CliBindings {
|
impl CliBindings {
|
||||||
fn from_parent<Cmd: FromArgMatches + CommandFactory + Serialize>() -> Self {
|
fn from_parent<Cmd: FromArgMatches + CommandFactory + Serialize>() -> Self {
|
||||||
@@ -93,36 +99,50 @@ impl CliBindings {
|
|||||||
}
|
}
|
||||||
fn from_leaf<Cmd: FromArgMatches + CommandFactory + Serialize + LeafCommand>() -> Self {
|
fn from_leaf<Cmd: FromArgMatches + CommandFactory + Serialize + LeafCommand>() -> Self {
|
||||||
Self {
|
Self {
|
||||||
display: Some(Box::new(|res| Cmd::display(todo!("{}", res)))),
|
display: Some(Box::new(|res| {
|
||||||
|
Ok(Cmd::display(imbl_value::from_value(res)?))
|
||||||
|
})),
|
||||||
..Self::from_parent::<Cmd>()
|
..Self::from_parent::<Cmd>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Command: DeserializeOwned + Sized {
|
/// Must be implemented for all commands
|
||||||
|
/// Use `Parent = NoParent` if the implementation requires no arguments from the parent command
|
||||||
|
pub trait Command: DeserializeOwned + Sized + Send {
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
type Parent: Command;
|
type Parent: Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Includes the parent method, and the arguments requested from the parent
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 ParentChain<Cmd: Command>(PhantomData<Cmd>);
|
||||||
pub struct Contains<T>(PhantomData<T>);
|
pub struct Contains<T>(PhantomData<T>);
|
||||||
impl<T, U> From<(Contains<T>, Contains<U>)> for Contains<(T, U)> {
|
impl<T, U> From<(Contains<T>, Contains<U>)> for Contains<Flat<T, U>> {
|
||||||
fn from(value: (Contains<T>, Contains<U>)) -> Self {
|
fn from(_: (Contains<T>, Contains<U>)) -> Self {
|
||||||
Self(PhantomData)
|
Self(PhantomData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use this as a Parent if your command does not require any arguments from its parents
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Root {}
|
pub struct NoParent {}
|
||||||
impl Command for Root {
|
impl Command for NoParent {
|
||||||
const NAME: &'static str = "";
|
const NAME: &'static str = "";
|
||||||
type Parent = Root;
|
type Parent = NoParent;
|
||||||
}
|
}
|
||||||
impl<Cmd> ParentChain<Cmd>
|
impl<Cmd> ParentChain<Cmd>
|
||||||
where
|
where
|
||||||
Cmd: Command,
|
Cmd: Command,
|
||||||
{
|
{
|
||||||
pub fn unit(&self) -> Contains<()> {
|
pub fn none(&self) -> Contains<NoParent> {
|
||||||
Contains(PhantomData)
|
Contains(PhantomData)
|
||||||
}
|
}
|
||||||
pub fn child(&self) -> Contains<Cmd> {
|
pub fn child(&self) -> Contains<Cmd> {
|
||||||
@@ -133,6 +153,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement this for a command that has no implementation, but simply exists to organize subcommands
|
||||||
pub trait ParentCommand<Context>: Command {
|
pub trait ParentCommand<Context>: Command {
|
||||||
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>>;
|
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>>;
|
||||||
}
|
}
|
||||||
@@ -147,34 +168,52 @@ impl<Context> DynCommand<Context> {
|
|||||||
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn from_parent_no_cli<Cmd: ParentCommand<Context>>() -> Self {
|
||||||
|
Self {
|
||||||
|
name: Cmd::NAME,
|
||||||
|
implementation: None,
|
||||||
|
cli: None,
|
||||||
|
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement this for any command with an implementation
|
||||||
pub trait LeafCommand: Command {
|
pub trait LeafCommand: Command {
|
||||||
type Ok: Serialize;
|
type Ok: DeserializeOwned + Serialize + Send;
|
||||||
type Err: Into<RpcError>;
|
type Err: From<RpcError> + Into<RpcError> + Send;
|
||||||
fn display(res: Self::Ok);
|
fn display(res: Self::Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement this if your Command's implementation is async
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait AsyncCommand<Context>: LeafCommand {
|
pub trait AsyncCommand<Context>: LeafCommand {
|
||||||
async fn implementation(
|
async fn implementation(
|
||||||
self,
|
self,
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
parent: Self::Parent,
|
parent: ParentInfo<Self::Parent>,
|
||||||
) -> Result<Self::Ok, Self::Err>;
|
) -> Result<Self::Ok, Self::Err>;
|
||||||
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>> {
|
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>> {
|
||||||
|
drop(chain);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<Context: Send> Implementation<Context> {
|
impl<Context: Send + 'static> Implementation<Context> {
|
||||||
fn for_async<Cmd: AsyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
fn for_async<Cmd: AsyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
||||||
|
drop(contains);
|
||||||
Self {
|
Self {
|
||||||
async_impl: Arc::new(|ctx, params| {
|
async_impl: Arc::new(|ctx, method, params| {
|
||||||
async move {
|
async move {
|
||||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||||
imbl_value::to_value(
|
imbl_value::to_value(
|
||||||
&extract::<Cmd>(¶ms)?
|
&extract::<Cmd>(¶ms)?
|
||||||
.implementation(ctx, parent)
|
.implementation(
|
||||||
|
ctx,
|
||||||
|
ParentInfo {
|
||||||
|
method,
|
||||||
|
args: parent,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.into())?,
|
.map_err(|e| e.into())?,
|
||||||
)
|
)
|
||||||
@@ -185,7 +224,7 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}),
|
}),
|
||||||
sync_impl: Arc::new(|ctx, params| {
|
sync_impl: Arc::new(|ctx, method, params| {
|
||||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||||
imbl_value::to_value(
|
imbl_value::to_value(
|
||||||
&Runtime::new()
|
&Runtime::new()
|
||||||
@@ -196,7 +235,13 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
data: Some(e.to_string().into()),
|
data: Some(e.to_string().into()),
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
})?
|
})?
|
||||||
.implementation(ctx, parent),
|
.implementation(
|
||||||
|
ctx,
|
||||||
|
ParentInfo {
|
||||||
|
method,
|
||||||
|
args: parent,
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map_err(|e| e.into())?,
|
.map_err(|e| e.into())?,
|
||||||
)
|
)
|
||||||
@@ -208,7 +253,7 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<Context: Send> DynCommand<Context> {
|
impl<Context: Send + 'static> DynCommand<Context> {
|
||||||
pub fn from_async<Cmd: AsyncCommand<Context> + FromArgMatches + CommandFactory + Serialize>(
|
pub fn from_async<Cmd: AsyncCommand<Context> + FromArgMatches + CommandFactory + Serialize>(
|
||||||
contains: Contains<Cmd::Parent>,
|
contains: Contains<Cmd::Parent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -219,20 +264,35 @@ impl<Context: Send> DynCommand<Context> {
|
|||||||
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn from_async_no_cli<Cmd: AsyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Cmd::NAME,
|
||||||
|
implementation: Some(Implementation::for_async::<Cmd>(contains)),
|
||||||
|
cli: None,
|
||||||
|
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement this if your Command's implementation is not async
|
||||||
pub trait SyncCommand<Context>: LeafCommand {
|
pub trait SyncCommand<Context>: LeafCommand {
|
||||||
const BLOCKING: bool;
|
const BLOCKING: bool;
|
||||||
fn implementation(self, ctx: Context, parent: Self::Parent) -> Result<Self::Ok, Self::Err>;
|
fn implementation(
|
||||||
|
self,
|
||||||
|
ctx: Context,
|
||||||
|
parent: ParentInfo<Self::Parent>,
|
||||||
|
) -> Result<Self::Ok, Self::Err>;
|
||||||
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>> {
|
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>> {
|
||||||
|
drop(chain);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<Context: Send> Implementation<Context> {
|
impl<Context: Send + 'static> Implementation<Context> {
|
||||||
fn for_sync<Cmd: SyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
fn for_sync<Cmd: SyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
||||||
|
drop(contains);
|
||||||
Self {
|
Self {
|
||||||
async_impl: if Cmd::BLOCKING {
|
async_impl: if Cmd::BLOCKING {
|
||||||
Arc::new(|ctx, params| {
|
Arc::new(|ctx, method, params| {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||||
imbl_value::to_value(
|
imbl_value::to_value(
|
||||||
@@ -241,7 +301,13 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
data: Some(e.to_string().into()),
|
data: Some(e.to_string().into()),
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
})?
|
})?
|
||||||
.implementation(ctx, parent)
|
.implementation(
|
||||||
|
ctx,
|
||||||
|
ParentInfo {
|
||||||
|
method,
|
||||||
|
args: parent,
|
||||||
|
},
|
||||||
|
)
|
||||||
.map_err(|e| e.into())?,
|
.map_err(|e| e.into())?,
|
||||||
)
|
)
|
||||||
.map_err(|e| RpcError {
|
.map_err(|e| RpcError {
|
||||||
@@ -258,7 +324,7 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Arc::new(|ctx, params| {
|
Arc::new(|ctx, method, params| {
|
||||||
async move {
|
async move {
|
||||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||||
imbl_value::to_value(
|
imbl_value::to_value(
|
||||||
@@ -267,7 +333,13 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
data: Some(e.to_string().into()),
|
data: Some(e.to_string().into()),
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
})?
|
})?
|
||||||
.implementation(ctx, parent)
|
.implementation(
|
||||||
|
ctx,
|
||||||
|
ParentInfo {
|
||||||
|
method,
|
||||||
|
args: parent,
|
||||||
|
},
|
||||||
|
)
|
||||||
.map_err(|e| e.into())?,
|
.map_err(|e| e.into())?,
|
||||||
)
|
)
|
||||||
.map_err(|e| RpcError {
|
.map_err(|e| RpcError {
|
||||||
@@ -278,7 +350,7 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sync_impl: Arc::new(|ctx, params| {
|
sync_impl: Arc::new(|ctx, method, params| {
|
||||||
let parent = extract::<Cmd::Parent>(¶ms)?;
|
let parent = extract::<Cmd::Parent>(¶ms)?;
|
||||||
imbl_value::to_value(
|
imbl_value::to_value(
|
||||||
&extract::<Cmd>(¶ms)
|
&extract::<Cmd>(¶ms)
|
||||||
@@ -286,7 +358,13 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
data: Some(e.to_string().into()),
|
data: Some(e.to_string().into()),
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
})?
|
})?
|
||||||
.implementation(ctx, parent)
|
.implementation(
|
||||||
|
ctx,
|
||||||
|
ParentInfo {
|
||||||
|
method,
|
||||||
|
args: parent,
|
||||||
|
},
|
||||||
|
)
|
||||||
.map_err(|e| e.into())?,
|
.map_err(|e| e.into())?,
|
||||||
)
|
)
|
||||||
.map_err(|e| RpcError {
|
.map_err(|e| RpcError {
|
||||||
@@ -297,7 +375,7 @@ impl<Context: Send> Implementation<Context> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<Context: Send> DynCommand<Context> {
|
impl<Context: Send + 'static> DynCommand<Context> {
|
||||||
pub fn from_sync<Cmd: SyncCommand<Context> + FromArgMatches + CommandFactory + Serialize>(
|
pub fn from_sync<Cmd: SyncCommand<Context> + FromArgMatches + CommandFactory + Serialize>(
|
||||||
contains: Contains<Cmd::Parent>,
|
contains: Contains<Cmd::Parent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -308,29 +386,12 @@ impl<Context: Send> DynCommand<Context> {
|
|||||||
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
pub fn from_sync_no_cli<Cmd: SyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
|
||||||
|
Self {
|
||||||
fn extract<T: DeserializeOwned>(value: &Value) -> Result<T, RpcError> {
|
name: Cmd::NAME,
|
||||||
imbl_value::from_value(value.clone()).map_err(|e| RpcError {
|
implementation: Some(Implementation::for_sync::<Cmd>(contains)),
|
||||||
data: Some(e.to_string().into()),
|
cli: None,
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn combine(v1: Value, v2: Value) -> Result<Value, RpcError> {
|
|
||||||
let (Value::Object(mut v1), Value::Object(v2)) = (v1, v2) else {
|
|
||||||
return Err(RpcError {
|
|
||||||
data: Some("params must be object".into()),
|
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
|
||||||
});
|
|
||||||
};
|
|
||||||
for (key, value) in v2 {
|
|
||||||
if v1.insert(key.clone(), value).is_some() {
|
|
||||||
return Err(RpcError {
|
|
||||||
data: Some(format!("duplicate key: {key}").into()),
|
|
||||||
..yajrc::INVALID_PARAMS_ERROR
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Value::Object(v1))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub use cli::*;
|
||||||
|
pub use command::*;
|
||||||
/// `#[command(...)]`
|
/// `#[command(...)]`
|
||||||
/// - `#[command(cli_only)]` -> executed by CLI instead of RPC server (leaf commands only)
|
/// - `#[command(cli_only)]` -> executed by CLI instead of RPC server (leaf commands only)
|
||||||
/// - `#[command(rpc_only)]` -> no CLI bindings (leaf commands only)
|
/// - `#[command(rpc_only)]` -> no CLI bindings (leaf commands only)
|
||||||
@@ -20,40 +22,12 @@
|
|||||||
///
|
///
|
||||||
/// See also: [arg](rpc_toolkit_macro::arg), [context](rpc_toolkit_macro::context)
|
/// See also: [arg](rpc_toolkit_macro::arg), [context](rpc_toolkit_macro::context)
|
||||||
pub use rpc_toolkit_macro::command;
|
pub use rpc_toolkit_macro::command;
|
||||||
/// `rpc_handler!(command, context, status_fn)`
|
|
||||||
/// - returns: [RpcHandler](rpc_toolkit::RpcHandler)
|
|
||||||
/// - `command`: path to an rpc command (with the `#[command]` attribute)
|
|
||||||
/// - `context`: The [Context] for `command`. Must implement [Clone](std::clone::Clone).
|
|
||||||
/// - `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_handler;
|
|
||||||
/// `rpc_server!(command, context, status_fn)`
|
|
||||||
/// - returns: [Server](hyper::Server)
|
|
||||||
/// - `command`: path to an rpc command (with the `#[command]` attribute)
|
|
||||||
/// - `context`: The [Context] for `command`. Must implement [Clone](std::clone::Clone).
|
|
||||||
/// - `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_ctx, 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_ctx` (optional): an expression that takes [&ArgMatches](clap::ArgMatches) and returns the [Context] used by `command`.
|
|
||||||
/// - 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 use {clap, futures, hyper, reqwest, serde, serde_json, tokio, url, yajrc};
|
pub use {clap, futures, hyper, reqwest, serde, serde_json, tokio, url, yajrc};
|
||||||
|
|
||||||
pub use crate::context::Context;
|
pub(crate) mod cli;
|
||||||
pub use crate::metadata::Metadata;
|
pub(crate) mod command;
|
||||||
pub use crate::rpc_server_helpers::RpcHandler;
|
// pub mod command_helpers;
|
||||||
|
// mod context;
|
||||||
mod command;
|
// mod metadata;
|
||||||
pub mod command_helpers;
|
// pub mod rpc_server_helpers;
|
||||||
mod context;
|
pub(crate) mod util;
|
||||||
mod metadata;
|
|
||||||
pub mod rpc_server_helpers;
|
|
||||||
|
|||||||
60
rpc-toolkit/src/util.rs
Normal file
60
rpc-toolkit/src/util.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use imbl_value::Value;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use yajrc::RpcError;
|
||||||
|
|
||||||
|
pub fn extract<T: DeserializeOwned>(value: &Value) -> Result<T, RpcError> {
|
||||||
|
imbl_value::from_value(value.clone()).map_err(|e| RpcError {
|
||||||
|
data: Some(e.to_string().into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn combine(v1: Value, v2: Value) -> Result<Value, RpcError> {
|
||||||
|
let (Value::Object(mut v1), Value::Object(v2)) = (v1, v2) else {
|
||||||
|
return Err(RpcError {
|
||||||
|
data: Some("params must be object".into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (key, value) in v2 {
|
||||||
|
if v1.insert(key.clone(), value).is_some() {
|
||||||
|
return Err(RpcError {
|
||||||
|
data: Some(format!("duplicate key: {key}").into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Object(v1))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_params(e: imbl_value::Error) -> RpcError {
|
||||||
|
RpcError {
|
||||||
|
data: Some(e.to_string().into()),
|
||||||
|
..yajrc::INVALID_PARAMS_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_error(e: imbl_value::Error) -> RpcError {
|
||||||
|
RpcError {
|
||||||
|
data: Some(e.to_string().into()),
|
||||||
|
..yajrc::PARSE_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Flat<A, B>(pub A, pub B);
|
||||||
|
impl<'de, A, B> Deserialize<'de> for Flat<A, B>
|
||||||
|
where
|
||||||
|
A: DeserializeOwned,
|
||||||
|
B: DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let v = Value::deserialize(deserializer)?;
|
||||||
|
let a = imbl_value::from_value(v.clone()).map_err(serde::de::Error::custom)?;
|
||||||
|
let b = imbl_value::from_value(v).map_err(serde::de::Error::custom)?;
|
||||||
|
Ok(Flat(a, b))
|
||||||
|
}
|
||||||
|
}
|
||||||
214
rpc-toolkit/tests/compat.rs
Normal file
214
rpc-toolkit/tests/compat.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use futures::FutureExt;
|
||||||
|
use hyper::Request;
|
||||||
|
use rpc_toolkit::clap::Arg;
|
||||||
|
use rpc_toolkit::hyper::http::Error as HttpError;
|
||||||
|
use rpc_toolkit::hyper::{Body, Response};
|
||||||
|
use rpc_toolkit::rpc_server_helpers::{
|
||||||
|
DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
|
||||||
|
};
|
||||||
|
use rpc_toolkit::serde::{Deserialize, Serialize};
|
||||||
|
use rpc_toolkit::url::Host;
|
||||||
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
|
use rpc_toolkit::{command, rpc_server, run_cli, Context, Metadata};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppState(Arc<ConfigSeed>);
|
||||||
|
impl From<AppState> for () {
|
||||||
|
fn from(_: AppState) -> Self {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConfigSeed {
|
||||||
|
host: Host,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for AppState {
|
||||||
|
fn host(&self) -> Host<&str> {
|
||||||
|
match &self.0.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.0.port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_string() -> String {
|
||||||
|
"test".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 + 'static,
|
||||||
|
E: Display,
|
||||||
|
>(
|
||||||
|
#[context] _ctx: AppState,
|
||||||
|
#[arg(short = 'a')] arg1: Option<String>,
|
||||||
|
#[arg(short = 'b', default = "test_string")] val: String,
|
||||||
|
#[arg(short = 'c', help = "I am the flag `c`!", default)] arg3: bool,
|
||||||
|
#[arg(stdin)] structured: U,
|
||||||
|
) -> Result<(Option<String>, String, bool, U), RpcError> {
|
||||||
|
Ok((arg1, val, arg3, structured))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dothething_impl<U: Serialize>(
|
||||||
|
ctx: AppState,
|
||||||
|
parent_data: (Option<String>, String, bool, U),
|
||||||
|
) -> Result<String, RpcError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{:?}, {:?}, {}, {}, {}",
|
||||||
|
ctx,
|
||||||
|
parent_data.0,
|
||||||
|
parent_data.1,
|
||||||
|
parent_data.2,
|
||||||
|
serde_json::to_string_pretty(&parent_data.3)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(about = "Does the thing")]
|
||||||
|
fn dothething2<U: Serialize + for<'a> Deserialize<'a> + FromStr<Err = E>, E: Display>(
|
||||||
|
#[parent_data] parent_data: (Option<String>, String, bool, U),
|
||||||
|
#[arg(stdin)] structured2: U,
|
||||||
|
) -> Result<String, RpcError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{:?}, {}, {}, {}, {}",
|
||||||
|
parent_data.0,
|
||||||
|
parent_data.1,
|
||||||
|
parent_data.2,
|
||||||
|
serde_json::to_string_pretty(&parent_data.3)?,
|
||||||
|
serde_json::to_string_pretty(&structured2)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cors<M: Metadata + 'static>(
|
||||||
|
req: &mut Request<Body>,
|
||||||
|
_: M,
|
||||||
|
) -> Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError> {
|
||||||
|
if req.method() == hyper::Method::OPTIONS {
|
||||||
|
Ok(Err(Response::builder()
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.body(Body::empty())?))
|
||||||
|
} else {
|
||||||
|
Ok(Ok(Box::new(|_, _| {
|
||||||
|
async move {
|
||||||
|
let res: DynMiddlewareStage3 = Box::new(|_, _| {
|
||||||
|
async move {
|
||||||
|
let res: DynMiddlewareStage4 = Box::new(|res| {
|
||||||
|
async move {
|
||||||
|
res.headers_mut()
|
||||||
|
.insert("Access-Control-Allow-Origin", "*".parse()?);
|
||||||
|
Ok::<_, HttpError>(())
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
});
|
||||||
|
Ok::<_, HttpError>(Ok(res))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
});
|
||||||
|
Ok::<_, HttpError>(Ok(res))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rpc() {
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
let seed = Arc::new(ConfigSeed {
|
||||||
|
host: Host::parse("localhost").unwrap(),
|
||||||
|
port: 8000,
|
||||||
|
});
|
||||||
|
let server = rpc_server!({
|
||||||
|
command: dothething::<String, _>,
|
||||||
|
context: AppState(seed),
|
||||||
|
middleware: [
|
||||||
|
cors,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let handle = tokio::spawn(server);
|
||||||
|
let mut cmd = tokio::process::Command::new("cargo")
|
||||||
|
.arg("test")
|
||||||
|
.arg("--package")
|
||||||
|
.arg("rpc-toolkit")
|
||||||
|
.arg("--test")
|
||||||
|
.arg("test")
|
||||||
|
.arg("--")
|
||||||
|
.arg("cli_test")
|
||||||
|
.arg("--exact")
|
||||||
|
.arg("--nocapture")
|
||||||
|
.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!(dbg!(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, _, _, _>(AppState(seed), (), None, &matches, "".into(), ())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn cli_example() {
|
||||||
|
run_cli! ({
|
||||||
|
command: dothething::<String, _>,
|
||||||
|
app: 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)),
|
||||||
|
context: matches => AppState(Arc::new(ConfigSeed {
|
||||||
|
host: Host::parse(matches.value_of("host").unwrap_or("localhost")).unwrap(),
|
||||||
|
port: matches.value_of("port").unwrap_or("8000").parse().unwrap(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
@@ -1,214 +1 @@
|
|||||||
use std::fmt::Display;
|
pub struct App;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use futures::FutureExt;
|
|
||||||
use hyper::Request;
|
|
||||||
use rpc_toolkit::clap::Arg;
|
|
||||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
|
||||||
use rpc_toolkit::hyper::{Body, Response};
|
|
||||||
use rpc_toolkit::rpc_server_helpers::{
|
|
||||||
DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
|
|
||||||
};
|
|
||||||
use rpc_toolkit::serde::{Deserialize, Serialize};
|
|
||||||
use rpc_toolkit::url::Host;
|
|
||||||
use rpc_toolkit::yajrc::RpcError;
|
|
||||||
use rpc_toolkit::{command, rpc_server, run_cli, Context, Metadata};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AppState(Arc<ConfigSeed>);
|
|
||||||
impl From<AppState> for () {
|
|
||||||
fn from(_: AppState) -> Self {
|
|
||||||
()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ConfigSeed {
|
|
||||||
host: Host,
|
|
||||||
port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context for AppState {
|
|
||||||
fn host(&self) -> Host<&str> {
|
|
||||||
match &self.0.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.0.port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_string() -> String {
|
|
||||||
"test".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 + 'static,
|
|
||||||
E: Display,
|
|
||||||
>(
|
|
||||||
#[context] _ctx: AppState,
|
|
||||||
#[arg(short = 'a')] arg1: Option<String>,
|
|
||||||
#[arg(short = 'b', default = "test_string")] val: String,
|
|
||||||
#[arg(short = 'c', help = "I am the flag `c`!", default)] arg3: bool,
|
|
||||||
#[arg(stdin)] structured: U,
|
|
||||||
) -> Result<(Option<String>, String, bool, U), RpcError> {
|
|
||||||
Ok((arg1, val, arg3, structured))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn dothething_impl<U: Serialize>(
|
|
||||||
ctx: AppState,
|
|
||||||
parent_data: (Option<String>, String, bool, U),
|
|
||||||
) -> Result<String, RpcError> {
|
|
||||||
Ok(format!(
|
|
||||||
"{:?}, {:?}, {}, {}, {}",
|
|
||||||
ctx,
|
|
||||||
parent_data.0,
|
|
||||||
parent_data.1,
|
|
||||||
parent_data.2,
|
|
||||||
serde_json::to_string_pretty(&parent_data.3)?
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command(about = "Does the thing")]
|
|
||||||
fn dothething2<U: Serialize + for<'a> Deserialize<'a> + FromStr<Err = E>, E: Display>(
|
|
||||||
#[parent_data] parent_data: (Option<String>, String, bool, U),
|
|
||||||
#[arg(stdin)] structured2: U,
|
|
||||||
) -> Result<String, RpcError> {
|
|
||||||
Ok(format!(
|
|
||||||
"{:?}, {}, {}, {}, {}",
|
|
||||||
parent_data.0,
|
|
||||||
parent_data.1,
|
|
||||||
parent_data.2,
|
|
||||||
serde_json::to_string_pretty(&parent_data.3)?,
|
|
||||||
serde_json::to_string_pretty(&structured2)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cors<M: Metadata + 'static>(
|
|
||||||
req: &mut Request<Body>,
|
|
||||||
_: M,
|
|
||||||
) -> Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError> {
|
|
||||||
if req.method() == hyper::Method::OPTIONS {
|
|
||||||
Ok(Err(Response::builder()
|
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
|
||||||
.body(Body::empty())?))
|
|
||||||
} else {
|
|
||||||
Ok(Ok(Box::new(|_, _| {
|
|
||||||
async move {
|
|
||||||
let res: DynMiddlewareStage3 = Box::new(|_, _| {
|
|
||||||
async move {
|
|
||||||
let res: DynMiddlewareStage4 = Box::new(|res| {
|
|
||||||
async move {
|
|
||||||
res.headers_mut()
|
|
||||||
.insert("Access-Control-Allow-Origin", "*".parse()?);
|
|
||||||
Ok::<_, HttpError>(())
|
|
||||||
}
|
|
||||||
.boxed()
|
|
||||||
});
|
|
||||||
Ok::<_, HttpError>(Ok(res))
|
|
||||||
}
|
|
||||||
.boxed()
|
|
||||||
});
|
|
||||||
Ok::<_, HttpError>(Ok(res))
|
|
||||||
}
|
|
||||||
.boxed()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_rpc() {
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
|
|
||||||
let seed = Arc::new(ConfigSeed {
|
|
||||||
host: Host::parse("localhost").unwrap(),
|
|
||||||
port: 8000,
|
|
||||||
});
|
|
||||||
let server = rpc_server!({
|
|
||||||
command: dothething::<String, _>,
|
|
||||||
context: AppState(seed),
|
|
||||||
middleware: [
|
|
||||||
cors,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
let handle = tokio::spawn(server);
|
|
||||||
let mut cmd = tokio::process::Command::new("cargo")
|
|
||||||
.arg("test")
|
|
||||||
.arg("--package")
|
|
||||||
.arg("rpc-toolkit")
|
|
||||||
.arg("--test")
|
|
||||||
.arg("test")
|
|
||||||
.arg("--")
|
|
||||||
.arg("cli_test")
|
|
||||||
.arg("--exact")
|
|
||||||
.arg("--nocapture")
|
|
||||||
.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!(dbg!(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, _, _, _>(AppState(seed), (), None, &matches, "".into(), ())
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn cli_example() {
|
|
||||||
run_cli! ({
|
|
||||||
command: dothething::<String, _>,
|
|
||||||
app: 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)),
|
|
||||||
context: matches => AppState(Arc::new(ConfigSeed {
|
|
||||||
host: Host::parse(matches.value_of("host").unwrap_or("localhost")).unwrap(),
|
|
||||||
port: matches.value_of("port").unwrap_or("8000").parse().unwrap(),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|||||||
Reference in New Issue
Block a user