1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-22 20:52:27 +00:00

Replace methods with more flexible endpoints

This commit is contained in:
msrd0 2021-01-18 00:05:30 +00:00
parent 0ac0f0f504
commit b807ae2796
87 changed files with 1497 additions and 1512 deletions

View file

@ -6,23 +6,24 @@ stages:
variables:
CARGO_HOME: $CI_PROJECT_DIR/cargo
RUST_LOG: info,gotham=debug,gotham_restful=trace
test-default:
stage: test
image: rust:1.42-slim
image: rust:1.43-slim
before_script:
- cargo -V
script:
- cargo test
cache:
key: cargo-1-42-default
key: cargo-1-43-default
paths:
- cargo/
- target/
test-full:
stage: test
image: rust:1.42-slim
image: rust:1.43-slim
before_script:
- apt update -y
- apt install -y --no-install-recommends libpq-dev
@ -30,7 +31,7 @@ test-full:
script:
- cargo test --no-default-features --features full
cache:
key: cargo-1-42-all
key: cargo-1-43-all
paths:
- cargo/
- target/
@ -86,6 +87,7 @@ rustfmt:
- cargo fmt --version
script:
- cargo fmt -- --check
- ./tests/ui/rustfmt.sh --check
doc:
stage: build

View file

@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Support custom HTTP response headers
- New `endpoint` router extension with associated `Endpoint` trait ([!18])
### Changed
- The cors handler can now copy headers from the request if desired
- All fields of `Response` are now private
- If not enabling the `openapi` feature, `without-openapi` has to be enabled
- The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18])
### Removed
- All pre-defined methods (`read`, `create`, ...) from our router extensions ([!18])
- All pre-defined method traits (`ResourceRead`, ...) ([!18])
## [0.1.1] - 2020-12-28
### Added
@ -25,3 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1.0] - 2020-10-02
Previous changes are not tracked by this changelog file. Refer to the [releases](https://gitlab.com/msrd0/gotham-restful/-/releases) for the changelog.
[!18]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/18

View file

@ -33,7 +33,9 @@ indexmap = { version = "1.3.2", optional = true }
jsonwebtoken = { version = "7.1.0", optional = true }
log = "0.4.8"
mime = "0.3.16"
once_cell = { version = "1.5", optional = true }
openapiv3 = { version = "0.3.2", optional = true }
regex = { version = "1.4", optional = true }
serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.58"
uuid = { version = "0.8.1", optional = true }
@ -42,12 +44,13 @@ uuid = { version = "0.8.1", optional = true }
diesel = { version = "1.4.4", features = ["postgres"] }
futures-executor = "0.3.5"
paste = "1.0"
pretty_env_logger = "0.4"
thiserror = "1.0.18"
trybuild = "1.0.27"
[features]
default = ["cors", "errorlog", "without-openapi"]
full = ["auth", "cors", "database", "errorlog", "openapi"]
full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"]
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
cors = []
@ -56,7 +59,7 @@ errorlog = []
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
without-openapi = []
openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"]
openapi = ["gotham_restful_derive/openapi", "indexmap", "once_cell", "openapiv3", "regex"]
[package.metadata.docs.rs]
no-default-features = true

113
README.md
View file

@ -17,8 +17,8 @@
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
</a>
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
<a href="https://blog.rust-lang.org/2020/04/23/Rust-1.43.0.html">
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.43+-orange.svg"/>
</a>
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
@ -27,7 +27,7 @@
<br/>
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
create resources with assigned methods that aim to be a more convenient way of creating handlers
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
for requests.
## Features
@ -45,23 +45,23 @@ for requests.
This crate is just as safe as you'd expect from anything written in safe Rust - and
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
## Methods
## Endpoints
Assuming you assign `/foobar` to your resource, you can implement the following methods:
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
| ----------- | ------------------ | --------- | ----------- |
| read_all | | GET | /foobar |
| read | id | GET | /foobar/:id |
| search | query | GET | /foobar/search |
| create | body | POST | /foobar |
| change_all | body | PUT | /foobar |
| change | id, body | PUT | /foobar/:id |
| remove_all | | DELETE | /foobar |
| remove | id | DELETE | /foobar/:id |
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
| ------------- | ------------------ | --------- | -------------- |
| read_all | | GET | /foobar |
| read | id | GET | /foobar/:id |
| search | query | GET | /foobar/search |
| create | body | POST | /foobar |
| change_all | body | PUT | /foobar |
| change | id, body | PUT | /foobar/:id |
| remove_all | | DELETE | /foobar |
| remove | id | DELETE | /foobar/:id |
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
simple example could look like this:
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
simple example looks like this:
```rust
/// Our RESTful resource.
@ -69,14 +69,14 @@ simple example could look like this:
#[resource(read)]
struct FooResource;
/// The return type of the foo read method.
/// The return type of the foo read endpoint.
#[derive(Serialize)]
struct Foo {
id: u64
}
/// The foo read method handler.
#[read(FooResource)]
/// The foo read endpoint.
#[read]
fn read(id: u64) -> Success<Foo> {
Foo { id }.into()
}
@ -84,17 +84,16 @@ fn read(id: u64) -> Success<Foo> {
## Arguments
Some methods require arguments. Those should be
* **id** Should be a deserializable json-primitive like `i64` or `String`.
Some endpoints require arguments. Those should be
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
* **query** Should be any deserializable object whose variables are json-primitives. It will
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
type needs to implement [`QueryStringExtractor`].
type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor).
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
have an async handler (that is, the function that the method macro is invoked on is declared
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
`Sync` there is unfortunately no more convenient way.
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
improve.
## Uploads and Downloads
@ -114,7 +113,7 @@ struct RawImage {
content_type: Mime
}
#[create(ImageResource)]
#[create]
fn create(body : RawImage) -> Raw<Vec<u8>> {
Raw::new(body.content, body.content_type)
}
@ -126,21 +125,23 @@ To make life easier for common use-cases, this create offers a few features that
when you implement your web server. The complete feature list is
- [`auth`](#authentication-feature) Advanced JWT middleware
- `chrono` openapi support for chrono types
- [`cors`](#cors-feature) CORS handling for all method handlers
- `full` enables all features except `without-openapi`
- [`cors`](#cors-feature) CORS handling for all endpoint handlers
- [`database`](#database-feature) diesel middleware support
- `errorlog` log errors returned from method handlers
- `errorlog` log errors returned from endpoint handlers
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
- `uuid` openapi support for uuid
- `without-openapi` (**default**) disables `openapi` support.
### Authentication Feature
In order to enable authentication support, enable the `auth` feature gate. This allows you to
register a middleware that can automatically check for the existence of an JWT authentication
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
None of this is currently supported by gotham's own JWT middleware.
A simple example that uses only a single secret could look like this:
A simple example that uses only a single secret looks like this:
```rust
#[derive(Resource)]
@ -159,7 +160,7 @@ struct AuthData {
exp: u64
}
#[read(SecretResource)]
#[read]
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
let intended_for = auth.ok()?.sub;
Ok(Secret { id, intended_for })
@ -185,14 +186,14 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a
configuration as a middleware.
A simple example that allows authentication from every origin (note that `*` always disallows
authentication), and every content type, could look like this:
authentication), and every content type, looks like this:
```rust
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
#[read_all]
fn read_all() {
// your handler
}
@ -221,7 +222,7 @@ note however that due to the way gotham's diesel middleware implementation, it i
to run async code while holding a database connection. If you need to combine async and database,
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
A simple non-async example could look like this:
A simple non-async example looks like this:
```rust
#[derive(Resource)]
@ -234,7 +235,7 @@ struct Foo {
value: String
}
#[read_all(FooResource)]
#[read_all]
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
foo::table.load(conn)
}
@ -261,7 +262,7 @@ In order to automatically create an openapi specification, gotham-restful needs
all routes and the types returned. `serde` does a great job at serialization but doesn't give
enough type information, so all types used in the router need to implement `OpenapiType`. This
can be derived for almoust any type and there should be no need to implement it manually. A simple
example could look like this:
example looks like this:
```rust
#[derive(Resource)]
@ -273,7 +274,7 @@ struct Foo {
bar: String
}
#[read_all(FooResource)]
#[read_all]
fn read_all() -> Success<Foo> {
Foo { bar: "Hello World".to_owned() }.into()
}
@ -293,22 +294,17 @@ fn main() {
}
```
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`
that will return the generated openapi specification. This allows you to easily write clients
in different languages without worying to exactly replicate your api in each of those languages.
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`.
It will return the generated openapi specification in JSON format. This allows you to easily write
clients in different languages without worying to exactly replicate your api in each of those
languages.
However, as of right now there is one caveat. If you wrote code before enabling the openapi feature,
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
In other words, put
```toml
[features]
openapi = ["gotham-restful/openapi"]
```
into your libraries `Cargo.toml` and use the following for all types used with handlers:
However, please note that by default, the `without-openapi` feature of this crate is enabled.
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
on some of the types in [`Endpoint`] and related traits. This means that some code might only
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
it is strongly recommended to pass both features through and conditionally enable the openapi
code, like this:
```rust
#[derive(Deserialize, Serialize)]
@ -318,18 +314,15 @@ struct Foo;
## Examples
There is a lack of good examples, but there is currently a collection of code in the [example]
directory, that might help you. Any help writing more examples is highly appreciated.
This readme and the crate documentation contain some of example. In addition to that, there is
a collection of code in the [example] directory that might help you. Any help writing more
examples is highly appreciated.
[diesel]: https://diesel.rs/
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
[gotham]: https://gotham.rs/
[serde_json]: https://github.com/serde-rs/json#serde-json----
[`CorsRoute`]: trait.CorsRoute.html
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
[`RequestBody`]: trait.RequestBody.html
[`State`]: ../gotham/state/struct.State.html
## Versioning

View file

@ -17,8 +17,8 @@
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
</a>
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
<a href="https://blog.rust-lang.org/2020/04/23/Rust-1.43.0.html">
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.43+-orange.svg"/>
</a>
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>

469
derive/src/endpoint.rs Normal file
View file

@ -0,0 +1,469 @@
use crate::util::{CollectToResult, PathEndsWith};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use std::str::FromStr;
use syn::{
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result,
ReturnType, Type
};
pub enum EndpointType {
ReadAll,
Read,
Search,
Create,
UpdateAll,
Update,
DeleteAll,
Delete
}
impl FromStr for EndpointType {
type Err = Error;
fn from_str(str: &str) -> Result<Self> {
match str {
"ReadAll" | "read_all" => Ok(Self::ReadAll),
"Read" | "read" => Ok(Self::Read),
"Search" | "search" => Ok(Self::Search),
"Create" | "create" => Ok(Self::Create),
"ChangeAll" | "change_all" => Ok(Self::UpdateAll),
"Change" | "change" => Ok(Self::Update),
"RemoveAll" | "remove_all" => Ok(Self::DeleteAll),
"Remove" | "remove" => Ok(Self::Delete),
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
}
}
}
impl EndpointType {
fn http_method(&self) -> TokenStream {
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)
}
}
fn uri(&self) -> TokenStream {
match self {
Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => quote!(""),
Self::Read | Self::Update | Self::Delete => quote!(":id"),
Self::Search => quote!("search")
}
}
fn has_placeholders(&self) -> LitBool {
match self {
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => LitBool {
value: false,
span: Span::call_site()
},
Self::Read | Self::Update | Self::Delete => LitBool {
value: true,
span: Span::call_site()
}
}
}
fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
match self {
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>)
}
}
fn needs_params(&self) -> LitBool {
match self {
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
LitBool {
value: false,
span: Span::call_site()
}
},
Self::Search => LitBool {
value: true,
span: Span::call_site()
}
}
}
fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
match self {
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)
}
}
fn needs_body(&self) -> LitBool {
match self {
Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => LitBool {
value: false,
span: Span::call_site()
},
Self::Create | Self::UpdateAll | Self::Update => LitBool {
value: true,
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)
}
}
}
#[allow(clippy::large_enum_variant)]
enum HandlerArgType {
StateRef,
StateMutRef,
MethodArg(Type),
DatabaseConnection(Type),
AuthStatus(Type),
AuthStatusRef(Type)
}
impl HandlerArgType {
fn is_method_arg(&self) -> bool {
matches!(self, Self::MethodArg(_))
}
fn is_database_conn(&self) -> bool {
matches!(self, Self::DatabaseConnection(_))
}
fn is_auth_status(&self) -> bool {
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
}
fn ty(&self) -> Option<&Type> {
match self {
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
_ => None
}
}
fn quote_ty(&self) -> Option<TokenStream> {
self.ty().map(|ty| quote!(#ty))
}
}
struct HandlerArg {
ident_span: Span,
ty: HandlerArgType
}
impl Spanned for HandlerArg {
fn span(&self) -> Span {
self.ident_span
}
}
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<HandlerArgType> {
let attr = attrs
.iter()
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
.map(|arg| arg.tokens.to_string());
// TODO issue a warning for _state usage once diagnostics become stable
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
return match ty {
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
HandlerArgType::StateRef
} else {
HandlerArgType::StateMutRef
}),
_ => Err(Error::new(
ty.span(),
"The state parameter has to be a (mutable) reference to gotham_restful::State"
))
};
}
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
return Ok(match ty {
Type::Reference(ty) => HandlerArgType::AuthStatusRef(*ty.elem),
ty => HandlerArgType::AuthStatus(ty)
});
}
if cfg!(feature = "database")
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
{
return Ok(HandlerArgType::DatabaseConnection(match ty {
Type::Reference(ty) => *ty.elem,
ty => ty
}));
}
Ok(HandlerArgType::MethodArg(ty))
}
fn interpret_arg(_index: usize, arg: &PatType) -> Result<HandlerArg> {
let pat = &arg.pat;
let orig_name = quote!(#pat);
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
Ok(HandlerArg {
ident_span: arg.pat.span(),
ty
})
}
#[cfg(feature = "openapi")]
fn expand_operation_id(operation_id: Option<Lit>) -> Option<TokenStream> {
match operation_id {
Some(operation_id) => Some(quote! {
fn operation_id() -> Option<String> {
Some(#operation_id.to_string())
}
}),
None => None
}
}
#[cfg(not(feature = "openapi"))]
fn expand_operation_id(_: Option<Lit>) -> 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()
})
});
quote! {
fn wants_auth() -> bool {
#wants_auth
}
}
}
pub fn endpoint_ident(fn_ident: &Ident) -> Ident {
format_ident!("{}___gotham_restful_endpoint", fn_ident)
}
fn expand_endpoint_type(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;
for meta in attrs {
match meta {
NestedMeta::Meta(Meta::NameValue(kv)) => {
if kv.path.ends_with("operation_id") {
operation_id = Some(kv.lit);
} else if kv.path.ends_with("wants_auth") {
wants_auth = Some(kv.lit);
} else {
return Err(Error::new(kv.path.span(), "Unknown attribute"));
}
},
_ => return Err(Error::new(meta.span(), "Invalid attribute syntax"))
}
}
#[cfg(not(feature = "openapi"))]
if let Some(operation_id) = operation_id {
return Err(Error::new(
operation_id.span(),
"`operation_id` is only supported with the openapi feature"
));
}
// extract arguments into pattern, ident and type
let args = fun
.sig
.inputs
.iter()
.enumerate()
.map(|(i, arg)| match arg {
FnArg::Typed(arg) => interpret_arg(i, arg),
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
})
.collect_to_result()?;
let fun_vis = &fun.vis;
let fun_ident = &fun.sig.ident;
let fun_is_async = fun.sig.asyncness.is_some();
let ident = endpoint_ident(fun_ident);
let dummy_ident = format_ident!("_IMPL_Endpoint_for_{}", ident);
let (output_ty, is_no_content) = match &fun.sig.output {
ReturnType::Default => (quote!(::gotham_restful::NoContent), true),
ReturnType::Type(_, ty) => (quote!(#ty), false)
};
let arg_tys = args.iter().filter(|arg| arg.ty.is_method_arg()).collect::<Vec<_>>();
let mut arg_ty_idx = 0;
let mut next_arg_ty = |return_none: bool| {
if return_none {
return Ok(None);
}
if arg_ty_idx >= arg_tys.len() {
return Err(Error::new(fun_ident.span(), "Too few arguments"));
}
let ty = arg_tys[arg_ty_idx].ty.ty().unwrap();
arg_ty_idx += 1;
Ok(Some(ty))
};
let http_method = ty.http_method();
let uri = ty.uri();
let has_placeholders = ty.has_placeholders();
let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?);
let needs_params = ty.needs_params();
let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?);
let needs_body = ty.needs_body();
let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?);
if arg_ty_idx < arg_tys.len() {
return Err(Error::new(fun_ident.span(), "Too many arguments"));
}
let mut handle_args: Vec<TokenStream> = Vec::new();
if has_placeholders.value {
handle_args.push(quote!(placeholders.id));
}
if needs_params.value {
handle_args.push(quote!(params));
}
if needs_body.value {
handle_args.push(quote!(body.unwrap()));
}
let handle_args = args.iter().map(|arg| match arg.ty {
HandlerArgType::StateRef | HandlerArgType::StateMutRef => quote!(state),
HandlerArgType::MethodArg(_) => handle_args.remove(0),
HandlerArgType::DatabaseConnection(_) => quote!(&conn),
HandlerArgType::AuthStatus(_) => quote!(auth),
HandlerArgType::AuthStatusRef(_) => quote!(&auth)
});
let expand_handle_content = || {
let mut state_block = quote!();
if let Some(arg) = args.iter().find(|arg| arg.ty.is_auth_status()) {
let auth_ty = arg.ty.quote_ty();
state_block = quote! {
#state_block
let auth: #auth_ty = state.borrow::<#auth_ty>().clone();
}
}
let mut handle_content = quote!(#fun_ident(#(#handle_args),*));
if fun_is_async {
if let Some(arg) = args.iter().find(|arg| matches!(arg.ty, HandlerArgType::StateRef)) {
return Err(Error::new(arg.span(), "Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`"));
}
handle_content = quote!(#handle_content.await);
}
if is_no_content {
handle_content = quote!(#handle_content; ::gotham_restful::NoContent)
}
if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) {
let conn_ty = arg.ty.quote_ty();
state_block = quote! {
#state_block
let repo = <::gotham_restful::export::Repo<#conn_ty>>::borrow_from(state).clone();
};
handle_content = quote! {
repo.run::<_, _, ()>(move |conn| {
Ok({ #handle_content })
}).await.unwrap()
};
}
Ok(quote! {
use ::gotham_restful::export::FutureExt as _;
#state_block
async move {
#handle_content
}.boxed()
})
};
let handle_content = match expand_handle_content() {
Ok(content) => content,
Err(err) => err.to_compile_error()
};
let tr8 = if cfg!(feature = "openapi") {
quote!(::gotham_restful::EndpointWithSchema)
} else {
quote!(::gotham_restful::Endpoint)
};
let operation_id = expand_operation_id(operation_id);
let wants_auth = expand_wants_auth(wants_auth, args.iter().any(|arg| arg.ty.is_auth_status()));
Ok(quote! {
#[doc(hidden)]
/// `gotham_restful` implementation detail
#[allow(non_camel_case_types)]
#fun_vis struct #ident;
#[allow(non_upper_case_globals)]
static #dummy_ident: () = {
impl #tr8 for #ident {
fn http_method() -> ::gotham_restful::gotham::hyper::Method {
#http_method
}
fn uri() -> ::std::borrow::Cow<'static, str> {
{ #uri }.into()
}
type Output = #output_ty;
fn has_placeholders() -> bool {
#has_placeholders
}
type Placeholders = #placeholder_ty;
fn needs_params() -> bool {
#needs_params
}
type Params = #params_ty;
fn needs_body() -> bool {
#needs_body
}
type Body = #body_ty;
fn handle(
state: &mut ::gotham_restful::gotham::state::State,
placeholders: Self::Placeholders,
params: Self::Params,
body: ::std::option::Option<Self::Body>
) -> ::gotham_restful::export::BoxFuture<'static, Self::Output> {
#handle_content
}
#operation_id
#wants_auth
}
};
})
}
pub fn expand_endpoint(ty: EndpointType, attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
let endpoint_type = match expand_endpoint_type(ty, attrs, &fun) {
Ok(code) => code,
Err(err) => err.to_compile_error()
};
Ok(quote! {
#fun
#endpoint_type
})
}

