clean up, add docs

This commit is contained in:
Aiden McClelland
2023-12-12 13:52:31 -07:00
parent 86465c1319
commit 6696da4590
6 changed files with 512 additions and 308 deletions

108
rpc-toolkit/src/cli.rs Normal file
View 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)?)
}
}

View File

@@ -10,17 +10,22 @@ use serde::ser::Serialize;
use tokio::runtime::Runtime;
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> {
name: &'static str,
implementation: Option<Implementation<Context>>,
cli: Option<CliBindings>,
subcommands: Vec<Self>,
pub(crate) name: &'static str,
pub(crate) implementation: Option<Implementation<Context>>,
pub(crate) cli: Option<CliBindings>,
pub(crate) subcommands: Vec<Self>,
}
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 {
Some(
cli.cmd
.clone()
.name(self.name)
.subcommands(self.subcommands.iter().filter_map(|c| c.cli_app())),
)
@@ -28,7 +33,7 @@ impl<Context> DynCommand<Context> {
None
}
}
fn impl_from_cli_matches(
pub(crate) fn impl_from_cli_matches(
&self,
matches: &ArgMatches,
parent: Value,
@@ -53,12 +58,13 @@ impl<Context> DynCommand<Context> {
Err(yajrc::METHOD_NOT_FOUND_ERROR)
}
}
pub fn run_cli(ctx: Context) {}
}
struct Implementation<Context> {
async_impl: Arc<dyn Fn(Context, Value) -> BoxFuture<'static, Result<Value, RpcError>>>,
sync_impl: Arc<dyn Fn(Context, Value) -> Result<Value, RpcError>>,
pub(crate) async_impl: Arc<
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 {
@@ -72,7 +78,7 @@ impl<Context> Clone for Implementation<Context> {
struct CliBindings {
cmd: clap::Command,
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 {
fn from_parent<Cmd: FromArgMatches + CommandFactory + Serialize>() -> Self {
@@ -93,36 +99,50 @@ impl CliBindings {
}
fn from_leaf<Cmd: FromArgMatches + CommandFactory + Serialize + LeafCommand>() -> 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>()
}
}
}
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;
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 Contains<T>(PhantomData<T>);
impl<T, U> From<(Contains<T>, Contains<U>)> for Contains<(T, U)> {
fn from(value: (Contains<T>, Contains<U>)) -> Self {
impl<T, U> From<(Contains<T>, Contains<U>)> for Contains<Flat<T, U>> {
fn from(_: (Contains<T>, Contains<U>)) -> Self {
Self(PhantomData)
}
}
/// Use this as a Parent if your command does not require any arguments from its parents
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Root {}
impl Command for Root {
pub struct NoParent {}
impl Command for NoParent {
const NAME: &'static str = "";
type Parent = Root;
type Parent = NoParent;
}
impl<Cmd> ParentChain<Cmd>
where
Cmd: Command,
{
pub fn unit(&self) -> Contains<()> {
pub fn none(&self) -> Contains<NoParent> {
Contains(PhantomData)
}
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 {
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>>;
}
@@ -147,34 +168,52 @@ impl<Context> DynCommand<Context> {
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 {
type Ok: Serialize;
type Err: Into<RpcError>;
type Ok: DeserializeOwned + Serialize + Send;
type Err: From<RpcError> + Into<RpcError> + Send;
fn display(res: Self::Ok);
}
/// Implement this if your Command's implementation is async
#[async_trait::async_trait]
pub trait AsyncCommand<Context>: LeafCommand {
async fn implementation(
self,
ctx: Context,
parent: Self::Parent,
parent: ParentInfo<Self::Parent>,
) -> Result<Self::Ok, Self::Err>;
fn subcommands(chain: ParentChain<Self>) -> Vec<DynCommand<Context>> {
drop(chain);
Vec::new()
}
}
impl<Context: Send> Implementation<Context> {
impl<Context: Send + 'static> Implementation<Context> {
fn for_async<Cmd: AsyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
drop(contains);
Self {
async_impl: Arc::new(|ctx, params| {
async_impl: Arc::new(|ctx, method, params| {
async move {
let parent = extract::<Cmd::Parent>(&params)?;
imbl_value::to_value(
&extract::<Cmd>(&params)?
.implementation(ctx, parent)
.implementation(
ctx,
ParentInfo {
method,
args: parent,
},
)
.await
.map_err(|e| e.into())?,
)
@@ -185,7 +224,7 @@ impl<Context: Send> Implementation<Context> {
}
.boxed()
}),
sync_impl: Arc::new(|ctx, params| {
sync_impl: Arc::new(|ctx, method, params| {
let parent = extract::<Cmd::Parent>(&params)?;
imbl_value::to_value(
&Runtime::new()
@@ -196,7 +235,13 @@ impl<Context: Send> Implementation<Context> {
data: Some(e.to_string().into()),
..yajrc::INVALID_PARAMS_ERROR
})?
.implementation(ctx, parent),
.implementation(
ctx,
ParentInfo {
method,
args: parent,
},
),
)
.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>(
contains: Contains<Cmd::Parent>,
) -> Self {
@@ -219,20 +264,35 @@ impl<Context: Send> DynCommand<Context> {
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 {
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>> {
drop(chain);
Vec::new()
}
}
impl<Context: Send> Implementation<Context> {
impl<Context: Send + 'static> Implementation<Context> {
fn for_sync<Cmd: SyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
drop(contains);
Self {
async_impl: if Cmd::BLOCKING {
Arc::new(|ctx, params| {
Arc::new(|ctx, method, params| {
tokio::task::spawn_blocking(move || {
let parent = extract::<Cmd::Parent>(&params)?;
imbl_value::to_value(
@@ -241,7 +301,13 @@ impl<Context: Send> Implementation<Context> {
data: Some(e.to_string().into()),
..yajrc::INVALID_PARAMS_ERROR
})?
.implementation(ctx, parent)
.implementation(
ctx,
ParentInfo {
method,
args: parent,
},
)
.map_err(|e| e.into())?,
)
.map_err(|e| RpcError {
@@ -258,7 +324,7 @@ impl<Context: Send> Implementation<Context> {
.boxed()
})
} else {
Arc::new(|ctx, params| {
Arc::new(|ctx, method, params| {
async move {
let parent = extract::<Cmd::Parent>(&params)?;
imbl_value::to_value(
@@ -267,7 +333,13 @@ impl<Context: Send> Implementation<Context> {
data: Some(e.to_string().into()),
..yajrc::INVALID_PARAMS_ERROR
})?
.implementation(ctx, parent)
.implementation(
ctx,
ParentInfo {
method,
args: parent,
},
)
.map_err(|e| e.into())?,
)
.map_err(|e| RpcError {
@@ -278,7 +350,7 @@ impl<Context: Send> Implementation<Context> {
.boxed()
})
},
sync_impl: Arc::new(|ctx, params| {
sync_impl: Arc::new(|ctx, method, params| {
let parent = extract::<Cmd::Parent>(&params)?;
imbl_value::to_value(
&extract::<Cmd>(&params)
@@ -286,7 +358,13 @@ impl<Context: Send> Implementation<Context> {
data: Some(e.to_string().into()),
..yajrc::INVALID_PARAMS_ERROR
})?
.implementation(ctx, parent)
.implementation(
ctx,
ParentInfo {
method,
args: parent,
},
)
.map_err(|e| e.into())?,
)
.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>(
contains: Contains<Cmd::Parent>,
) -> Self {
@@ -308,29 +386,12 @@ impl<Context: Send> DynCommand<Context> {
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
}
}
}
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
})
}
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
});
pub fn from_sync_no_cli<Cmd: SyncCommand<Context>>(contains: Contains<Cmd::Parent>) -> Self {
Self {
name: Cmd::NAME,
implementation: Some(Implementation::for_sync::<Cmd>(contains)),
cli: None,
subcommands: Cmd::subcommands(ParentChain::<Cmd>(PhantomData)),
}
}
Ok(Value::Object(v1))
}

View File

@@ -1,3 +1,5 @@
pub use cli::*;
pub use command::*;
/// `#[command(...)]`
/// - `#[command(cli_only)]` -> executed by CLI instead of RPC server (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)
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 crate::context::Context;
pub use crate::metadata::Metadata;
pub use crate::rpc_server_helpers::RpcHandler;
mod command;
pub mod command_helpers;
mod context;
mod metadata;
pub mod rpc_server_helpers;
pub(crate) mod cli;
pub(crate) mod command;
// pub mod command_helpers;
// mod context;
// mod metadata;
// pub mod rpc_server_helpers;
pub(crate) mod util;

60
rpc-toolkit/src/util.rs Normal file
View 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
View 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(),
}))
})
}
////////////////////////////////////////////////

View File

@@ -1,214 +1 @@
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(),
}))
})
}
////////////////////////////////////////////////
pub struct App;