Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive for OpenApiResponderInner #157

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
**/*.rs.bk
Cargo.lock
/.idea
.vscode
.vscode
.DS_Store
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn merge_description_lines(doc: &str) -> Option<String> {
none_if_empty(desc)
}

fn get_doc(attrs: &[Attribute]) -> Option<String> {
pub fn get_doc(attrs: &[Attribute]) -> Option<String> {
let doc = attrs
.iter()
.filter_map(|attr| {
Expand Down
19 changes: 18 additions & 1 deletion rocket-okapi-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
//! - `#[derive(OpenApiFromRequest)]`: Implement `OpenApiFromRequest` trait for a given struct.
//!

mod doc_attr;
mod openapi_attr;
mod openapi_spec;
mod parse_routes;
mod responder_derive;

use proc_macro::TokenStream;
use quote::quote;
Expand Down Expand Up @@ -85,7 +87,7 @@ pub fn openapi_spec(input: TokenStream) -> TokenStream {
.into()
}

/// Derive marco for the `OpenApiFromRequest` trait.
/// Derive macro for the `OpenApiFromRequest` trait.
///
/// This derive trait is a very simple implementation for anything that does not
/// require any other special headers or parameters to be validated.
Expand Down Expand Up @@ -136,6 +138,21 @@ pub fn open_api_from_request_derive(input: TokenStream) -> TokenStream {
gen.into()
}

/// Derive for the [`OpenApiResponderInner`](rocket_okapi::response::OpenApiResponderInner) trait.
///
/// Derive is fully compatible with the syntax of the
/// [`#[response]`](https://api.rocket.rs/v0.5/rocket/derive.Responder#field-attribute) attribute
/// from Rocket and does not require any code changes.
#[proc_macro_derive(OpenApiResponder, attributes(responder))]
pub fn open_api_responder_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();

match responder_derive::derive(ast) {
Ok(v) => v.into(),
Err(err) => err.write_errors().into(),
}
}

fn get_add_operation_fn_name(route_fn_name: &Ident) -> Ident {
Ident::new(
&format!("okapi_add_operation_for_{}_", route_fn_name),
Expand Down
5 changes: 2 additions & 3 deletions rocket-okapi-codegen/src/openapi_attr/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod doc_attr;
mod route_attr;

use crate::get_add_operation_fn_name;
use crate::{doc_attr::get_title_and_desc_from_doc, get_add_operation_fn_name};
use darling::FromMeta;
use proc_macro::TokenStream;
use proc_macro2::Span;
Expand Down Expand Up @@ -295,7 +294,7 @@ fn create_route_operation_fn(
.replace("..>", "}")
.replace('>', "}");
let method = Ident::new(&to_pascal_case_string(route.method), Span::call_site());
let (title, desc) = doc_attr::get_title_and_desc_from_doc(&route_fn.attrs);
let (title, desc) = get_title_and_desc_from_doc(&route_fn.attrs);
let title = match title {
Some(x) => quote!(Some(#x.to_owned())),
None => quote!(None),
Expand Down
113 changes: 113 additions & 0 deletions rocket-okapi-codegen/src/responder_derive/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
mod response_attr;

use crate::doc_attr::get_doc;
use darling::{FromMeta, Result};
use proc_macro2::TokenStream;
use quote::quote;
use response_attr::ResponseAttribute;
use syn::{parse_quote, Attribute, DeriveInput, Fields, GenericParam, Generics};

pub fn derive(input: DeriveInput) -> Result<TokenStream> {
let responses_variants: Vec<(Vec<Attribute>, Fields)> = match input.data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
[(input.attrs.clone(), fields)].to_vec()
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
variants.into_iter().map(|v| (v.attrs, v.fields)).collect()
}
syn::Data::Union(_) => {
return Err(darling::Error::custom("unions are not supported").with_span(&input));
}
};

let name = input.ident;

let generics = add_trait_bound(input.generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let responses = responses_variants
.into_iter()
.filter_map(|(variant_attrs, variant_fields)| {
variant_to_responses(&input.attrs, variant_attrs, variant_fields).transpose()
})
.collect::<Result<Vec<_>>>()?;

Ok(quote! {
impl #impl_generics ::rocket_okapi::response::OpenApiResponderInner for #name #ty_generics #where_clause {
fn responses(
gen: &mut ::rocket_okapi::gen::OpenApiGenerator,
) -> ::rocket_okapi::Result<::rocket_okapi::okapi::openapi3::Responses> {
let mut responses = ::rocket_okapi::okapi::openapi3::Responses::default();
#(
responses.responses.extend({ #responses }.responses);
)*
Ok(responses)
}
}
})
}

fn add_trait_bound(mut generics: Generics) -> Generics {
for param in generics.params.iter_mut() {
if let GenericParam::Type(param) = param {
param.bounds.push(parse_quote!(
::rocket_okapi::response::OpenApiResponderInner
));
}
}
generics
}

fn variant_to_responses(
entity_attrs: &[Attribute],
attrs: Vec<Attribute>,
fields: Fields,
) -> Result<Option<TokenStream>> {
let response_attribute = get_response_attr_or_default(&attrs)?;

let field = fields
.into_iter()
.next()
.ok_or(darling::Error::custom("need at least one field"))?;
let response_type = &field.ty;

let status = response_attribute.status;
let set_status =
status.map(|status| quote! { ::rocket_okapi::util::set_status_code(&mut r, #status)?; });

let content_type = response_attribute.content_type;
let set_content_type =
content_type.map(|ct| quote! { ::rocket_okapi::util::set_content_type(&mut r, #ct)?; });

let description = get_doc(&field.attrs)
.or_else(|| get_doc(&attrs))
.or_else(|| get_doc(entity_attrs));
let set_description =
description.map(|doc| quote! { ::rocket_okapi::util::set_description(&mut r, #doc)?; });

Ok(Some(quote! {
let mut r = <#response_type as ::rocket_okapi::response::OpenApiResponderInner>::responses(gen)?;
#set_status
#set_content_type
#set_description
r
}))
}

fn get_response_attr_or_default(attrs: &[Attribute]) -> Result<ResponseAttribute> {
let mut attrs = attrs.iter().filter(|a| a.path.is_ident("response"));

let Some(attr) = attrs.next() else {
return Ok(ResponseAttribute::default());
};

if let Some(second_attr) = attrs.next() {
return Err(
darling::Error::custom("`response` attribute may be specified only once")
.with_span(second_attr),
);
}

let meta = attr.parse_meta()?;
ResponseAttribute::from_meta(&meta)
}
63 changes: 63 additions & 0 deletions rocket-okapi-codegen/src/responder_derive/response_attr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use darling::FromMeta;
use quote::ToTokens;

/// This structure documents all the properties that can be used in
/// the `#[response]` attribute. For example: `#[response(status = 404)]`
#[derive(Default, FromMeta)]
#[darling(default)]
pub struct ResponseAttribute {
/// Status code of the response. By default, depends on the field type
pub status: Option<Status>,

/// Content type of the response. By default, depends on the field type
pub content_type: Option<ContentType>,
}

/// Derive macro wrapper for the Rocket's Status type.
/// Implements [`darling::FromMeta`] and [`quote::ToTokens`].
pub struct Status(pub rocket_http::Status);

impl Default for Status {
fn default() -> Self {
Self(rocket_http::Status::Ok)
}
}

impl FromMeta for Status {
fn from_value(value: &syn::Lit) -> darling::Result<Self> {
let syn::Lit::Int(int) = value else {
return Err(darling::Error::unexpected_lit_type(value));
};

let code = int.base10_parse::<u16>()?;
if code < 100 || code >= 600 {
return Err(darling::Error::custom("status must be in range [100, 599]").with_span(int));
}

Ok(Self(rocket_http::Status::new(code)))
}
}

impl ToTokens for Status {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
self.0.code.to_tokens(tokens);
}
}

/// Derive macro wrapper for the Rocket's ContentType type.
/// Implements [`darling::FromMeta`] and [`quote::ToTokens`].
pub struct ContentType(pub rocket_http::ContentType);

impl FromMeta for ContentType {
fn from_string(value: &str) -> darling::Result<Self> {
rocket_http::ContentType::parse_flexible(value)
.map(Self)
.ok_or_else(|| darling::Error::unsupported_format(value))
}
}

impl ToTokens for ContentType {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
self.0.to_string().to_tokens(tokens);
}
}
9 changes: 9 additions & 0 deletions rocket-okapi/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ pub fn set_content_type(responses: &mut Responses, content_type: impl ToString)
Ok(())
}

/// Replaces the description for all responses.
pub fn set_description(responses: &mut Responses, description: impl ToString) -> Result<()> {
for ref mut resp_refor in responses.responses.values_mut() {
let response = ensure_not_ref(resp_refor)?;
response.description = description.to_string();
}
Ok(())
}

/// Adds a `Response` to a `Responses` object with the given status code, Content-Type and `SchemaObject`.
pub fn add_schema_response(
responses: &mut Responses,
Expand Down