View file

@ -5,24 +5,32 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re
mod util;
mod endpoint;
use endpoint::{expand_endpoint, EndpointType};
mod from_body;
use from_body::expand_from_body;
mod method;
use method::{expand_method, Method};
mod request_body;
use request_body::expand_request_body;
mod resource;
use resource::expand_resource;
mod resource_error;
use resource_error::expand_resource_error;
#[cfg(feature = "openapi")]
mod openapi_type;
#[cfg(feature = "openapi")]
use openapi_type::expand_openapi_type;
mod private_openapi_trait;
use private_openapi_trait::expand_private_openapi_trait;
#[inline]
fn print_tokens(tokens: TokenStream2) -> TokenStream {
//eprintln!("{}", tokens);
// eprintln!("{}", tokens);
tokens.into()
}
@ -77,40 +85,47 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream {
#[proc_macro_attribute]
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
}
#[proc_macro_attribute]
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item))
}
#[proc_macro_attribute]
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item))
}
#[proc_macro_attribute]
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item))
}
#[proc_macro_attribute]
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item))
}
#[proc_macro_attribute]
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item))
}
#[proc_macro_attribute]
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item))
}
#[proc_macro_attribute]
pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item))
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Delete, attr, item))
}
/// PRIVATE MACRO - DO NOT USE
#[doc(hidden)]
#[proc_macro_attribute]
pub fn _private_openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_macro(attr, item, expand_private_openapi_trait)
}

View file

