mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-14 09:27:04 +00:00
api: add generic Body parameter
Since we already know we'll want to be using hyper::Body and bytes::Bytes as API output, we need to allow making routers for each kind. Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
b82b14d947
commit
7d2c13da95
@ -5,7 +5,6 @@ version = "0.1.0"
|
|||||||
authors = [ "Wolfgang Bumiller <w.bumiller@proxmox.com>" ]
|
authors = [ "Wolfgang Bumiller <w.bumiller@proxmox.com>" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "0.4"
|
|
||||||
failure = "0.1"
|
failure = "0.1"
|
||||||
futures-preview = "0.3.0-alpha"
|
futures-preview = "0.3.0-alpha"
|
||||||
http = "0.1"
|
http = "0.1"
|
||||||
@ -14,4 +13,5 @@ serde_derive = "1.0"
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
bytes = "0.4"
|
||||||
lazy_static = "1.3"
|
lazy_static = "1.3"
|
||||||
|
@ -10,36 +10,41 @@ use super::{ApiOutput, ApiType};
|
|||||||
/// wrapped in a `http::Response` with a status code of `200`, but if an API method returns a
|
/// wrapped in a `http::Response` with a status code of `200`, but if an API method returns a
|
||||||
/// `http::Response`, we don't want that, our wrappers produced by the `#[api]` macro simply call
|
/// `http::Response`, we don't want that, our wrappers produced by the `#[api]` macro simply call
|
||||||
/// `output.into_api_output()`, and the trait implementation decides how to proceed.
|
/// `output.into_api_output()`, and the trait implementation decides how to proceed.
|
||||||
pub trait IntoApiOutput<T> {
|
pub trait IntoApiOutput<Body, T> {
|
||||||
fn into_api_output(self) -> ApiOutput;
|
fn into_api_output(self) -> ApiOutput<Body>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ApiType + serde::Serialize> IntoApiOutput<()> for T {
|
impl<Body, T> IntoApiOutput<Body, ()> for T
|
||||||
|
where
|
||||||
|
Body: 'static,
|
||||||
|
T: ApiType + serde::Serialize,
|
||||||
|
Body: From<String>,
|
||||||
|
{
|
||||||
/// By default, any serializable type is serialized into a `{"data": output}` json structure,
|
/// By default, any serializable type is serialized into a `{"data": output}` json structure,
|
||||||
/// and returned as http status 200.
|
/// and returned as http status 200.
|
||||||
fn into_api_output(self) -> ApiOutput {
|
fn into_api_output(self) -> ApiOutput<Body> {
|
||||||
let output = serde_json::to_value(self)?;
|
let output = serde_json::to_value(self)?;
|
||||||
let res = json!({ "data": output });
|
let res = json!({ "data": output });
|
||||||
let output = serde_json::to_string(&res)?;
|
let output = serde_json::to_string(&res)?;
|
||||||
Ok(http::Response::builder()
|
Ok(http::Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.body(bytes::Bytes::from(output))?)
|
.body(Body::from(output))?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Methods returning `ApiOutput` (which is a `Result<http::Result<Bytes>, Error>`) don't need
|
/// Methods returning `ApiOutput` (which is a `Result<http::Result<Bytes>, Error>`) don't need
|
||||||
/// anything to happen to the value anymore, return the result as is:
|
/// anything to happen to the value anymore, return the result as is:
|
||||||
impl IntoApiOutput<ApiOutput> for ApiOutput {
|
impl<Body> IntoApiOutput<Body, ApiOutput<Body>> for ApiOutput<Body> {
|
||||||
fn into_api_output(self) -> ApiOutput {
|
fn into_api_output(self) -> ApiOutput<Body> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Methods returning a `http::Response` (without the `Result<_, Error>` around it) need to be
|
/// Methods returning a `http::Response` (without the `Result<_, Error>` around it) need to be
|
||||||
/// wrapped in a `Result`, as we do apply a `?` operator on our methods.
|
/// wrapped in a `Result`, as we do apply a `?` operator on our methods.
|
||||||
impl IntoApiOutput<ApiOutput> for http::Response<bytes::Bytes> {
|
impl<Body> IntoApiOutput<Body, ApiOutput<Body>> for http::Response<Body> {
|
||||||
fn into_api_output(self) -> ApiOutput {
|
fn into_api_output(self) -> ApiOutput<Body> {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use http::Response;
|
use http::Response;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -11,13 +10,13 @@ use serde_json::Value;
|
|||||||
/// Method entries in a `Router` are actually just `&dyn ApiMethodInfo` trait objects.
|
/// Method entries in a `Router` are actually just `&dyn ApiMethodInfo` trait objects.
|
||||||
/// This contains all the info required to call, document, or command-line-complete parameters for
|
/// This contains all the info required to call, document, or command-line-complete parameters for
|
||||||
/// a method.
|
/// a method.
|
||||||
pub trait ApiMethodInfo {
|
pub trait ApiMethodInfo<Body> {
|
||||||
fn description(&self) -> &'static str;
|
fn description(&self) -> &'static str;
|
||||||
fn parameters(&self) -> &'static [Parameter];
|
fn parameters(&self) -> &'static [Parameter];
|
||||||
fn return_type(&self) -> &'static TypeInfo;
|
fn return_type(&self) -> &'static TypeInfo;
|
||||||
fn protected(&self) -> bool;
|
fn protected(&self) -> bool;
|
||||||
fn reload_timezone(&self) -> bool;
|
fn reload_timezone(&self) -> bool;
|
||||||
fn handler(&self) -> fn(Value) -> super::ApiFuture;
|
fn handler(&self) -> fn(Value) -> super::ApiFuture<Body>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shortcut to not having to type it out. This function signature is just a dummy and not yet
|
/// Shortcut to not having to type it out. This function signature is just a dummy and not yet
|
||||||
@ -46,16 +45,16 @@ pub struct TypeInfo {
|
|||||||
/// Otherwise this is mostly there so we can run the tests in the tests subdirectory without
|
/// Otherwise this is mostly there so we can run the tests in the tests subdirectory without
|
||||||
/// depending on the api-macro crate. Tests using the macros belong into the api-macro crate itself
|
/// depending on the api-macro crate. Tests using the macros belong into the api-macro crate itself
|
||||||
/// after all!
|
/// after all!
|
||||||
pub struct ApiMethod {
|
pub struct ApiMethod<Body> {
|
||||||
pub description: &'static str,
|
pub description: &'static str,
|
||||||
pub parameters: &'static [Parameter],
|
pub parameters: &'static [Parameter],
|
||||||
pub return_type: &'static TypeInfo,
|
pub return_type: &'static TypeInfo,
|
||||||
pub protected: bool,
|
pub protected: bool,
|
||||||
pub reload_timezone: bool,
|
pub reload_timezone: bool,
|
||||||
pub handler: fn(Value) -> super::ApiFuture,
|
pub handler: fn(Value) -> super::ApiFuture<Body>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiMethodInfo for ApiMethod {
|
impl<Body> ApiMethodInfo<Body> for ApiMethod<Body> {
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
self.description
|
self.description
|
||||||
}
|
}
|
||||||
@ -76,7 +75,7 @@ impl ApiMethodInfo for ApiMethod {
|
|||||||
self.reload_timezone
|
self.reload_timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handler(&self) -> fn(Value) -> super::ApiFuture {
|
fn handler(&self) -> fn(Value) -> super::ApiFuture<Body> {
|
||||||
self.handler
|
self.handler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,7 +221,20 @@ unconstrained_api_type! {String, isize, usize, i64, u64, i32, u32, i16, u16, i8,
|
|||||||
unconstrained_api_type! {Vec<String>}
|
unconstrained_api_type! {Vec<String>}
|
||||||
|
|
||||||
// Raw return types are also okay:
|
// Raw return types are also okay:
|
||||||
unconstrained_api_type! {Response<Bytes>}
|
impl<Body> ApiType for Response<Body> {
|
||||||
|
fn verify(&self) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_info() -> &'static TypeInfo {
|
||||||
|
const INFO: TypeInfo = TypeInfo {
|
||||||
|
name: "http::Response<>",
|
||||||
|
description: "A raw http response",
|
||||||
|
complete_fn: None,
|
||||||
|
};
|
||||||
|
&INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: make const once feature(const_fn) is stable!
|
// FIXME: make const once feature(const_fn) is stable!
|
||||||
pub fn get_type_info<T: ApiType>() -> &'static TypeInfo {
|
pub fn get_type_info<T: ApiType>() -> &'static TypeInfo {
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use http::Response;
|
use http::Response;
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ mod router;
|
|||||||
pub use router::*;
|
pub use router::*;
|
||||||
|
|
||||||
/// Return type of an API method.
|
/// Return type of an API method.
|
||||||
pub type ApiOutput = Result<Response<Bytes>, Error>;
|
pub type ApiOutput<Body> = Result<Response<Body>, Error>;
|
||||||
|
|
||||||
/// Future type of an API method. In order to support `async fn` this is a pinned box.
|
/// Future type of an API method. In order to support `async fn` this is a pinned box.
|
||||||
pub type ApiFuture = Pin<Box<dyn Future<Output = ApiOutput>>>;
|
pub type ApiFuture<Body> = Pin<Box<dyn Future<Output = ApiOutput<Body>>>>;
|
||||||
|
@ -13,16 +13,16 @@ use super::ApiMethodInfo;
|
|||||||
/// When subdirectories are supposed to be passed as a `String` parameter to methods beneath the
|
/// When subdirectories are supposed to be passed as a `String` parameter to methods beneath the
|
||||||
/// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this
|
/// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this
|
||||||
/// point, so all method calls beneath will receive a parameter ot that particular name.
|
/// point, so all method calls beneath will receive a parameter ot that particular name.
|
||||||
pub enum SubRoute {
|
pub enum SubRoute<Body: 'static> {
|
||||||
/// Call this router for any further subdirectory paths, and provide the relative path via the
|
/// Call this router for any further subdirectory paths, and provide the relative path via the
|
||||||
/// given parameter.
|
/// given parameter.
|
||||||
Wildcard(&'static str),
|
Wildcard(&'static str),
|
||||||
|
|
||||||
/// This is used for plain subdirectories.
|
/// This is used for plain subdirectories.
|
||||||
Directories(HashMap<&'static str, Router>),
|
Directories(HashMap<&'static str, Router<Body>>),
|
||||||
|
|
||||||
/// Match subdirectories as the given parameter name to the underlying router.
|
/// Match subdirectories as the given parameter name to the underlying router.
|
||||||
Parameter(&'static str, Box<Router>),
|
Parameter(&'static str, Box<Router<Body>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`,
|
/// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`,
|
||||||
@ -30,24 +30,27 @@ pub enum SubRoute {
|
|||||||
/// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of
|
/// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of
|
||||||
/// the two is the case.
|
/// the two is the case.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Router {
|
pub struct Router<Body: 'static> {
|
||||||
/// The `GET` http method.
|
/// The `GET` http method.
|
||||||
pub get: Option<&'static dyn ApiMethodInfo>,
|
pub get: Option<&'static dyn ApiMethodInfo<Body>>,
|
||||||
|
|
||||||
/// The `PUT` http method.
|
/// The `PUT` http method.
|
||||||
pub put: Option<&'static dyn ApiMethodInfo>,
|
pub put: Option<&'static dyn ApiMethodInfo<Body>>,
|
||||||
|
|
||||||
/// The `POST` http method.
|
/// The `POST` http method.
|
||||||
pub post: Option<&'static dyn ApiMethodInfo>,
|
pub post: Option<&'static dyn ApiMethodInfo<Body>>,
|
||||||
|
|
||||||
/// The `DELETE` http method.
|
/// The `DELETE` http method.
|
||||||
pub delete: Option<&'static dyn ApiMethodInfo>,
|
pub delete: Option<&'static dyn ApiMethodInfo<Body>>,
|
||||||
|
|
||||||
/// Specifies the behavior of sub directories. See [`SubRoute`].
|
/// Specifies the behavior of sub directories. See [`SubRoute`].
|
||||||
pub subroute: Option<SubRoute>,
|
pub subroute: Option<SubRoute<Body>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
impl<Body> Router<Body>
|
||||||
|
where
|
||||||
|
Self: Default,
|
||||||
|
{
|
||||||
/// Create a new empty router.
|
/// Create a new empty router.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
@ -110,7 +113,7 @@ impl Router {
|
|||||||
/// Builder method to provide a `GET` method info.
|
/// Builder method to provide a `GET` method info.
|
||||||
pub fn get<I>(mut self, method: &'static I) -> Self
|
pub fn get<I>(mut self, method: &'static I) -> Self
|
||||||
where
|
where
|
||||||
I: ApiMethodInfo,
|
I: ApiMethodInfo<Body>,
|
||||||
{
|
{
|
||||||
self.get = Some(method);
|
self.get = Some(method);
|
||||||
self
|
self
|
||||||
@ -119,7 +122,7 @@ impl Router {
|
|||||||
/// Builder method to provide a `PUT` method info.
|
/// Builder method to provide a `PUT` method info.
|
||||||
pub fn put<I>(mut self, method: &'static I) -> Self
|
pub fn put<I>(mut self, method: &'static I) -> Self
|
||||||
where
|
where
|
||||||
I: ApiMethodInfo,
|
I: ApiMethodInfo<Body>,
|
||||||
{
|
{
|
||||||
self.put = Some(method);
|
self.put = Some(method);
|
||||||
self
|
self
|
||||||
@ -128,7 +131,7 @@ impl Router {
|
|||||||
/// Builder method to provide a `POST` method info.
|
/// Builder method to provide a `POST` method info.
|
||||||
pub fn post<I>(mut self, method: &'static I) -> Self
|
pub fn post<I>(mut self, method: &'static I) -> Self
|
||||||
where
|
where
|
||||||
I: ApiMethodInfo,
|
I: ApiMethodInfo<Body>,
|
||||||
{
|
{
|
||||||
self.post = Some(method);
|
self.post = Some(method);
|
||||||
self
|
self
|
||||||
@ -137,7 +140,7 @@ impl Router {
|
|||||||
/// Builder method to provide a `DELETE` method info.
|
/// Builder method to provide a `DELETE` method info.
|
||||||
pub fn delete<I>(mut self, method: &'static I) -> Self
|
pub fn delete<I>(mut self, method: &'static I) -> Self
|
||||||
where
|
where
|
||||||
I: ApiMethodInfo,
|
I: ApiMethodInfo<Body>,
|
||||||
{
|
{
|
||||||
self.delete = Some(method);
|
self.delete = Some(method);
|
||||||
self
|
self
|
||||||
@ -147,7 +150,7 @@ impl Router {
|
|||||||
///
|
///
|
||||||
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
||||||
/// already have a subdir entry!
|
/// already have a subdir entry!
|
||||||
pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router) -> Self {
|
pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router<Body>) -> Self {
|
||||||
if self.subroute.is_some() {
|
if self.subroute.is_some() {
|
||||||
panic!("match_parameter can only be used once and without sub directories");
|
panic!("match_parameter can only be used once and without sub directories");
|
||||||
}
|
}
|
||||||
@ -159,7 +162,7 @@ impl Router {
|
|||||||
///
|
///
|
||||||
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
||||||
/// already have a subdir entry!
|
/// already have a subdir entry!
|
||||||
pub fn subdir(mut self, dir_name: &'static str, router: Router) -> Self {
|
pub fn subdir(mut self, dir_name: &'static str, router: Router<Body>) -> Self {
|
||||||
let previous = match self.subroute {
|
let previous = match self.subroute {
|
||||||
Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router),
|
Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router),
|
||||||
None => {
|
None => {
|
||||||
|
@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
use proxmox_api::Router;
|
use proxmox_api::Router;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic() {
|
fn basic() {
|
||||||
let info: &proxmox_api::ApiMethod = &methods::GET_PEOPLE;
|
let info: &proxmox_api::ApiMethod<Bytes> = &methods::GET_PEOPLE;
|
||||||
let get_subpath: &proxmox_api::ApiMethod = &methods::GET_SUBPATH;
|
let get_subpath: &proxmox_api::ApiMethod<Bytes> = &methods::GET_SUBPATH;
|
||||||
let router = Router::new()
|
let router: Router<Bytes> = Router::new()
|
||||||
.subdir(
|
.subdir(
|
||||||
"people",
|
"people",
|
||||||
Router::new().parameter_subdir("person", Router::new().get(info)),
|
Router::new().parameter_subdir("person", Router::new().get(info)),
|
||||||
@ -35,7 +37,7 @@ fn basic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_with_matched_params(
|
fn check_with_matched_params(
|
||||||
router: &Router,
|
router: &Router<Bytes>,
|
||||||
path: &str,
|
path: &str,
|
||||||
param_name: &str,
|
param_name: &str,
|
||||||
param_value: &str,
|
param_value: &str,
|
||||||
@ -84,6 +86,7 @@ fn check_with_matched_params(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod methods {
|
mod methods {
|
||||||
|
use bytes::Bytes;
|
||||||
use failure::{bail, Error};
|
use failure::{bail, Error};
|
||||||
use http::Response;
|
use http::Response;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -94,14 +97,14 @@ mod methods {
|
|||||||
get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter, TypeInfo,
|
get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter, TypeInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_people(value: Value) -> ApiOutput {
|
pub async fn get_people(value: Value) -> ApiOutput<Bytes> {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.body(value["person"].as_str().unwrap().into())?)
|
.body(value["person"].as_str().unwrap().into())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_subpath(value: Value) -> ApiOutput {
|
pub async fn get_subpath(value: Value) -> ApiOutput<Bytes> {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
@ -116,14 +119,14 @@ mod methods {
|
|||||||
type_info: get_type_info::<String>(),
|
type_info: get_type_info::<String>(),
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
pub static ref GET_PEOPLE: ApiMethod = {
|
pub static ref GET_PEOPLE: ApiMethod<Bytes> = {
|
||||||
ApiMethod {
|
ApiMethod {
|
||||||
description: "get some people",
|
description: "get some people",
|
||||||
parameters: &GET_PEOPLE_PARAMS,
|
parameters: &GET_PEOPLE_PARAMS,
|
||||||
return_type: get_type_info::<String>(),
|
return_type: get_type_info::<String>(),
|
||||||
protected: false,
|
protected: false,
|
||||||
reload_timezone: false,
|
reload_timezone: false,
|
||||||
handler: |value: Value| -> ApiFuture { Box::pin(get_people(value)) },
|
handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(get_people(value)) },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
static ref GET_SUBPATH_PARAMS: Vec<Parameter> = {
|
static ref GET_SUBPATH_PARAMS: Vec<Parameter> = {
|
||||||
@ -133,14 +136,14 @@ mod methods {
|
|||||||
type_info: get_type_info::<String>(),
|
type_info: get_type_info::<String>(),
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
pub static ref GET_SUBPATH: ApiMethod = {
|
pub static ref GET_SUBPATH: ApiMethod<Bytes> = {
|
||||||
ApiMethod {
|
ApiMethod {
|
||||||
description: "get the 'subpath' parameter returned back",
|
description: "get the 'subpath' parameter returned back",
|
||||||
parameters: &GET_SUBPATH_PARAMS,
|
parameters: &GET_SUBPATH_PARAMS,
|
||||||
return_type: get_type_info::<String>(),
|
return_type: get_type_info::<String>(),
|
||||||
protected: false,
|
protected: false,
|
||||||
reload_timezone: false,
|
reload_timezone: false,
|
||||||
handler: |value: Value| -> ApiFuture { Box::pin(get_subpath(value)) },
|
handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(get_subpath(value)) },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user