mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 12:42:28 +00:00
Custom Endpoints
This commit is contained in:
parent
002cfb1b4d
commit
5261aa9931
28 changed files with 524 additions and 46 deletions
|
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Added
|
||||
- Support custom HTTP response headers
|
||||
- New `endpoint` router extension with associated `Endpoint` trait ([!18])
|
||||
- Support for custom endpoints using the `#[endpoint]` macro ([!19])
|
||||
|
||||
### Changed
|
||||
- The cors handler can now copy headers from the request if desired
|
||||
|
@ -34,3 +35,4 @@ Previous changes are not tracked by this changelog file. Refer to the [releases]
|
|||
|
||||
|
||||
[!18]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/18
|
||||
[!19]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/19
|
||||
|
|
29
README.md
29
README.md
|
@ -47,6 +47,11 @@ This crate is just as safe as you'd expect from anything written in safe Rust -
|
|||
|
||||
## Endpoints
|
||||
|
||||
There are a set of pre-defined endpoints that should cover the majority of REST APIs. However,
|
||||
it is also possible to define your own endpoints.
|
||||
|
||||
### Pre-defined Endpoints
|
||||
|
||||
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
|
||||
|
||||
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
|
@ -82,6 +87,30 @@ fn read(id: u64) -> Success<Foo> {
|
|||
}
|
||||
```
|
||||
|
||||
### Custom Endpoints
|
||||
|
||||
Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that
|
||||
of the pre-defined endpoints, but you need to give it more context:
|
||||
|
||||
```rust
|
||||
use gotham_restful::gotham::hyper::Method;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(custom_endpoint)]
|
||||
struct CustomResource;
|
||||
|
||||
/// This type is used to parse path parameters.
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
struct CustomPath {
|
||||
name: String
|
||||
}
|
||||
|
||||
#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)]
|
||||
fn custom_endpoint(path: CustomPath) -> Success<String> {
|
||||
path.name.into()
|
||||
}
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
Some endpoints require arguments. Those should be
|
||||
|
|
|
@ -19,8 +19,11 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
|||
|
||||
[dependencies]
|
||||
heck = "0.3.1"
|
||||
once_cell = "1.5"
|
||||
paste = "1.0"
|
||||
proc-macro2 = "1.0.13"
|
||||
quote = "1.0.6"
|
||||
regex = "1.4"
|
||||
syn = { version = "1.0.22", features = ["full"] }
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
use crate::util::{CollectToResult, PathEndsWith};
|
||||
use crate::util::{CollectToResult, ExpectLit, PathEndsWith};
|
||||
use once_cell::sync::Lazy;
|
||||
use paste::paste;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use regex::Regex;
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result,
|
||||
ReturnType, Type
|
||||
parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, Expr, FnArg, ItemFn, LitBool, LitStr, Meta, NestedMeta,
|
||||
PatType, Result, ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum EndpointType {
|
||||
|
@ -15,9 +18,52 @@ pub enum EndpointType {
|
|||
UpdateAll,
|
||||
Update,
|
||||
DeleteAll,
|
||||
Delete
|
||||
Delete,
|
||||
Custom {
|
||||
method: Option<Expr>,
|
||||
uri: Option<LitStr>,
|
||||
params: Option<LitBool>,
|
||||
body: Option<LitBool>
|
||||
}
|
||||
}
|
||||
|
||||
impl EndpointType {
|
||||
pub fn custom() -> Self {
|
||||
Self::Custom {
|
||||
method: None,
|
||||
uri: None,
|
||||
params: None,
|
||||
body: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! endpoint_type_setter {
|
||||
($name:ident : $ty:ty) => {
|
||||
impl EndpointType {
|
||||
paste! {
|
||||
fn [<set_ $name>](&mut self, span: Span, [<new_ $name>]: $ty) -> Result<()> {
|
||||
match self {
|
||||
Self::Custom { $name, .. } if $name.is_some() => {
|
||||
Err(Error::new(span, concat!("`", concat!(stringify!($name), "` must not appear more than once"))))
|
||||
},
|
||||
Self::Custom { $name, .. } => {
|
||||
*$name = Some([<new_ $name>]);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(Error::new(span, concat!("`", concat!(stringify!($name), "` can only be used on custom endpoints"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
endpoint_type_setter!(method: Expr);
|
||||
endpoint_type_setter!(uri: LitStr);
|
||||
endpoint_type_setter!(params: LitBool);
|
||||
endpoint_type_setter!(body: LitBool);
|
||||
|
||||
impl FromStr for EndpointType {
|
||||
type Err = Error;
|
||||
|
||||
|
@ -36,21 +82,26 @@ impl FromStr for EndpointType {
|
|||
}
|
||||
}
|
||||
|
||||
static URI_PLACEHOLDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"(^|/):(?P<name>[^/]+)(/|$)"#).unwrap());
|
||||
|
||||
impl EndpointType {
|
||||
fn http_method(&self) -> TokenStream {
|
||||
fn http_method(&self) -> Option<TokenStream> {
|
||||
let hyper_method = quote!(::gotham_restful::gotham::hyper::Method);
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search => quote!(::gotham_restful::gotham::hyper::Method::GET),
|
||||
Self::Create => quote!(::gotham_restful::gotham::hyper::Method::POST),
|
||||
Self::UpdateAll | Self::Update => quote!(::gotham_restful::gotham::hyper::Method::PUT),
|
||||
Self::DeleteAll | Self::Delete => quote!(::gotham_restful::gotham::hyper::Method::DELETE)
|
||||
Self::ReadAll | Self::Read | Self::Search => Some(quote!(#hyper_method::GET)),
|
||||
Self::Create => Some(quote!(#hyper_method::POST)),
|
||||
Self::UpdateAll | Self::Update => Some(quote!(#hyper_method::PUT)),
|
||||
Self::DeleteAll | Self::Delete => Some(quote!(#hyper_method::DELETE)),
|
||||
Self::Custom { method, .. } => method.as_ref().map(ToTokens::to_token_stream)
|
||||
}
|
||||
}
|
||||
|
||||
fn uri(&self) -> TokenStream {
|
||||
fn uri(&self) -> Option<TokenStream> {
|
||||
match self {
|
||||
Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => quote!(""),
|
||||
Self::Read | Self::Update | Self::Delete => quote!(":id"),
|
||||
Self::Search => quote!("search")
|
||||
Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => Some(quote!("")),
|
||||
Self::Read | Self::Update | Self::Delete => Some(quote!(":id")),
|
||||
Self::Search => Some(quote!("search")),
|
||||
Self::Custom { uri, .. } => uri.as_ref().map(ToTokens::to_token_stream)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +114,13 @@ impl EndpointType {
|
|||
Self::Read | Self::Update | Self::Delete => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
},
|
||||
Self::Custom { uri, .. } => LitBool {
|
||||
value: uri
|
||||
.as_ref()
|
||||
.map(|uri| URI_PLACEHOLDER_REGEX.is_match(&uri.value()))
|
||||
.unwrap_or(false),
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +130,14 @@ impl EndpointType {
|
|||
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||
},
|
||||
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>)
|
||||
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>),
|
||||
Self::Custom { .. } => {
|
||||
if self.has_placeholders().value {
|
||||
arg_ty.to_token_stream()
|
||||
} else {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +152,11 @@ impl EndpointType {
|
|||
Self::Search => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
},
|
||||
Self::Custom { params, .. } => params.clone().unwrap_or_else(|| LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +165,14 @@ impl EndpointType {
|
|||
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor)
|
||||
},
|
||||
Self::Search => quote!(#arg_ty)
|
||||
Self::Search => quote!(#arg_ty),
|
||||
Self::Custom { .. } => {
|
||||
if self.needs_params().value {
|
||||
arg_ty.to_token_stream()
|
||||
} else {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,14 +185,25 @@ impl EndpointType {
|
|||
Self::Create | Self::UpdateAll | Self::Update => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
},
|
||||
Self::Custom { body, .. } => body.clone().unwrap_or_else(|| LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn body_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => quote!(()),
|
||||
Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty)
|
||||
Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty),
|
||||
Self::Custom { .. } => {
|
||||
if self.needs_body().value {
|
||||
arg_ty.to_token_stream()
|
||||
} else {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -219,7 +306,7 @@ fn interpret_arg(_index: usize, arg: &PatType) -> Result<HandlerArg> {
|
|||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn expand_operation_id(operation_id: Option<Lit>) -> Option<TokenStream> {
|
||||
fn expand_operation_id(operation_id: Option<LitStr>) -> Option<TokenStream> {
|
||||
match operation_id {
|
||||
Some(operation_id) => Some(quote! {
|
||||
fn operation_id() -> Option<String> {
|
||||
|
@ -231,16 +318,14 @@ fn expand_operation_id(operation_id: Option<Lit>) -> Option<TokenStream> {
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn expand_operation_id(_: Option<Lit>) -> Option<TokenStream> {
|
||||
fn expand_operation_id(_: Option<LitStr>) -> Option<TokenStream> {
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_wants_auth(wants_auth: Option<Lit>, default: bool) -> TokenStream {
|
||||
let wants_auth = wants_auth.unwrap_or_else(|| {
|
||||
Lit::Bool(LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
})
|
||||
fn expand_wants_auth(wants_auth: Option<LitBool>, default: bool) -> TokenStream {
|
||||
let wants_auth = wants_auth.unwrap_or_else(|| LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
});
|
||||
|
||||
quote! {
|
||||
|
@ -256,22 +341,30 @@ pub fn endpoint_ident(fn_ident: &Ident) -> Ident {
|
|||
|
||||
// clippy doesn't realize that vectors can be used in closures
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_collect))]
|
||||
fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result<TokenStream> {
|
||||
fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result<TokenStream> {
|
||||
// reject unsafe functions
|
||||
if let Some(unsafety) = fun.sig.unsafety {
|
||||
return Err(Error::new(unsafety.span(), "Endpoint handler methods must not be unsafe"));
|
||||
}
|
||||
|
||||
// parse arguments
|
||||
let mut operation_id: Option<Lit> = None;
|
||||
let mut wants_auth: Option<Lit> = None;
|
||||
let mut operation_id: Option<LitStr> = None;
|
||||
let mut wants_auth: Option<LitBool> = None;
|
||||
for meta in attrs {
|
||||
match meta {
|
||||
NestedMeta::Meta(Meta::NameValue(kv)) => {
|
||||
if kv.path.ends_with("operation_id") {
|
||||
operation_id = Some(kv.lit);
|
||||
operation_id = Some(kv.lit.expect_str()?);
|
||||
} else if kv.path.ends_with("wants_auth") {
|
||||
wants_auth = Some(kv.lit);
|
||||
wants_auth = Some(kv.lit.expect_bool()?);
|
||||
} else if kv.path.ends_with("method") {
|
||||
ty.set_method(kv.path.span(), kv.lit.expect_str()?.parse_with(Expr::parse)?)?;
|
||||
} else if kv.path.ends_with("uri") {
|
||||
ty.set_uri(kv.path.span(), kv.lit.expect_str()?)?;
|
||||
} else if kv.path.ends_with("params") {
|
||||
ty.set_params(kv.path.span(), kv.lit.expect_bool()?)?;
|
||||
} else if kv.path.ends_with("body") {
|
||||
ty.set_body(kv.path.span(), kv.lit.expect_bool()?)?;
|
||||
} else {
|
||||
return Err(Error::new(kv.path.span(), "Unknown attribute"));
|
||||
}
|
||||
|
@ -324,8 +417,18 @@ fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) ->
|
|||
Ok(Some(ty))
|
||||
};
|
||||
|
||||
let http_method = ty.http_method();
|
||||
let uri = ty.uri();
|
||||
let http_method = ty.http_method().ok_or_else(|| {
|
||||
Error::new(
|
||||
Span::call_site(),
|
||||
"Missing `method` attribute (e.g. `#[endpoint(method = \"gotham_restful::gotham::hyper::Method::GET\")]`)"
|
||||
)
|
||||
})?;
|
||||
let uri = ty.uri().ok_or_else(|| {
|
||||
Error::new(
|
||||
Span::call_site(),
|
||||
"Missing `uri` attribute (e.g. `#[endpoint(uri = \"custom_endpoint\")]`)"
|
||||
)
|
||||
})?;
|
||||
let has_placeholders = ty.has_placeholders();
|
||||
let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?);
|
||||
let needs_params = ty.needs_params();
|
||||
|
@ -339,7 +442,11 @@ fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) ->
|
|||
|
||||
let mut handle_args: Vec<TokenStream> = Vec::new();
|
||||
if has_placeholders.value {
|
||||
handle_args.push(quote!(placeholders.id));
|
||||
if matches!(ty, EndpointType::Custom { .. }) {
|
||||
handle_args.push(quote!(placeholders));
|
||||
} else {
|
||||
handle_args.push(quote!(placeholders.id));
|
||||
}
|
||||
}
|
||||
if needs_params.value {
|
||||
handle_args.push(quote!(params));
|
||||
|
|
|
@ -87,6 +87,11 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
|||
expand_derive(input, expand_resource_error)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::custom(), attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||
use std::iter;
|
||||
use syn::{Error, Path};
|
||||
use syn::{Error, Lit, LitBool, LitStr, Path, Result};
|
||||
|
||||
pub trait CollectToResult {
|
||||
pub(crate) trait CollectToResult {
|
||||
type Item;
|
||||
|
||||
fn collect_to_result(self) -> Result<Vec<Self::Item>, Error>;
|
||||
fn collect_to_result(self) -> Result<Vec<Self::Item>>;
|
||||
}
|
||||
|
||||
impl<Item, I> CollectToResult for I
|
||||
where
|
||||
I: Iterator<Item = Result<Item, Error>>
|
||||
I: Iterator<Item = Result<Item>>
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn collect_to_result(self) -> Result<Vec<Item>, Error> {
|
||||
self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| match (code, res) {
|
||||
fn collect_to_result(self) -> Result<Vec<Item>> {
|
||||
self.fold(Ok(Vec::new()), |res, code| match (code, res) {
|
||||
(Ok(code), Ok(mut codes)) => {
|
||||
codes.push(code);
|
||||
Ok(codes)
|
||||
|
@ -30,6 +30,27 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ExpectLit {
|
||||
fn expect_bool(self) -> Result<LitBool>;
|
||||
fn expect_str(self) -> Result<LitStr>;
|
||||
}
|
||||
|
||||
impl ExpectLit for Lit {
|
||||
fn expect_bool(self) -> Result<LitBool> {
|
||||
match self {
|
||||
Self::Bool(bool) => Ok(bool),
|
||||
_ => Err(Error::new(self.span(), "Expected boolean literal"))
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_str(self) -> Result<LitStr> {
|
||||
match self {
|
||||
Self::Str(str) => Ok(str),
|
||||
_ => Err(Error::new(self.span(), "Expected string literal"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PathEndsWith {
|
||||
fn ends_with(&self, s: &str) -> bool;
|
||||
}
|
||||
|
@ -40,7 +61,7 @@ impl PathEndsWith for Path {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn remove_parens(input: TokenStream) -> TokenStream {
|
||||
pub(crate) fn remove_parens(input: TokenStream) -> TokenStream {
|
||||
let iter = input.into_iter().flat_map(|tt| {
|
||||
if let TokenTree::Group(group) = &tt {
|
||||
if group.delimiter() == Delimiter::Parenthesis {
|
||||
|
|
40
src/lib.rs
40
src/lib.rs
|
@ -25,6 +25,11 @@ This crate is just as safe as you'd expect from anything written in safe Rust -
|
|||
|
||||
# Endpoints
|
||||
|
||||
There are a set of pre-defined endpoints that should cover the majority of REST APIs. However,
|
||||
it is also possible to define your own endpoints.
|
||||
|
||||
## Pre-defined Endpoints
|
||||
|
||||
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
|
||||
|
||||
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
|
@ -70,6 +75,41 @@ fn read(id: u64) -> Success<Foo> {
|
|||
# }
|
||||
```
|
||||
|
||||
## Custom Endpoints
|
||||
|
||||
Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that
|
||||
of the pre-defined endpoints, but you need to give it more context:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_derive;
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::router::builder::*;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
use gotham_restful::gotham::hyper::Method;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(custom_endpoint)]
|
||||
struct CustomResource;
|
||||
|
||||
/// This type is used to parse path parameters.
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
struct CustomPath {
|
||||
name: String
|
||||
}
|
||||
|
||||
#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)]
|
||||
fn custom_endpoint(path: CustomPath) -> Success<String> {
|
||||
path.name.into()
|
||||
}
|
||||
# fn main() {
|
||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
# route.resource::<CustomResource>("custom");
|
||||
# }));
|
||||
# }
|
||||
```
|
||||
|
||||
# Arguments
|
||||
|
||||
Some endpoints require arguments. Those should be
|
||||
|
|
|
@ -83,10 +83,14 @@ macro_rules! implOpenapiRouter {
|
|||
}
|
||||
|
||||
static URI_PLACEHOLDER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(^|/):(?P<name>[^/]+)(/|$)"#).unwrap());
|
||||
Lazy::new(|| Regex::new(r#"(?P<prefix>^|/):(?P<name>[^/]+)(?P<suffix>/|$)"#).unwrap());
|
||||
let uri: &str = &E::uri();
|
||||
let uri =
|
||||
URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| format!("{{{}}}", &captures["name"]));
|
||||
let uri = URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| {
|
||||
format!(
|
||||
"{}{{{}}}{}",
|
||||
&captures["prefix"], &captures["name"], &captures["suffix"]
|
||||
)
|
||||
});
|
||||
let path = if uri.is_empty() {
|
||||
format!("{}/{}", self.0.scope.unwrap_or_default(), self.1)
|
||||
} else {
|
||||
|
|
|
@ -44,6 +44,56 @@
|
|||
},
|
||||
"openapi": "3.0.2",
|
||||
"paths": {
|
||||
"/custom": {
|
||||
"patch": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/custom/read/{from}/with/{id}": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "from",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"style": "simple"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"style": "simple"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/img/{id}": {
|
||||
"get": {
|
||||
"operationId": "getImage",
|
||||
|
|
|
@ -5,6 +5,7 @@ extern crate gotham_derive;
|
|||
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use gotham::{
|
||||
hyper::Method,
|
||||
pipeline::{new_pipeline, single::single_pipeline},
|
||||
router::builder::*,
|
||||
test::TestServer
|
||||
|
@ -81,6 +82,22 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets>
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(custom_read_with, custom_patch)]
|
||||
struct CustomResource;
|
||||
|
||||
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
|
||||
struct ReadWithPath {
|
||||
from: String,
|
||||
id: u64
|
||||
}
|
||||
|
||||
#[endpoint(method = "Method::GET", uri = "read/:from/with/:id")]
|
||||
fn custom_read_with(_path: ReadWithPath) {}
|
||||
|
||||
#[endpoint(method = "Method::PATCH", uri = "", body = true)]
|
||||
fn custom_patch(_body: String) {}
|
||||
|
||||
#[test]
|
||||
fn openapi_specification() {
|
||||
let info = OpenapiInfo {
|
||||
|
@ -97,8 +114,9 @@ fn openapi_specification() {
|
|||
let server = TestServer::new(build_router(chain, pipelines, |router| {
|
||||
router.with_openapi(info, |mut router| {
|
||||
router.resource::<ImageResource>("img");
|
||||
router.get_openapi("openapi");
|
||||
router.resource::<SecretResource>("secret");
|
||||
router.resource::<CustomResource>("custom");
|
||||
router.get_openapi("openapi");
|
||||
});
|
||||
}))
|
||||
.unwrap();
|
||||
|
|
11
tests/ui/endpoint/custom_method_invalid_expr.rs
Normal file
11
tests/ui/endpoint/custom_method_invalid_expr.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(method = "I like pizza", uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/custom_method_invalid_expr.stderr
Normal file
11
tests/ui/endpoint/custom_method_invalid_expr.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: unexpected token
|
||||
--> $DIR/custom_method_invalid_expr.rs:8:21
|
||||
|
|
||||
8 | #[endpoint(method = "I like pizza", uri = "custom_read")]
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/custom_method_invalid_expr.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/custom_method_invalid_type.rs
Normal file
11
tests/ui/endpoint/custom_method_invalid_type.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(method = "String::new()", uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
8
tests/ui/endpoint/custom_method_invalid_type.stderr
Normal file
8
tests/ui/endpoint/custom_method_invalid_type.stderr
Normal file
|
@ -0,0 +1,8 @@
|
|||
error[E0308]: mismatched types
|
||||
--> $DIR/custom_method_invalid_type.rs:8:21
|
||||
|
|
||||
8 | #[endpoint(method = "String::new()", uri = "custom_read")]
|
||||
| --------------------^^^^^^^^^^^^^^^-----------------------
|
||||
| | |
|
||||
| | expected struct `Method`, found struct `std::string::String`
|
||||
| expected `Method` because of return type
|
11
tests/ui/endpoint/custom_method_missing.rs
Normal file
11
tests/ui/endpoint/custom_method_missing.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
13
tests/ui/endpoint/custom_method_missing.stderr
Normal file
13
tests/ui/endpoint/custom_method_missing.stderr
Normal file
|
@ -0,0 +1,13 @@
|
|||
error: Missing `method` attribute (e.g. `#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]`)
|
||||
--> $DIR/custom_method_missing.rs:8:1
|
||||
|
|
||||
8 | #[endpoint(uri = "custom_read")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/custom_method_missing.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/custom_uri_missing.rs
Normal file
11
tests/ui/endpoint/custom_uri_missing.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
13
tests/ui/endpoint/custom_uri_missing.stderr
Normal file
13
tests/ui/endpoint/custom_uri_missing.stderr
Normal file
|
@ -0,0 +1,13 @@
|
|||
error: Missing `uri` attribute (e.g. `#[endpoint(uri = "custom_endpoint")]`)
|
||||
--> $DIR/custom_uri_missing.rs:8:1
|
||||
|
|
||||
8 | #[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/custom_uri_missing.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/non_custom_body_attribute.rs
Normal file
11
tests/ui/endpoint/non_custom_body_attribute.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(body = false)]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/non_custom_body_attribute.stderr
Normal file
11
tests/ui/endpoint/non_custom_body_attribute.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: `body` can only be used on custom endpoints
|
||||
--> $DIR/non_custom_body_attribute.rs:8:12
|
||||
|
|
||||
8 | #[read_all(body = false)]
|
||||
| ^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/non_custom_body_attribute.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/non_custom_method_attribute.rs
Normal file
11
tests/ui/endpoint/non_custom_method_attribute.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(method = "gotham_restful::gotham::hyper::Method::GET")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/non_custom_method_attribute.stderr
Normal file
11
tests/ui/endpoint/non_custom_method_attribute.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: `method` can only be used on custom endpoints
|
||||
--> $DIR/non_custom_method_attribute.rs:8:12
|
||||
|
|
||||
8 | #[read_all(method = "gotham_restful::gotham::hyper::Method::GET")]
|
||||
| ^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/non_custom_method_attribute.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/non_custom_params_attribute.rs
Normal file
11
tests/ui/endpoint/non_custom_params_attribute.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(params = true)]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/non_custom_params_attribute.stderr
Normal file
11
tests/ui/endpoint/non_custom_params_attribute.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: `params` can only be used on custom endpoints
|
||||
--> $DIR/non_custom_params_attribute.rs:8:12
|
||||
|
|
||||
8 | #[read_all(params = true)]
|
||||
| ^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/non_custom_params_attribute.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/non_custom_uri_attribute.rs
Normal file
11
tests/ui/endpoint/non_custom_uri_attribute.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/non_custom_uri_attribute.stderr
Normal file
11
tests/ui/endpoint/non_custom_uri_attribute.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: `uri` can only be used on custom endpoints
|
||||
--> $DIR/non_custom_uri_attribute.rs:8:12
|
||||
|
|
||||
8 | #[read_all(uri = "custom_read")]
|
||||
| ^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/non_custom_uri_attribute.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/wants_auth_non_bool.rs
Normal file
11
tests/ui/endpoint/wants_auth_non_bool.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(wants_auth = "yes, please")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/wants_auth_non_bool.stderr
Normal file
11
tests/ui/endpoint/wants_auth_non_bool.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: Expected boolean literal
|
||||
--> $DIR/wants_auth_non_bool.rs:8:25
|
||||
|
|
||||
8 | #[read_all(wants_auth = "yes, please")]
|
||||
| ^^^^^^^^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/wants_auth_non_bool.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
Loading…
Add table
Reference in a new issue