@ -1,466 +0,0 @@
use crate::util::CollectToResult;
use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use std::str::FromStr;
use syn::{
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Path, Result,
ReturnType, Type
};
pub enum Method {
ReadAll,
Read,
Search,
Create,
ChangeAll,
Change,
RemoveAll,
Remove
}
impl FromStr for Method {
type Err = Error;
fn from_str(str: &str) -> Result<Self> {
match str {
"ReadAll" | "read_all" => Ok(Self::ReadAll),
"Read" | "read" => Ok(Self::Read),
"Search" | "search" => Ok(Self::Search),
"Create" | "create" => Ok(Self::Create),
"ChangeAll" | "change_all" => Ok(Self::ChangeAll),
"Change" | "change" => Ok(Self::Change),
"RemoveAll" | "remove_all" => Ok(Self::RemoveAll),
"Remove" | "remove" => Ok(Self::Remove),
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
}
}
}
impl Method {
pub fn type_names(&self) -> Vec<&'static str> {
use Method::*;
match self {
ReadAll | RemoveAll => vec![],
Read | Remove => vec!["ID"],
Search => vec!["Query"],
Create | ChangeAll => vec!["Body"],
Change => vec!["ID", "Body"]
}
}
pub fn trait_ident(&self) -> Ident {
use Method::*;
let name = match self {
ReadAll => "ReadAll",
Read => "Read",
Search => "Search",
Create => "Create",
ChangeAll => "ChangeAll",
Change => "Change",
RemoveAll => "RemoveAll",
Remove => "Remove"
};
format_ident!("Resource{}", name)
}
pub fn fn_ident(&self) -> Ident {
use Method::*;
let name = match self {
ReadAll => "read_all",
Read => "read",
Search => "search",
Create => "create",
ChangeAll => "change_all",
Change => "change",
RemoveAll => "remove_all",
Remove => "remove"
};
format_ident!("{}", name)
}
pub fn handler_struct_ident(&self, resource: &str) -> Ident {
format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident())
}
pub fn setup_ident(&self, resource: &str) -> Ident {
format_ident!("_gotham_restful_{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident())
}
}
#[allow(clippy::large_enum_variant)]
enum MethodArgumentType {
StateRef,
StateMutRef,
MethodArg(Type),
DatabaseConnection(Type),
AuthStatus(Type),
AuthStatusRef(Type)
}
impl MethodArgumentType {
fn is_method_arg(&self) -> bool {
matches!(self, Self::MethodArg(_))
}
fn is_database_conn(&self) -> bool {
matches!(self, Self::DatabaseConnection(_))
}
fn is_auth_status(&self) -> bool {
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
}
fn ty(&self) -> Option<&Type> {
match self {
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
_ => None
}
}
fn quote_ty(&self) -> Option<TokenStream> {
self.ty().map(|ty| quote!(#ty))
}
}
struct MethodArgument {
ident: Ident,
ident_span: Span,
ty: MethodArgumentType
}
impl Spanned for MethodArgument {
fn span(&self) -> Span {
self.ident_span
}
}
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<MethodArgumentType> {
let attr = attrs
.iter()
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
.map(|arg| arg.tokens.to_string());
// TODO issue a warning for _state usage once diagnostics become stable
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
return match ty {
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
MethodArgumentType::StateRef
} else {
MethodArgumentType::StateMutRef
}),
_ => Err(Error::new(
ty.span(),
"The state parameter has to be a (mutable) reference to gotham_restful::State"
))
};
}
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
return Ok(match ty {
Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem),
ty => MethodArgumentType::AuthStatus(ty)
});
}
if cfg!(feature = "database")
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
{
return Ok(MethodArgumentType::DatabaseConnection(match ty {
Type::Reference(ty) => *ty.elem,
ty => ty
}));
}
Ok(MethodArgumentType::MethodArg(ty))
}
fn interpret_arg(index: usize, arg: &PatType) -> Result<MethodArgument> {
let pat = &arg.pat;
let ident = format_ident!("arg{}", index);
let orig_name = quote!(#pat);
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
Ok(MethodArgument {
ident,
ident_span: arg.pat.span(),
ty
})
}
#[cfg(feature = "openapi")]
fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream {
let mut operation_id: Option<&Lit> = None;
for meta in attrs {
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) {
operation_id = Some(&kv.lit)
}
}
}
match operation_id {
Some(operation_id) => quote! {
fn operation_id() -> Option<String>
{
Some(#operation_id.to_string())
}
},
None => quote!()
}
}
#[cfg(not(feature = "openapi"))]
fn expand_operation_id(_: &[NestedMeta]) -> TokenStream {
quote!()
}
fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream {
let default_lit = Lit::Bool(LitBool {
value: default,
span: Span::call_site()
});
let mut wants_auth = &default_lit;
for meta in attrs {
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) {
wants_auth = &kv.lit
}
}
}
quote! {
fn wants_auth() -> bool
{
#wants_auth
}
}
}
#[allow(clippy::comparison_chain)]
fn setup_body(
method: &Method,
fun: &ItemFn,
attrs: &[NestedMeta],
resource_name: &str,
resource_path: &Path
) -> Result<TokenStream> {
let krate = super::krate();
let fun_ident = &fun.sig.ident;
let fun_is_async = fun.sig.asyncness.is_some();
if let Some(unsafety) = fun.sig.unsafety {
return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe"));
}
let trait_ident = method.trait_ident();
let method_ident = method.fn_ident();
let handler_ident = method.handler_struct_ident(resource_name);
let (ret, is_no_content) = match &fun.sig.output {
ReturnType::Default => (quote!(#krate::NoContent), true),
ReturnType::Type(_, ty) => (quote!(#ty), false)
};
// some default idents we'll need
let state_ident = format_ident!("state");
let repo_ident = format_ident!("repo");
let conn_ident = format_ident!("conn");
let auth_ident = format_ident!("auth");
let res_ident = format_ident!("res");
// extract arguments into pattern, ident and type
let args = fun
.sig
.inputs
.iter()
.enumerate()
.map(|(i, arg)| match arg {
FnArg::Typed(arg) => interpret_arg(i, arg),
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
})
.collect_to_result()?;
// extract the generic parameters to use
let ty_names = method.type_names();
let ty_len = ty_names.len();
let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect();
if generics_args.len() > ty_len {
return Err(Error::new(generics_args[ty_len].span(), "Too many arguments"));
} else if generics_args.len() < ty_len {
return Err(Error::new(fun_ident.span(), "Too few arguments"));
}
let generics: Vec<TokenStream> = generics_args
.iter()
.map(|arg| arg.ty.quote_ty().unwrap())
.zip(ty_names)
.map(|(arg, name)| {
let ident = format_ident!("{}", name);
quote!(type #ident = #arg;)
})
.collect();
// extract the definition of our method
let mut args_def: Vec<TokenStream> = args
.iter()
.filter(|arg| (*arg).ty.is_method_arg())
.map(|arg| {
let ident = &arg.ident;
let ty = arg.ty.quote_ty();
quote!(#ident : #ty)
})
.collect();
args_def.insert(0, quote!(mut #state_ident : #krate::State));
// extract the arguments to pass over to the supplied method
let args_pass: Vec<TokenStream> = args
.iter()
.map(|arg| match (&arg.ty, &arg.ident) {
(MethodArgumentType::StateRef, _) => quote!(&#state_ident),
(MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident),
(MethodArgumentType::MethodArg(_), ident) => quote!(#ident),
(MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident),
(MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident),
(MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident)
})
.collect();
// prepare the method block
let mut block = quote!(#fun_ident(#(#args_pass),*));
let mut state_block = quote!();
if fun_is_async {
if let Some(arg) = args.iter().find(|arg| matches!((*arg).ty, MethodArgumentType::StateRef)) {
return Err(Error::new(
arg.span(),
"async fn must not take &State as an argument as State is not Sync, consider taking &mut State"
));
}
block = quote!(#block.await);
}
if is_no_content {
block = quote!(#block; Default::default())
}
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) {
if fun_is_async {
return Err(Error::new(
arg.span(),
"async fn is not supported when database support is required, consider boxing"
));
}
let conn_ty = arg.ty.quote_ty();
state_block = quote! {
#state_block
let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone();
};
block = quote! {
{
let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| {
let #res_ident = { #block };
Ok((#state_ident, #res_ident))
}).await.unwrap();
#state_ident = #res_ident.0;
#res_ident.1
}
};
}
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) {
let auth_ty = arg.ty.quote_ty();
state_block = quote! {
#state_block
let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone();
};
}
// prepare the where clause
let mut where_clause = quote!(#resource_path : #krate::Resource,);
for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) {
let auth_ty = arg.ty.quote_ty();
where_clause = quote!(#where_clause #auth_ty : Clone,);
}
// attribute generated code
let operation_id = expand_operation_id(attrs);
let wants_auth = expand_wants_auth(attrs, args.iter().any(|arg| (*arg).ty.is_auth_status()));
// put everything together
let mut dummy = format_ident!("_IMPL_RESOURCEMETHOD_FOR_{}", fun_ident);
dummy.set_span(Span::call_site());
Ok(quote! {
struct #handler_ident;
impl #krate::ResourceMethod for #handler_ident {
type Res = #ret;
#operation_id
#wants_auth
}
impl #krate::#trait_ident for #handler_ident
where #where_clause
{
#(#generics)*
fn #method_ident(#(#args_def),*) -> std::pin::Pin<Box<dyn std::future::Future<Output = (#krate::State, #ret)> + Send>> {
#[allow(unused_imports)]
use #krate::{export::FutureExt, FromState};
#state_block
async move {
let #res_ident = { #block };
(#state_ident, #res_ident)
}.boxed()
}
}
route.#method_ident::<#handler_ident>();
})
}
pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
let krate = super::krate();
// parse attributes
if attrs.len() < 1 {
return Err(Error::new(
Span::call_site(),
"Missing Resource struct. Example: #[read_all(MyResource)]"
));
}
let resource_path = match attrs.remove(0) {
NestedMeta::Meta(Meta::Path(path)) => path,
p => {
return Err(Error::new(
p.span(),
"Expected name of the Resource struct this method belongs to"
))
},
};
let resource_name = resource_path
.segments
.last()
.map(|s| s.ident.to_string())
.ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?;
let fun_vis = &fun.vis;
let setup_ident = method.setup_ident(&resource_name);
let setup_body = match setup_body(&method, &fun, &attrs, &resource_name, &resource_path) {
Ok(body) => body,
Err(err) => err.to_compile_error()
};
Ok(quote! {
#fun
#[deny(dead_code)]
#[doc(hidden)]
/// `gotham_restful` implementation detail.
#fun_vis fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D) {
#setup_body
}
})
}

View file

@ -3,7 +3,8 @@ use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound,
TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate
};
pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
@ -17,24 +18,46 @@ pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
}
}
fn expand_where(generics: &Generics) -> TokenStream {
fn update_generics(generics: &Generics, where_clause: &mut Option<WhereClause>) {
if generics.params.is_empty() {
return quote!();
return;
}
let krate = super::krate();
let idents = generics
.params
.iter()
.map(|param| match param {
GenericParam::Type(ty) => Some(ty.ident.clone()),
_ => None
})
.filter(|param| param.is_some())
.map(|param| param.unwrap());
if where_clause.is_none() {
*where_clause = Some(WhereClause {
where_token: Default::default(),
predicates: Default::default()
});
}
let where_clause = where_clause.as_mut().unwrap();
quote! {
where #(#idents : #krate::OpenapiType),*
for param in &generics.params {
if let GenericParam::Type(ty_param) = param {
where_clause.predicates.push(WherePredicate::Type(PredicateType {
lifetimes: None,
bounded_ty: Type::Path(TypePath {
qself: None,
path: Path {
leading_colon: None,
segments: vec![PathSegment {
ident: ty_param.ident.clone(),
arguments: Default::default()
}]
.into_iter()
.collect()
}
}),
colon_token: Default::default(),
bounds: vec![TypeParamBound::Trait(TraitBound {
paren_token: None,
modifier: TraitBoundModifier::None,
lifetimes: None,
path: syn::parse_str("::gotham_restful::OpenapiType").unwrap()
})]
.into_iter()
.collect()
}));
}
}
}
@ -106,7 +129,9 @@ fn expand_variant(variant: &Variant) -> Result<TokenStream> {
fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
let krate = super::krate();
let where_clause = expand_where(&generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut where_clause = where_clause.cloned();
update_generics(&generics, &mut where_clause);
let attrs = parse_attributes(&attrs)?;
let nullable = attrs.nullable;
@ -118,7 +143,7 @@ fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: D
let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
Ok(quote! {
impl #generics #krate::OpenapiType for #ident #generics
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
#where_clause
{
fn schema() -> #krate::OpenapiSchema
@ -208,7 +233,9 @@ fn expand_field(field: &Field) -> Result<TokenStream> {
fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
let krate = super::krate();
let where_clause = expand_where(&generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut where_clause = where_clause.cloned();
update_generics(&generics, &mut where_clause);
let attrs = parse_attributes(&attrs)?;
let nullable = attrs.nullable;
@ -229,7 +256,7 @@ fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input:
};
Ok(quote! {
impl #generics #krate::OpenapiType for #ident #generics
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
#where_clause
{
fn schema() -> #krate::OpenapiSchema

View file

@ -0,0 +1,165 @@
use crate::util::{remove_parens, CollectToResult, PathEndsWith};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{
parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, ItemTrait, LitStr, Meta, NestedMeta, PredicateType,
Result, TraitItem, WherePredicate
};
struct TraitItemAttrs {
openapi_only: bool,
openapi_bound: Vec<PredicateType>,
non_openapi_bound: Vec<PredicateType>,
other_attrs: Vec<Attribute>
}
impl TraitItemAttrs {
fn parse(attrs: Vec<Attribute>) -> Result<Self> {
let mut openapi_only = false;
let mut openapi_bound = Vec::new();
let mut non_openapi_bound = Vec::new();
let mut other = Vec::new();
for attr in attrs {
if attr.path.ends_with("openapi_only") {
openapi_only = true;
} else if attr.path.ends_with("openapi_bound") {
let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?;
let predicate = attr_arg.parse_with(WherePredicate::parse)?;
openapi_bound.push(match predicate {
WherePredicate::Type(ty) => ty,
_ => return Err(Error::new(predicate.span(), "Expected type bound"))
});
} else if attr.path.ends_with("non_openapi_bound") {
let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?;
let predicate = attr_arg.parse_with(WherePredicate::parse)?;
non_openapi_bound.push(match predicate {
WherePredicate::Type(ty) => ty,
_ => return Err(Error::new(predicate.span(), "Expected type bound"))
});
} else {
other.push(attr);
}
}
Ok(Self {
openapi_only,
openapi_bound,
non_openapi_bound,
other_attrs: other
})
}
}
pub(crate) fn expand_private_openapi_trait(mut attrs: AttributeArgs, tr8: ItemTrait) -> Result<TokenStream> {
let tr8_attrs = &tr8.attrs;
let vis = &tr8.vis;
let ident = &tr8.ident;
let generics = &tr8.generics;
let colon_token = &tr8.colon_token;
let supertraits = &tr8.supertraits;
if attrs.len() != 1 {
return Err(Error::new(
Span::call_site(),
"Expected one argument. Example: #[_private_openapi_trait(OpenapiTraitName)]"
));
}
let openapi_ident = match attrs.remove(0) {
NestedMeta::Meta(Meta::Path(path)) => path,
p => {
return Err(Error::new(
p.span(),
"Expected name of the Resource struct this method belongs to"
))
},
};
let orig_trait = {
let items = tr8
.items
.clone()
.into_iter()
.map(|item| {
Ok(match item {
TraitItem::Method(mut method) => {
let attrs = TraitItemAttrs::parse(method.attrs)?;
method.attrs = attrs.other_attrs;
for bound in attrs.non_openapi_bound {
method
.sig
.generics
.type_params_mut()
.filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string())
.for_each(|param| param.bounds.extend(bound.bounds.clone()));
}
if attrs.openapi_only {
None
} else {
Some(TraitItem::Method(method))
}
},
TraitItem::Type(mut ty) => {
let attrs = TraitItemAttrs::parse(ty.attrs)?;
ty.attrs = attrs.other_attrs;
Some(TraitItem::Type(ty))
},
item => Some(item)
})
})
.collect_to_result()?;
quote! {
#(#tr8_attrs)*
#vis trait #ident #generics #colon_token #supertraits {
#(#items)*
}
}
};
let openapi_trait = if !cfg!(feature = "openapi") {
None
} else {
let items = tr8
.items
.clone()
.into_iter()
.map(|item| {
Ok(match item {
TraitItem::Method(mut method) => {
let attrs = TraitItemAttrs::parse(method.attrs)?;
method.attrs = attrs.other_attrs;
for bound in attrs.openapi_bound {
method
.sig
.generics
.type_params_mut()
.filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string())
.for_each(|param| param.bounds.extend(bound.bounds.clone()));
}
TraitItem::Method(method)
},
TraitItem::Type(mut ty) => {
let attrs = TraitItemAttrs::parse(ty.attrs)?;
ty.attrs = attrs.other_attrs;
for bound in attrs.openapi_bound {
ty.bounds.extend(bound.bounds.clone());
}
TraitItem::Type(ty)
},
item => item
})
})
.collect_to_result()?;
Some(quote! {
#(#tr8_attrs)*
#vis trait #openapi_ident #generics #colon_token #supertraits {
#(#items)*
}
})
};
Ok(quote! {
#orig_trait
#openapi_trait
})
}

View file

@ -1,12 +1,15 @@
use crate::{method::Method, util::CollectToResult};
use crate::{
endpoint::endpoint_ident,
util::{CollectToResult, PathEndsWith}
};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::{iter, str::FromStr};
use std::iter;
use syn::{
parenthesized,
parse::{Parse, ParseStream},
punctuated::Punctuated,
DeriveInput, Error, Result, Token
DeriveInput, Result, Token
};
struct MethodList(Punctuated<Ident, Token![,]>);
@ -23,29 +26,22 @@ impl Parse for MethodList {
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
let krate = super::krate();
let ident = input.ident;
let name = ident.to_string();
let methods =
input
.attrs
.into_iter()
.filter(
|attr| {
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string())
} // TODO wtf
)
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
.flat_map(|list| match list {
Ok(iter) => Box::new(iter.map(|method| {
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
let ident = method.setup_ident(&name);
Ok(quote!(#ident(&mut route);))
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
Err(err) => Box::new(iter::once(Err(err)))
})
.collect_to_result()?;
let methods = input
.attrs
.into_iter()
.filter(|attr| attr.path.ends_with("resource"))
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
.flat_map(|list| match list {
Ok(iter) => Box::new(iter.map(|method| {
let ident = endpoint_ident(&method);
Ok(quote!(route.endpoint::<#ident>();))
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
Err(err) => Box::new(iter::once(Err(err)))
})
.collect_to_result()?;
Ok(quote! {
let non_openapi_impl = quote! {
impl #krate::Resource for #ident
{
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
@ -53,5 +49,22 @@ pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
#(#methods)*
}
}
};
let openapi_impl = if !cfg!(feature = "openapi") {
None
} else {
Some(quote! {
impl #krate::ResourceWithSchema for #ident
{
fn setup<D : #krate::DrawResourceRoutesWithSchema>(mut route : D)
{
#(#methods)*
}
}
})
};
Ok(quote! {
#non_openapi_impl
#openapi_impl
})
}

View file

@ -1,6 +1,6 @@
use proc_macro2::{Delimiter, TokenStream, TokenTree};
use std::iter;
use syn::Error;
use syn::{Error, Path};
pub trait CollectToResult {
type Item;
@ -30,6 +30,16 @@ where
}
}
pub(crate) trait PathEndsWith {
fn ends_with(&self, s: &str) -> bool;
}
impl PathEndsWith for Path {
fn ends_with(&self, s: &str) -> bool {
self.segments.last().map(|segment| segment.ident.to_string()).as_deref() == Some(s)
}
}
pub fn remove_parens(input: TokenStream) -> TokenStream {
let iter = input.into_iter().flat_map(|tt| {
if let TokenTree::Group(group) = &tt {

View file

@ -18,7 +18,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
fake = "2.2.2"
gotham = { version = "0.5.0", default-features = false }
gotham_derive = "0.5.0"
gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"] }
gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"], default-features = false }
log = "0.4.8"
pretty_env_logger = "0.4"
serde = "1.0.110"

View file

@ -136,7 +136,7 @@ struct AuthData {
exp: u64
}
#[read_all(AuthResource)]
#[read_all]
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
format!("{:?}", auth).into()
}

View file

@ -246,7 +246,7 @@ where
fn cors(&mut self, path: &str, method: Method);
}
fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
pub(crate) fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
let config = CorsConfig::try_borrow_from(&state);
// prepare the response

105
src/endpoint.rs Normal file
View file

@ -0,0 +1,105 @@
use crate::{RequestBody, ResourceResult};
use futures_util::future::BoxFuture;
use gotham::{
extractor::{PathExtractor, QueryStringExtractor},
hyper::{Body, Method},
state::State
};
use std::borrow::Cow;
// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands.
#[_private_openapi_trait(EndpointWithSchema)]
pub trait Endpoint {
/// The HTTP Verb of this endpoint.
fn http_method() -> Method;
/// The URI that this endpoint listens on in gotham's format.
fn uri() -> Cow<'static, str>;
/// The output type that provides the response.
type Output: ResourceResult + Send;
/// Returns `true` _iff_ the URI contains placeholders. `false` by default.
fn has_placeholders() -> bool {
false
}
/// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor]
/// if `has_placeholders()` returns `false`.
#[openapi_bound("Placeholders: crate::OpenapiType")]
type Placeholders: PathExtractor<Body> + Sync;
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
fn needs_params() -> bool {
false
}
/// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor]
/// if `needs_params()` returns `false`.
#[openapi_bound("Params: crate::OpenapiType")]
type Params: QueryStringExtractor<Body> + Sync;
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
fn needs_body() -> bool {
false
}
/// The type to parse the body into. Use `()` if `needs_body()` returns `false`.
type Body: RequestBody + Send;
/// Returns `true` if the request wants to know the auth status of the client. `false` by default.
fn wants_auth() -> bool {
false
}
/// Replace the automatically generated operation id with a custom one. Only relevant for the
/// OpenAPI Specification.
#[openapi_only]
fn operation_id() -> Option<String> {
None
}
/// The handler for this endpoint.
fn handle(
state: &mut State,
placeholders: Self::Placeholders,
params: Self::Params,
body: Option<Self::Body>
) -> BoxFuture<'static, Self::Output>;
}
#[cfg(feature = "openapi")]
impl<E: EndpointWithSchema> Endpoint for E {
fn http_method() -> Method {
E::http_method()
}
fn uri() -> Cow<'static, str> {
E::uri()
}
type Output = E::Output;
fn has_placeholders() -> bool {
E::has_placeholders()
}
type Placeholders = E::Placeholders;
fn needs_params() -> bool {
E::needs_params()
}
type Params = E::Params;
fn needs_body() -> bool {
E::needs_body()
}
type Body = E::Body;
fn wants_auth() -> bool {
E::wants_auth()
}
fn handle(
state: &mut State,
placeholders: Self::Placeholders,
params: Self::Params,
body: Option<Self::Body>
) -> BoxFuture<'static, Self::Output> {
E::handle(state, placeholders, params, body)
}
}

View file

@ -9,7 +9,7 @@
#![forbid(unsafe_code)]
/*!
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
create resources with assigned methods that aim to be a more convenient way of creating handlers
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
for requests.
# Features
@ -27,23 +27,23 @@ for requests.
This crate is just as safe as you'd expect from anything written in safe Rust - and
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
# Methods
# Endpoints
Assuming you assign `/foobar` to your resource, you can implement the following methods:
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
| ----------- | ------------------ | --------- | ----------- |
| read_all | | GET | /foobar |
| read | id | GET | /foobar/:id |
| search | query | GET | /foobar/search |
| create | body | POST | /foobar |
| change_all | body | PUT | /foobar |
| change | id, body | PUT | /foobar/:id |
| remove_all | | DELETE | /foobar |
| remove | id | DELETE | /foobar/:id |
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
| ------------- | ------------------ | --------- | -------------- |
| read_all | | GET | /foobar |
| read | id | GET | /foobar/:id |
| search | query | GET | /foobar/search |
| create | body | POST | /foobar |
| change_all | body | PUT | /foobar |
| change | id, body | PUT | /foobar/:id |
| remove_all | | DELETE | /foobar |
| remove | id | DELETE | /foobar/:id |
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
simple example could look like this:
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
simple example looks like this:
```rust,no_run
# #[macro_use] extern crate gotham_restful_derive;
@ -55,15 +55,15 @@ simple example could look like this:
#[resource(read)]
struct FooResource;
/// The return type of the foo read method.
/// The return type of the foo read endpoint.
#[derive(Serialize)]
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
struct Foo {
id: u64
}
/// The foo read method handler.
#[read(FooResource)]
/// The foo read endpoint.
#[read]
fn read(id: u64) -> Success<Foo> {
Foo { id }.into()
}
@ -76,17 +76,16 @@ fn read(id: u64) -> Success<Foo> {
# Arguments
Some methods require arguments. Those should be
* **id** Should be a deserializable json-primitive like `i64` or `String`.
Some endpoints require arguments. Those should be
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
* **query** Should be any deserializable object whose variables are json-primitives. It will
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
type needs to implement [`QueryStringExtractor`].
type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor).
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
have an async handler (that is, the function that the method macro is invoked on is declared
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
`Sync` there is unfortunately no more convenient way.
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
improve.
# Uploads and Downloads
@ -110,7 +109,7 @@ struct RawImage {
content_type: Mime
}
#[create(ImageResource)]
#[create]
fn create(body : RawImage) -> Raw<Vec<u8>> {
Raw::new(body.content, body.content_type)
}
@ -127,21 +126,23 @@ To make life easier for common use-cases, this create offers a few features that
when you implement your web server. The complete feature list is
- [`auth`](#authentication-feature) Advanced JWT middleware
- `chrono` openapi support for chrono types
- [`cors`](#cors-feature) CORS handling for all method handlers
- `full` enables all features except `without-openapi`
- [`cors`](#cors-feature) CORS handling for all endpoint handlers
- [`database`](#database-feature) diesel middleware support
- `errorlog` log errors returned from method handlers
- `errorlog` log errors returned from endpoint handlers
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
- `uuid` openapi support for uuid
- `without-openapi` (**default**) disables `openapi` support.
## Authentication Feature
In order to enable authentication support, enable the `auth` feature gate. This allows you to
register a middleware that can automatically check for the existence of an JWT authentication
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
None of this is currently supported by gotham's own JWT middleware.
A simple example that uses only a single secret could look like this:
A simple example that uses only a single secret looks like this:
```rust,no_run
# #[macro_use] extern crate gotham_restful_derive;
@ -167,7 +168,7 @@ struct AuthData {
exp: u64
}
#[read(SecretResource)]
#[read]
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
let intended_for = auth.ok()?.sub;
Ok(Secret { id, intended_for })
@ -194,7 +195,7 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a
configuration as a middleware.
A simple example that allows authentication from every origin (note that `*` always disallows
authentication), and every content type, could look like this:
authentication), and every content type, looks like this:
```rust,no_run
# #[macro_use] extern crate gotham_restful_derive;
@ -207,7 +208,7 @@ authentication), and every content type, could look like this:
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
#[read_all]
fn read_all() {
// your handler
}
@ -237,7 +238,7 @@ note however that due to the way gotham's diesel middleware implementation, it i
to run async code while holding a database connection. If you need to combine async and database,
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
A simple non-async example could look like this:
A simple non-async example looks like this:
```rust,no_run
# #[macro_use] extern crate diesel;
@ -267,7 +268,7 @@ struct Foo {
value: String
}
#[read_all(FooResource)]
#[read_all]
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
foo::table.load(conn)
}
@ -295,7 +296,7 @@ In order to automatically create an openapi specification, gotham-restful needs
all routes and the types returned. `serde` does a great job at serialization but doesn't give
enough type information, so all types used in the router need to implement `OpenapiType`. This
can be derived for almoust any type and there should be no need to implement it manually. A simple
example could look like this:
example looks like this:
```rust,no_run
# #[macro_use] extern crate gotham_restful_derive;
@ -313,7 +314,7 @@ struct Foo {
bar: String
}
#[read_all(FooResource)]
#[read_all]
fn read_all() -> Success<Foo> {
Foo { bar: "Hello World".to_owned() }.into()
}
@ -334,48 +335,37 @@ fn main() {
# }
```
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`
that will return the generated openapi specification. This allows you to easily write clients
in different languages without worying to exactly replicate your api in each of those languages.
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`.
It will return the generated openapi specification in JSON format. This allows you to easily write
clients in different languages without worying to exactly replicate your api in each of those
languages.
However, as of right now there is one caveat. If you wrote code before enabling the openapi feature,
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
In other words, put
However, please note that by default, the `without-openapi` feature of this crate is enabled.
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
on some of the types in [`Endpoint`] and related traits. This means that some code might only
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
it is strongly recommended to pass both features through and conditionally enable the openapi
code, like this:
```toml
[features]
openapi = ["gotham-restful/openapi"]
```
into your libraries `Cargo.toml` and use the following for all types used with handlers:
```
# #[cfg(feature = "openapi")]
# mod openapi_feature_enabled {
# use gotham_restful::OpenapiType;
```rust
# #[macro_use] extern crate gotham_restful;
# use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
struct Foo;
# }
```
# Examples
There is a lack of good examples, but there is currently a collection of code in the [example]
directory, that might help you. Any help writing more examples is highly appreciated.
This readme and the crate documentation contain some of example. In addition to that, there is
a collection of code in the [example] directory that might help you. Any help writing more
examples is highly appreciated.
[diesel]: https://diesel.rs/
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
[gotham]: https://gotham.rs/
[serde_json]: https://github.com/serde-rs/json#serde-json----
[`CorsRoute`]: trait.CorsRoute.html
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
[`RequestBody`]: trait.RequestBody.html
[`State`]: ../gotham/state/struct.State.html
*/
#[cfg(all(feature = "openapi", feature = "without-openapi"))]
@ -390,6 +380,8 @@ extern crate self as gotham_restful;
#[macro_use]
extern crate gotham_derive;
#[macro_use]
extern crate gotham_restful_derive;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde;
@ -409,7 +401,9 @@ pub use gotham_restful_derive::*;
/// Not public API
#[doc(hidden)]
pub mod export {
pub use futures_util::future::FutureExt;
pub use crate::routing::PathExtractor as IdPlaceholder;
pub use futures_util::future::{BoxFuture, FutureExt};
pub use serde_json;
@ -441,11 +435,10 @@ pub use openapi::{
types::{OpenapiSchema, OpenapiType}
};
mod resource;
pub use resource::{
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll,
ResourceRemove, ResourceRemoveAll, ResourceSearch
};
mod endpoint;
pub use endpoint::Endpoint;
#[cfg(feature = "openapi")]
pub use endpoint::EndpointWithSchema;
mod response;
pub use response::Response;
@ -457,9 +450,21 @@ pub use result::{
};
mod routing;
#[cfg(feature = "openapi")]
pub use routing::WithOpenapi;
pub use routing::{DrawResourceRoutes, DrawResources};
#[cfg(feature = "openapi")]
pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi};
mod types;
pub use types::*;
/// This trait must be implemented for every resource. It allows you to register the different
/// endpoints that can be handled by this resource to be registered with the underlying router.
///
/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`.
#[_private_openapi_trait(ResourceWithSchema)]
pub trait Resource {
/// Register all methods handled by this resource with the underlying router.
#[openapi_bound("D: crate::DrawResourceRoutesWithSchema")]
#[non_openapi_bound("D: crate::DrawResourceRoutes")]
fn setup<D>(route: D);
}

View file

@ -1,5 +1,5 @@
use super::SECURITY_NAME;
use crate::{resource::*, result::*, OpenapiSchema, RequestBody};
use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody};
use indexmap::IndexMap;
use mime::Mime;
use openapiv3::{
@ -8,31 +8,40 @@ use openapiv3::{
};
#[derive(Default)]
struct OperationParams<'a> {
path_params: Vec<(&'a str, ReferenceOr<Schema>)>,
struct OperationParams {
path_params: Option<OpenapiSchema>,
query_params: Option<OpenapiSchema>
}
impl<'a> OperationParams<'a> {
fn add_path_params(&self, params: &mut Vec<ReferenceOr<Parameter>>) {
for param in &self.path_params {
impl OperationParams {
fn add_path_params(path_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
let path_params = match path_params {
Some(pp) => pp.schema,
None => return
};
let path_params = match path_params {
SchemaKind::Type(Type::Object(ty)) => ty,
_ => panic!("Path Parameters needs to be a plain struct")
};
for (name, schema) in path_params.properties {
let required = path_params.required.contains(&name);
params.push(Item(Parameter::Path {
parameter_data: ParameterData {
name: (*param).0.to_string(),
name,
description: None,
required: true,
required,
deprecated: None,
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
format: ParameterSchemaOrContent::Schema(schema.unbox()),
example: None,
examples: IndexMap::new()
},
style: Default::default()
}));
}))
}
}
fn add_query_params(self, params: &mut Vec<ReferenceOr<Parameter>>) {
let query_params = match self.query_params {
fn add_query_params(query_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
let query_params = match query_params {
Some(qp) => qp.schema,
None => return
};
@ -61,51 +70,48 @@ impl<'a> OperationParams<'a> {
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
self.add_path_params(&mut params);
self.add_query_params(&mut params);
Self::add_path_params(self.path_params, &mut params);
Self::add_query_params(self.query_params, &mut params);
params
}
}
pub struct OperationDescription<'a> {
pub struct OperationDescription {
operation_id: Option<String>,
default_status: crate::StatusCode,
accepted_types: Option<Vec<Mime>>,
schema: ReferenceOr<Schema>,
params: OperationParams<'a>,
params: OperationParams,
body_schema: Option<ReferenceOr<Schema>>,
supported_types: Option<Vec<Mime>>,
requires_auth: bool
}
impl<'a> OperationDescription<'a> {
pub fn new<Handler: ResourceMethod>(schema: ReferenceOr<Schema>) -> Self {
impl OperationDescription {
pub fn new<E: EndpointWithSchema>(schema: ReferenceOr<Schema>) -> Self {
Self {
operation_id: Handler::operation_id(),
default_status: Handler::Res::default_status(),
accepted_types: Handler::Res::accepted_types(),
operation_id: E::operation_id(),
default_status: E::Output::default_status(),
accepted_types: E::Output::accepted_types(),
schema,
params: Default::default(),
body_schema: None,
supported_types: None,
requires_auth: Handler::wants_auth()
requires_auth: E::wants_auth()
}
}
pub fn add_path_param(mut self, name: &'a str, schema: ReferenceOr<Schema>) -> Self {
self.params.path_params.push((name, schema));
self
pub fn set_path_params(&mut self, params: OpenapiSchema) {
self.params.path_params = Some(params);
}
pub fn with_query_params(mut self, params: OpenapiSchema) -> Self {
pub fn set_query_params(&mut self, params: OpenapiSchema) {
self.params.query_params = Some(params);
self
}
pub fn with_body<Body: RequestBody>(mut self, schema: ReferenceOr<Schema>) -> Self {
pub fn set_body<Body: RequestBody>(&mut self, schema: ReferenceOr<Schema>) {
self.body_schema = Some(schema);
self.supported_types = Body::supported_types();
self
}
fn schema_to_content(types: Vec<Mime>, schema: ReferenceOr<Schema>) -> IndexMap<String, MediaType> {

View file

@ -1,6 +1,8 @@
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
use crate::{resource::*, routing::*, OpenapiType};
use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*};
use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema};
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use std::panic::RefUnwindSafe;
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
@ -51,150 +53,62 @@ macro_rules! implOpenapiRouter {
}
}
impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>>
impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>>
where
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P: RefUnwindSafe + Send + Sync + 'static
{
fn resource<R: Resource>(&mut self, path: &str) {
fn resource<R: ResourceWithSchema>(&mut self, path: &str) {
R::setup((self, path));
}
}
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
where
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P: RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler: ResourceReadAll>(&mut self) {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
let schema = (self.0).openapi_builder.add_schema::<E::Output>();
let mut descr = OperationDescription::new::<E>(schema);
if E::has_placeholders() {
descr.set_path_params(E::Placeholders::schema());
}
if E::needs_params() {
descr.set_query_params(E::Params::schema());
}
if E::needs_body() {
let body_schema = (self.0).openapi_builder.add_schema::<E::Body>();
descr.set_body::<E::Body>(body_schema);
}
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
static URI_PLACEHOLDER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(^|/):(?P<name>[^/]+)(/|$)"#).unwrap());
let uri: &str = &E::uri();
let uri =
URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| format!("{{{}}}", &captures["name"]));
let path = if uri.is_empty() {
format!("{}/{}", self.0.scope.unwrap_or_default(), self.1)
} else {
format!("{}/{}/{}", self.0.scope.unwrap_or_default(), self.1, uri)
};
let op = descr.into_operation();
let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
match E::http_method() {
Method::GET => item.get = Some(op),
Method::PUT => item.put = Some(op),
Method::POST => item.post = Some(op),
Method::DELETE => item.delete = Some(op),
Method::OPTIONS => item.options = Some(op),
Method::HEAD => item.head = Some(op),
Method::PATCH => item.patch = Some(op),
Method::TRACE => item.trace = Some(op),
method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method)
};
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).read_all::<Handler>()
}
fn read<Handler: ResourceRead>(&mut self) {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(
OperationDescription::new::<Handler>(schema)
.add_path_param("id", id_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).read::<Handler>()
}
fn search<Handler: ResourceSearch>(&mut self) {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(
OperationDescription::new::<Handler>(schema)
.with_query_params(Handler::Query::schema())
.into_operation()
);
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).search::<Handler>()
}
fn create<Handler: ResourceCreate>(&mut self)
where
Handler::Res: 'static,
Handler::Body: 'static
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.post = Some(
OperationDescription::new::<Handler>(schema)
.with_body::<Handler::Body>(body_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).create::<Handler>()
}
fn change_all<Handler: ResourceChangeAll>(&mut self)
where
Handler::Res: 'static,
Handler::Body: 'static
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.put = Some(
OperationDescription::new::<Handler>(schema)
.with_body::<Handler::Body>(body_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).change_all::<Handler>()
}
fn change<Handler: ResourceChange>(&mut self)
where
Handler::Res: 'static,
Handler::Body: 'static
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.put = Some(
OperationDescription::new::<Handler>(schema)
.add_path_param("id", id_schema)
.with_body::<Handler::Body>(body_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).change::<Handler>()
}
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).remove_all::<Handler>()
}
fn remove<Handler: ResourceRemove>(&mut self) {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.delete = Some(
OperationDescription::new::<Handler>(schema)
.add_path_param("id", id_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).remove::<Handler>()
(&mut *(self.0).router, self.1).endpoint::<E>()
}
}
};

View file

@ -1,5 +1,6 @@
#[cfg(feature = "chrono")]
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor};
use indexmap::IndexMap;
use openapiv3::{
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType,
@ -86,6 +87,20 @@ impl OpenapiType for () {
}
}
impl OpenapiType for NoopPathExtractor {
fn schema() -> OpenapiSchema {
warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want.");
<()>::schema()
}
}
impl OpenapiType for NoopQueryStringExtractor {
fn schema() -> OpenapiSchema {
warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want.");
<()>::schema()
}
}
impl OpenapiType for bool {
fn schema() -> OpenapiSchema {
OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))

View file

@ -1,99 +0,0 @@
use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType};
use gotham::{extractor::QueryStringExtractor, hyper::Body, state::State};
use std::{future::Future, pin::Pin};
/// This trait must be implemented for every resource. It allows you to register the different
/// methods that can be handled by this resource to be registered with the underlying router.
///
/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`.
pub trait Resource {
/// Register all methods handled by this resource with the underlying router.
fn setup<D: DrawResourceRoutes>(route: D);
}
/// A common trait for every resource method. It defines the return type as well as some general
/// information about a resource method.
///
/// It is not recommended to implement this yourself. Rather, just write your handler method and
/// annotate it with `#[<method>(YourResource)]`, where `<method>` is one of the supported
/// resource methods.
pub trait ResourceMethod {
type Res: ResourceResult + Send + 'static;
#[cfg(feature = "openapi")]
fn operation_id() -> Option<String> {
None
}
fn wants_auth() -> bool {
false
}
}
/// The read_all [ResourceMethod].
pub trait ResourceReadAll: ResourceMethod {
/// Handle a GET request on the Resource root.
fn read_all(state: State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The read [ResourceMethod].
pub trait ResourceRead: ResourceMethod {
/// The ID type to be parsed from the request path.
type ID: ResourceID + 'static;
/// Handle a GET request on the Resource with an id.
fn read(state: State, id: Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The search [ResourceMethod].
pub trait ResourceSearch: ResourceMethod {
/// The Query type to be parsed from the request parameters.
type Query: ResourceType + QueryStringExtractor<Body> + Sync;
/// Handle a GET request on the Resource with additional search parameters.
fn search(state: State, query: Self::Query) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The create [ResourceMethod].
pub trait ResourceCreate: ResourceMethod {
/// The Body type to be parsed from the request body.
type Body: RequestBody;
/// Handle a POST request on the Resource root.
fn create(state: State, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The change_all [ResourceMethod].
pub trait ResourceChangeAll: ResourceMethod {
/// The Body type to be parsed from the request body.
type Body: RequestBody;
/// Handle a PUT request on the Resource root.
fn change_all(state: State, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The change [ResourceMethod].
pub trait ResourceChange: ResourceMethod {
/// The Body type to be parsed from the request body.
type Body: RequestBody;
/// The ID type to be parsed from the request path.
type ID: ResourceID + 'static;
/// Handle a PUT request on the Resource with an id.
fn change(state: State, id: Self::ID, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The remove_all [ResourceMethod].
pub trait ResourceRemoveAll: ResourceMethod {
/// Handle a DELETE request on the Resource root.
fn remove_all(state: State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}
/// The remove [ResourceMethod].
pub trait ResourceRemove: ResourceMethod {
/// The ID type to be parsed from the request path.
type ID: ResourceID + 'static;
/// Handle a DELETE request on the Resource with an id.
fn remove(state: State, id: Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
}

View file

@ -33,7 +33,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
# #[derive(Clone, Deserialize)]
# struct MyAuthData { exp : u64 }
#
#[read_all(MyResource)]
#[read_all]
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
let auth_data = match auth {
AuthStatus::Authenticated(data) => data,
@ -102,7 +102,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
# #[derive(Clone, Deserialize)]
# struct MyAuthData { exp : u64 }
#
#[read_all(MyResource)]
#[read_all]
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
let auth_data = match auth {
AuthStatus::Authenticated(data) => data,

View file

@ -21,8 +21,8 @@ the function attributes:
# #[resource(read_all)]
# struct MyResource;
#
#[read_all(MyResource)]
fn read_all(_state: &mut State) {
#[read_all]
fn read_all() {
// do something
}
# }

View file

@ -25,7 +25,7 @@ example that simply returns its body:
#[resource(create)]
struct ImageResource;
#[create(ImageResource)]
#[create]
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
body
}

View file

@ -34,8 +34,8 @@ struct MyResponse {
message: &'static str
}
#[read_all(MyResource)]
fn read_all(_state: &mut State) -> Success<MyResponse> {
#[read_all]
fn read_all() -> Success<MyResponse> {
let res = MyResponse { message: "I'm always happy" };
res.into()
}

View file

@ -3,37 +3,33 @@ use crate::openapi::{
builder::{OpenapiBuilder, OpenapiInfo},
router::OpenapiRouter
};
#[cfg(feature = "cors")]
use crate::CorsRoute;
use crate::{
resource::{
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceRead, ResourceReadAll, ResourceRemove,
ResourceRemoveAll, ResourceSearch
},
result::{ResourceError, ResourceResult},
RequestBody, Response, StatusCode
Endpoint, FromBody, Resource, Response, StatusCode
};
use futures_util::{future, future::FutureExt};
use gotham::{
handler::{HandlerError, HandlerFuture},
handler::HandlerError,
helpers::http::response::{create_empty_response, create_response},
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method},
pipeline::chain::PipelineHandleChain,
router::{
builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher, RouterBuilder, ScopeBuilder},
builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder},
non_match::RouteNonMatch,
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
route::matcher::{
AcceptHeaderRouteMatcher, AccessControlRequestMethodMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher
}
},
state::{FromState, State}
};
use mime::{Mime, APPLICATION_JSON};
use std::{future::Future, panic::RefUnwindSafe, pin::Pin};
use std::panic::RefUnwindSafe;
/// Allow us to extract an id from a path.
#[derive(Deserialize, StateData, StaticResponseExtender)]
struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
id: ID
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
pub id: ID
}
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
@ -48,37 +44,20 @@ pub trait WithOpenapi<D> {
/// This trait adds the `resource` method to gotham's routing. It allows you to register
/// any RESTful [Resource] with a path.
#[_private_openapi_trait(DrawResourcesWithSchema)]
pub trait DrawResources {
fn resource<R: Resource>(&mut self, path: &str);
#[openapi_bound("R: crate::ResourceWithSchema")]
#[non_openapi_bound("R: crate::Resource")]
fn resource<R>(&mut self, path: &str);
}
/// This trait allows to draw routes within an resource. Use this only inside the
/// [Resource::setup] method.
#[_private_openapi_trait(DrawResourceRoutesWithSchema)]
pub trait DrawResourceRoutes {
fn read_all<Handler: ResourceReadAll>(&mut self);
fn read<Handler: ResourceRead>(&mut self);
fn search<Handler: ResourceSearch>(&mut self);
fn create<Handler: ResourceCreate>(&mut self)
where
Handler::Res: 'static,
Handler::Body: 'static;
fn change_all<Handler: ResourceChangeAll>(&mut self)
where
Handler::Res: 'static,
Handler::Body: 'static;
fn change<Handler: ResourceChange>(&mut self)
where
Handler::Res: 'static,
Handler::Body: 'static;
fn remove_all<Handler: ResourceRemoveAll>(&mut self);
fn remove<Handler: ResourceRemove>(&mut self);
#[openapi_bound("E: crate::EndpointWithSchema")]
#[non_openapi_bound("E: crate::Endpoint")]
fn endpoint<E: 'static>(&mut self);
}
fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
@ -108,149 +87,42 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body>
r
}
async fn to_handler_future<F, R>(
state: State,
get_result: F
) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)>
where
F: FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
R: ResourceResult
{
let (state, res) = get_result(state).await;
let res = res.into_response().await;
match res {
Ok(res) => {
let r = response_from(res, &state);
Ok((state, r))
},
Err(e) => Err((state, e.into()))
}
}
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError> {
trace!("entering endpoint_handler");
let placeholders = E::Placeholders::take_from(state);
let params = E::Params::take_from(state);
async fn body_to_res<B, F, R>(
mut state: State,
get_result: F
) -> (State, Result<gotham::hyper::Response<Body>, HandlerError>)
where
B: RequestBody,
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
R: ResourceResult
{
let body = to_bytes(Body::take_from(&mut state)).await;
let body = match E::needs_body() {
true => {
let body = to_bytes(Body::take_from(state)).await?;
let body = match body {
Ok(body) => body,
Err(e) => return (state, Err(e.into()))
};
let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) {
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
None => {
debug!("Missing Content-Type: Returning 415 Response");
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
return Ok(res);
}
};
let content_type: Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
None => {
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
return (state, Ok(res));
}
};
let res = {
let body = match B::from_body(body, content_type) {
Ok(body) => body,
Err(e) => {
let error: ResourceError = e.into();
let res = match serde_json::to_string(&error) {
Ok(json) => {
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
Ok(res)
},
Err(e) => Err(e.into())
};
return (state, res);
match E::Body::from_body(body, content_type) {
Ok(body) => Some(body),
Err(e) => {
debug!("Invalid Body: Returning 400 Response");
let error: ResourceError = e.into();
let json = serde_json::to_string(&error)?;
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
return Ok(res);
}
}
};
get_result(state, body)
};
let (state, res) = res.await;
let res = res.into_response().await;
let res = match res {
Ok(res) => {
let r = response_from(res, &state);
Ok(r)
},
Err(e) => Err(e.into())
false => None
};
(state, res)
}
fn handle_with_body<B, F, R>(state: State, get_result: F) -> Pin<Box<HandlerFuture>>
where
B: RequestBody + 'static,
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
R: ResourceResult + Send + 'static
{
body_to_res(state, get_result)
.then(|(state, res)| match res {
Ok(ok) => future::ok((state, ok)),
Err(err) => future::err((state, err))
})
.boxed()
}
fn read_all_handler<Handler: ResourceReadAll>(state: State) -> Pin<Box<HandlerFuture>> {
to_handler_future(state, |state| Handler::read_all(state)).boxed()
}
fn read_handler<Handler: ResourceRead>(state: State) -> Pin<Box<HandlerFuture>> {
let id = {
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
path.id.clone()
};
to_handler_future(state, |state| Handler::read(state, id)).boxed()
}
fn search_handler<Handler: ResourceSearch>(mut state: State) -> Pin<Box<HandlerFuture>> {
let query = Handler::Query::take_from(&mut state);
to_handler_future(state, |state| Handler::search(state, query)).boxed()
}
fn create_handler<Handler: ResourceCreate>(state: State) -> Pin<Box<HandlerFuture>>
where
Handler::Res: 'static,
Handler::Body: 'static
{
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
}
fn change_all_handler<Handler: ResourceChangeAll>(state: State) -> Pin<Box<HandlerFuture>>
where
Handler::Res: 'static,
Handler::Body: 'static
{
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body))
}
fn change_handler<Handler: ResourceChange>(state: State) -> Pin<Box<HandlerFuture>>
where
Handler::Res: 'static,
Handler::Body: 'static
{
let id = {
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
path.id.clone()
};
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body))
}
fn remove_all_handler<Handler: ResourceRemoveAll>(state: State) -> Pin<Box<HandlerFuture>> {
to_handler_future(state, |state| Handler::remove_all(state)).boxed()
}
fn remove_handler<Handler: ResourceRemove>(state: State) -> Pin<Box<HandlerFuture>> {
let id = {
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
path.id.clone()
};
to_handler_future(state, |state| Handler::remove(state, id)).boxed()
let out = E::handle(state, placeholders, params, body).await;
let res = out.into_response().await?;
debug!("Returning response {:?}", res);
Ok(response_from(res, state))
}
#[derive(Clone)]
@ -267,8 +139,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader {
}
}
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
fn from(types: Option<Vec<Mime>>) -> Self {
impl MaybeMatchAcceptHeader {
fn new(types: Option<Vec<Mime>>) -> Self {
let types = match types {
Some(types) if types.is_empty() => None,
types => types
@ -279,6 +151,12 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
}
}
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
fn from(types: Option<Vec<Mime>>) -> Self {
Self::new(types)
}
}
#[derive(Clone)]
struct MaybeMatchContentTypeHeader {
matcher: Option<ContentTypeHeaderRouteMatcher>
@ -293,14 +171,20 @@ impl RouteMatcher for MaybeMatchContentTypeHeader {
}
}
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
fn from(types: Option<Vec<Mime>>) -> Self {
impl MaybeMatchContentTypeHeader {
fn new(types: Option<Vec<Mime>>) -> Self {
Self {
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
}
}
}
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
fn from(types: Option<Vec<Mime>>) -> Self {
Self::new(types)
}
}
macro_rules! implDrawResourceRoutes {
($implType:ident) => {
#[cfg(feature = "openapi")]
@ -332,108 +216,30 @@ macro_rules! implDrawResourceRoutes {
}
}
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
where
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P: RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler: ResourceReadAll>(&mut self) {
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
self.0
.get(&self.1)
.extend_route_matcher(matcher)
.to(|state| read_all_handler::<Handler>(state));
}
fn endpoint<E: Endpoint + 'static>(&mut self) {
let uri = format!("{}/{}", self.1, E::uri());
debug!("Registering endpoint for {}", uri);
self.0.associate(&uri, |assoc| {
assoc
.request(vec![E::http_method()])
.add_route_matcher(MaybeMatchAcceptHeader::new(E::Output::accepted_types()))
.with_path_extractor::<E::Placeholders>()
.with_query_string_extractor::<E::Params>()
.to_async_borrowing(endpoint_handler::<E>);
fn read<Handler: ResourceRead>(&mut self) {
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
self.0
.get(&format!("{}/:id", self.1))
.extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<Handler::ID>>()
.to(|state| read_handler::<Handler>(state));
}
fn search<Handler: ResourceSearch>(&mut self) {
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
self.0
.get(&format!("{}/search", self.1))
.extend_route_matcher(matcher)
.with_query_string_extractor::<Handler::Query>()
.to(|state| search_handler::<Handler>(state));
}
fn create<Handler: ResourceCreate>(&mut self)
where
Handler::Res: Send + 'static,
Handler::Body: 'static
{
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
self.0
.post(&self.1)
.extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher)
.to(|state| create_handler::<Handler>(state));
#[cfg(feature = "cors")]
self.0.cors(&self.1, Method::POST);
}
fn change_all<Handler: ResourceChangeAll>(&mut self)
where
Handler::Res: Send + 'static,
Handler::Body: 'static
{
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
self.0
.put(&self.1)
.extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher)
.to(|state| change_all_handler::<Handler>(state));
#[cfg(feature = "cors")]
self.0.cors(&self.1, Method::PUT);
}
fn change<Handler: ResourceChange>(&mut self)
where
Handler::Res: Send + 'static,
Handler::Body: 'static
{
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
let path = format!("{}/:id", self.1);
self.0
.put(&path)
.extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher)
.with_path_extractor::<PathExtractor<Handler::ID>>()
.to(|state| change_handler::<Handler>(state));
#[cfg(feature = "cors")]
self.0.cors(&path, Method::PUT);
}
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
self.0
.delete(&self.1)
.extend_route_matcher(matcher)
.to(|state| remove_all_handler::<Handler>(state));
#[cfg(feature = "cors")]
self.0.cors(&self.1, Method::DELETE);
}
fn remove<Handler: ResourceRemove>(&mut self) {
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let path = format!("{}/:id", self.1);
self.0
.delete(&path)
.extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<Handler::ID>>()
.to(|state| remove_handler::<Handler>(state));
#[cfg(feature = "cors")]
self.0.cors(&path, Method::POST);
#[cfg(feature = "cors")]
if E::http_method() != Method::GET {
assoc
.options()
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
.to(crate::cors::cors_preflight_handler);
}
});
}
}
};

View file

@ -4,7 +4,7 @@ use crate::OpenapiType;
use gotham::hyper::body::Bytes;
use mime::{Mime, APPLICATION_JSON};
use serde::{de::DeserializeOwned, Serialize};
use std::{error::Error, panic::RefUnwindSafe};
use std::error::Error;
#[cfg(not(feature = "openapi"))]
pub trait ResourceType {}
@ -98,12 +98,3 @@ impl<T: ResourceType + DeserializeOwned> RequestBody for T {
Some(vec![APPLICATION_JSON])
}
}
/// A type than can be used as a parameter to a resource method. Implemented for every type
/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of
/// type [OpenapiType].
///
/// [OpenapiType]: trait.OpenapiType.html
pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {}
impl<T: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T {}

View file

@ -30,55 +30,57 @@ struct FooSearch {
}
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
#[read_all(FooResource)]
#[read_all]
async fn read_all() -> Raw<&'static [u8]> {
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
}
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
#[read(FooResource)]
#[read]
async fn read(_id: u64) -> Raw<&'static [u8]> {
Raw::new(READ_RESPONSE, TEXT_PLAIN)
}
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
#[search(FooResource)]
#[search]
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
}
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
#[create(FooResource)]
#[create]
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
}
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
#[change_all(FooResource)]
#[change_all]
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
}
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
#[change(FooResource)]
#[change]
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
}
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
#[remove_all(FooResource)]
#[remove_all]
async fn remove_all() -> Raw<&'static [u8]> {
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
}
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
#[remove(FooResource)]
#[remove]
async fn remove(_id: u64) -> Raw<&'static [u8]> {
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
}
#[test]
fn async_methods() {
let _ = pretty_env_logger::try_init_timed();
let server = TestServer::new(build_simple_router(|router| {
router.resource::<FooResource>("foo");
}))

View file

@ -16,10 +16,10 @@ use mime::TEXT_PLAIN;
#[resource(read_all, change_all)]
struct FooResource;
#[read_all(FooResource)]
#[read_all]
fn read_all() {}
#[change_all(FooResource)]
#[change_all]
fn change_all(_body: Raw<Vec<u8>>) {}
fn test_server(cfg: CorsConfig) -> TestServer {

View file

@ -15,7 +15,7 @@ struct Foo {
content_type: Mime
}
#[create(FooResource)]
#[create]
fn create(body: Foo) -> Raw<Vec<u8>> {
Raw::new(body.content, body.content_type)
}

View file

@ -22,23 +22,23 @@ use util::{test_get_response, test_openapi_response};
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
#[derive(Resource)]
#[resource(read, change)]
#[resource(get_image, set_image)]
struct ImageResource;
#[derive(FromBody, RequestBody)]
#[supported_types(IMAGE_PNG)]
struct Image(Vec<u8>);
#[read(ImageResource, operation_id = "getImage")]
#[read(operation_id = "getImage")]
fn get_image(_id: u64) -> Raw<&'static [u8]> {
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
}
#[change(ImageResource, operation_id = "setImage")]
#[change(operation_id = "setImage")]
fn set_image(_id: u64, _image: Image) {}
#[derive(Resource)]
#[resource(read, search)]
#[resource(read_secret, search_secret)]
struct SecretResource;
#[derive(Deserialize, Clone)]
@ -67,13 +67,13 @@ struct SecretQuery {
minute: Option<u16>
}
#[read(SecretResource)]
#[read]
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
auth.ok()?;
Ok(Secret { code: 4.2 })
}
#[search(SecretResource)]
#[search]
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
auth.ok()?;
Ok(Secrets {
@ -82,7 +82,7 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets>
}
#[test]
fn openapi_supports_scope() {
fn openapi_specification() {
let info = OpenapiInfo {
title: "This is just a test".to_owned(),
version: "1.2.3".to_owned(),

View file

@ -15,7 +15,7 @@ const RESPONSE: &[u8] = b"This is the only valid response.";
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
#[read_all]
fn read_all() -> Raw<&'static [u8]> {
Raw::new(RESPONSE, TEXT_PLAIN)
}

View file

@ -30,55 +30,57 @@ struct FooSearch {
}
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
#[read_all(FooResource)]
#[read_all]
fn read_all() -> Raw<&'static [u8]> {
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
}
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
#[read(FooResource)]
#[read]
fn read(_id: u64) -> Raw<&'static [u8]> {
Raw::new(READ_RESPONSE, TEXT_PLAIN)
}
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
#[search(FooResource)]
#[search]
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
}
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
#[create(FooResource)]
#[create]
fn create(_body: FooBody) -> Raw<&'static [u8]> {
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
}
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
#[change_all(FooResource)]
#[change_all]
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
}
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
#[change(FooResource)]
#[change]
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
}
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
#[remove_all(FooResource)]
#[remove_all]
fn remove_all() -> Raw<&'static [u8]> {
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
}
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
#[remove(FooResource)]
#[remove]
fn remove(_id: u64) -> Raw<&'static [u8]> {
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
}
#[test]
fn sync_methods() {
let _ = pretty_env_logger::try_init_timed();
let server = TestServer::new(build_simple_router(|router| {
router.resource::<FooResource>("foo");
}))

View file

@ -6,23 +6,12 @@ fn trybuild_ui() {
let t = TestCases::new();
// always enabled
t.compile_fail("tests/ui/from_body_enum.rs");
t.compile_fail("tests/ui/method_async_state.rs");
t.compile_fail("tests/ui/method_for_unknown_resource.rs");
t.compile_fail("tests/ui/method_no_resource.rs");
t.compile_fail("tests/ui/method_self.rs");
t.compile_fail("tests/ui/method_too_few_args.rs");
t.compile_fail("tests/ui/method_too_many_args.rs");
t.compile_fail("tests/ui/method_unsafe.rs");
t.compile_fail("tests/ui/resource_unknown_method.rs");
t.compile_fail("tests/ui/endpoint/*.rs");
t.compile_fail("tests/ui/from_body/*.rs");
t.compile_fail("tests/ui/resource/*.rs");
// require the openapi feature
if cfg!(feature = "openapi") {
t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs");
t.compile_fail("tests/ui/openapi_type_nullable_non_bool.rs");
t.compile_fail("tests/ui/openapi_type_rename_non_string.rs");
t.compile_fail("tests/ui/openapi_type_tuple_struct.rs");
t.compile_fail("tests/ui/openapi_type_union.rs");
t.compile_fail("tests/ui/openapi_type_unknown_key.rs");
t.compile_fail("tests/ui/openapi_type/*.rs");
}
}

View file

@ -0,0 +1,12 @@
#[macro_use]
extern crate gotham_restful;
use gotham_restful::State;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all]
async fn read_all(state: &State) {}
fn main() {}

View file

@ -0,0 +1,5 @@
error: Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`
--> $DIR/async_state.rs:10:19
|
10 | async fn read_all(state: &State) {}
| ^^^^^

View file

@ -1,14 +1,11 @@
#[macro_use] extern crate gotham_restful;
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
fn read_all(self)
{
}
fn read_all() {}
fn main()
{
}
fn main() {}

View file

@ -0,0 +1,11 @@
error: Invalid attribute syntax
--> $DIR/invalid_attribute.rs:8:12
|
8 | #[read_all(FooResource)]
| ^^^^^^^^^^^
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
--> $DIR/invalid_attribute.rs:5:12
|
5 | #[resource(read_all)]
| ^^^^^^^^ not found in this scope

View file

@ -1,14 +1,11 @@
#[macro_use] extern crate gotham_restful;
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all]
fn read_all()
{
}
fn read_all(self) {}
fn main()
{
}
fn main() {}

View file

@ -0,0 +1,19 @@
error: Didn't expect self parameter
--> $DIR/self.rs:9:13
|
9 | fn read_all(self) {}
| ^^^^
error: `self` parameter is only allowed in associated functions
--> $DIR/self.rs:9:13
|
9 | fn read_all(self) {}
| ^^^^ not semantically valid as function parameter
|
= note: associated functions are those in `impl` or `trait` definitions
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
--> $DIR/self.rs:5:12
|
5 | #[resource(read_all)]
| ^^^^^^^^ not found in this scope

View file

@ -0,0 +1,11 @@
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read)]
struct FooResource;
#[read]
fn read() {}
fn main() {}

View file

@ -0,0 +1,11 @@
error: Too few arguments
--> $DIR/too_few_args.rs:9:4
|
9 | fn read() {}
| ^^^^
error[E0412]: cannot find type `read___gotham_restful_endpoint` in this scope
--> $DIR/too_few_args.rs:5:12
|
5 | #[resource(read)]
| ^^^^ not found in this scope

View file

@ -0,0 +1,11 @@
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all]
fn read_all(_id: u64) {}
fn main() {}

View file

@ -0,0 +1,11 @@
error: Too many arguments
--> $DIR/too_many_args.rs:9:4
|
9 | fn read_all(_id: u64) {}
| ^^^^^^^^
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
--> $DIR/too_many_args.rs:5:12
|
5 | #[resource(read_all)]
| ^^^^^^^^ not found in this scope

View file

@ -0,0 +1,11 @@
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all(pineapple = "on pizza")]
fn read_all() {}
fn main() {}

View file

@ -0,0 +1,11 @@
error: Unknown attribute
--> $DIR/unknown_attribute.rs:8:12
|
8 | #[read_all(pineapple = "on pizza")]
| ^^^^^^^^^
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
--> $DIR/unknown_attribute.rs:5:12
|
5 | #[resource(read_all)]
| ^^^^^^^^ not found in this scope

View file

@ -0,0 +1,11 @@
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all]
unsafe fn read_all() {}
fn main() {}

View file

@ -0,0 +1,11 @@
error: Endpoint handler methods must not be unsafe
--> $DIR/unsafe.rs:9:1
|
9 | unsafe fn read_all() {}
| ^^^^^^
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
--> $DIR/unsafe.rs:5:12
|
5 | #[resource(read_all)]
| ^^^^^^^^ not found in this scope

View file

@ -0,0 +1,10 @@
#[macro_use]
extern crate gotham_restful;
#[derive(FromBody)]
enum FromBodyEnum {
SomeVariant(Vec<u8>),
OtherVariant(String)
}
fn main() {}

View file

@ -1,5 +1,5 @@
error: #[derive(FromBody)] only works for structs
--> $DIR/from_body_enum.rs:4:1
--> $DIR/enum.rs:5:1
|
4 | enum FromBodyEnum
5 | enum FromBodyEnum {
| ^^^^

View file

@ -1,12 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(FromBody)]
enum FromBodyEnum
{
SomeVariant(Vec<u8>),
OtherVariant(String)
}
fn main()
{
}

View file

@ -1,15 +0,0 @@
#[macro_use] extern crate gotham_restful;
use gotham_restful::State;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
async fn read_all(state : &State)
{
}
fn main()
{
}

View file

@ -1,5 +0,0 @@
error: async fn must not take &State as an argument as State is not Sync, consider taking &mut State
--> $DIR/method_async_state.rs:9:19
|
9 | async fn read_all(state : &State)
| ^^^^^

View file

@ -1,10 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[read_all(UnknownResource)]
fn read_all()
{
}
fn main()
{
}

View file

@ -1,5 +0,0 @@
error[E0412]: cannot find type `UnknownResource` in this scope
--> $DIR/method_for_unknown_resource.rs:3:12
|
3 | #[read_all(UnknownResource)]
| ^^^^^^^^^^^^^^^ not found in this scope

View file

@ -1,15 +0,0 @@
error: Missing Resource struct. Example: #[read_all(MyResource)]
--> $DIR/method_no_resource.rs:7:1
|
7 | #[read_all]
| ^^^^^^^^^^^
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0425]: cannot find function `_gotham_restful_foo_resource_read_all_setup_impl` in this scope
--> $DIR/method_no_resource.rs:3:10
|
3 | #[derive(Resource)]
| ^^^^^^^^ not found in this scope
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -1,13 +0,0 @@
error: Didn't expect self parameter
--> $DIR/method_self.rs:8:13
|
8 | fn read_all(self)
| ^^^^
error: `self` parameter is only allowed in associated functions
--> $DIR/method_self.rs:8:13
|
8 | fn read_all(self)
| ^^^^ not semantically valid as function parameter
|
= note: associated functions are those in `impl` or `trait` definitions

View file

@ -1,14 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(Resource)]
#[resource(read)]
struct FooResource;
#[read(FooResource)]
fn read()
{
}
fn main()
{
}

View file

@ -1,5 +0,0 @@
error: Too few arguments
--> $DIR/method_too_few_args.rs:8:4
|
8 | fn read()
| ^^^^

View file

@ -1,14 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
fn read_all(_id : u64)
{
}
fn main()
{
}

View file

@ -1,5 +0,0 @@
error: Too many arguments
--> $DIR/method_too_many_args.rs:8:13
|
8 | fn read_all(_id : u64)
| ^^^

View file

@ -1,14 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_all)]
struct FooResource;
#[read_all(FooResource)]
unsafe fn read_all()
{
}
fn main()
{
}

View file

@ -1,5 +0,0 @@
error: Resource methods must not be unsafe
--> $DIR/method_unsafe.rs:8:1
|
8 | unsafe fn read_all()
| ^^^^^^

View file

@ -0,0 +1,12 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
enum Food {
Pasta,
Pizza { pineapple: bool },
Rice,
Other(String)
}
fn main() {}

View file

@ -1,11 +1,11 @@
error: #[derive(OpenapiType)] does not support enum variants with fields
--> $DIR/openapi_type_enum_with_fields.rs:7:2
--> $DIR/enum_with_fields.rs:7:2
|
7 | Pizza { pineapple : bool },
7 | Pizza { pineapple: bool },
| ^^^^^
error: #[derive(OpenapiType)] does not support enum variants with fields
--> $DIR/openapi_type_enum_with_fields.rs:9:2
--> $DIR/enum_with_fields.rs:9:2
|
9 | Other(String)
| ^^^^^

View file

@ -0,0 +1,10 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo {
#[openapi(nullable = "yes, please")]
bar: String
}
fn main() {}

View file

@ -1,5 +1,5 @@
error: Expected bool
--> $DIR/openapi_type_nullable_non_bool.rs:6:23
--> $DIR/nullable_non_bool.rs:6:23
|
6 | #[openapi(nullable = "yes, please")]
| ^^^^^^^^^^^^^

View file

@ -0,0 +1,10 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo {
#[openapi(rename = 42)]
bar: String
}
fn main() {}

View file

@ -1,5 +1,5 @@
error: Expected string literal
--> $DIR/openapi_type_rename_non_string.rs:6:21
--> $DIR/rename_non_string.rs:6:21
|
6 | #[openapi(rename = 42)]
| ^^

View file

@ -0,0 +1,7 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo(String);
fn main() {}

View file

@ -1,5 +1,5 @@
error: #[derive(OpenapiType)] does not support unnamed fields
--> $DIR/openapi_type_tuple_struct.rs:4:11
--> $DIR/tuple_struct.rs:5:11
|
4 | struct Foo(String);
5 | struct Foo(String);
| ^^^^^^^^

View file

@ -0,0 +1,10 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
union IntOrPointer {
int: u64,
pointer: *mut String
}
fn main() {}

View file

@ -1,5 +1,5 @@
error: #[derive(OpenapiType)] only works for structs and enums
--> $DIR/openapi_type_union.rs:4:1
--> $DIR/union.rs:5:1
|
4 | union IntOrPointer
5 | union IntOrPointer {
| ^^^^^

View file

@ -0,0 +1,10 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo {
#[openapi(like = "pizza")]
bar: String
}
fn main() {}

View file

@ -1,5 +1,5 @@
error: Unknown key
--> $DIR/openapi_type_unknown_key.rs:6:12
--> $DIR/unknown_key.rs:6:12
|
6 | #[openapi(like = "pizza")]
| ^^^^

View file

@ -1,14 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(OpenapiType)]
enum Food
{
Pasta,
Pizza { pineapple : bool },
Rice,
Other(String)
}
fn main()
{
}

View file

@ -1,12 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo
{
#[openapi(nullable = "yes, please")]
bar : String
}
fn main()
{
}

View file

@ -1,12 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo
{
#[openapi(rename = 42)]
bar : String
}
fn main()
{
}

View file

@ -1,8 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo(String);
fn main()
{
}

View file

@ -1,12 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(OpenapiType)]
union IntOrPointer
{
int: u64,
pointer: *mut String
}
fn main()
{
}

View file

@ -1,12 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo
{
#[openapi(like = "pizza")]
bar : String
}
fn main()
{
}

View file

@ -0,0 +1,11 @@
#[macro_use]
extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_any)]
struct FooResource;
#[read_all]
fn read_all() {}
fn main() {}

View file

@ -0,0 +1,8 @@
error[E0412]: cannot find type `read_any___gotham_restful_endpoint` in this scope
--> $DIR/unknown_method.rs:5:12
|
5 | #[resource(read_any)]
| ^^^^^^^^ help: a struct with a similar name exists: `read_all___gotham_restful_endpoint`
...
8 | #[read_all]
| ----------- similarly named struct `read_all___gotham_restful_endpoint` defined here

View file

@ -1,14 +0,0 @@
#[macro_use] extern crate gotham_restful;
#[derive(Resource)]
#[resource(read_any)]
struct FooResource;
#[read_all(FooResource)]
fn read_all()
{
}
fn main()
{
}

View file

@ -1,14 +0,0 @@
error: Unknown method: `read_any'
--> $DIR/resource_unknown_method.rs:4:12
|
4 | #[resource(read_any)]
| ^^^^^^^^
error[E0277]: the trait bound `FooResource: Resource` is not satisfied
--> $DIR/resource_unknown_method.rs:7:1
|
7 | #[read_all(FooResource)]
| ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Resource` is not implemented for `FooResource`
|
= help: see issue #48214
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

15
tests/ui/rustfmt.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
rustfmt=${RUSTFMT:-rustfmt}
version="$($rustfmt -V)"
if [[ $version != *nightly* ]]; then
rustfmt="$rustfmt +nightly"
fi
return=0
find "$(dirname "$0")" -name '*.rs' -type f | while read file; do
$rustfmt --config-path "$(dirname "$0")/../../rustfmt.toml" "$@" "$file" || return=1
done
exit $return

View file

@ -2,12 +2,14 @@ use gotham::{
hyper::Body,
test::TestServer
};
use log::info;
use mime::Mime;
#[allow(unused_imports)]
use std::{fs::File, io::{Read, Write}, str};
pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8])
{
info!("GET {}", path);
let res = server.client().get(path).perform().unwrap().read_body().unwrap();
let body : &[u8] = res.as_ref();
assert_eq!(body, expected);
@ -17,6 +19,7 @@ pub fn test_post_response<B>(server : &TestServer, path : &str, body : B, mime :
where
B : Into<Body>
{
info!("POST {}", path);
let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap();
let body : &[u8] = res.as_ref();
assert_eq!(body, expected);
@ -26,6 +29,7 @@ pub fn test_put_response<B>(server : &TestServer, path : &str, body : B, mime :
where
B : Into<Body>
{
info!("PUT {}", path);
let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap();
let body : &[u8] = res.as_ref();
assert_eq!(body, expected);
@ -33,6 +37,7 @@ where
pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
{
info!("DELETE {}", path);
let res = server.client().delete(path).perform().unwrap().read_body().unwrap();
let body : &[u8] = res.as_ref();
assert_eq!(body, expected);
@ -41,12 +46,14 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
#[cfg(feature = "openapi")]
pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str)
{
info!("GET {}", path);
let res = server.client().get(path).perform().unwrap().read_body().unwrap();
let body = serde_json::to_string_pretty(&serde_json::from_slice::<serde_json::Value>(res.as_ref()).unwrap()).unwrap();
match File::open(output_file) {
Ok(mut file) => {
let mut expected = String::new();
file.read_to_string(&mut expected).unwrap();
eprintln!("{}", body);
assert_eq!(body, expected);
},
Err(_) => {