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

fix(generics): support generic response types #332

Merged
merged 9 commits into from
Mar 18, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow automatically adding the module path to the openapi component name, via a feature "path-in-definition" [PR#373](https://github.com/paperclip-rs/paperclip/pull/373)
- Add missing ip, ipv4 and ipv6 string format types
- Add support for actix-web 4
- Add support for Schemas wrapping Generic types (e.g. `DataResponse<T>` where `T` also derives
`Apiv2Schema`) [PR#332](https://github.com/paperclip-rs/paperclip/pull/332)

### Fixed
- Add more tuple sizes for web::Path for OperationModifier impl [PR#379](https://github.com/paperclip-rs/paperclip/pull/379)
- Add missing extensions to openapi v2 Info
- Schemas that enclose Generics are no longer conflicting/overwritten

## [0.6.1] - 2021-10-15
### Fixed
Expand Down
32 changes: 22 additions & 10 deletions core/src/v2/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,9 @@ impl<T: chrono::TimeZone> OperationModifier for chrono::DateTime<T> {}

#[cfg(feature = "nightly")]
impl<T> Apiv2Schema for Json<T> {
default const NAME: Option<&'static str> = None;
default fn name() -> Option<String> {
None
}

default fn raw_schema() -> DefaultSchemaRaw {
Default::default()
Expand All @@ -290,7 +292,9 @@ impl<T> Apiv2Schema for Json<T> {

/// JSON needs specialization because it updates the global definitions.
impl<T: Apiv2Schema> Apiv2Schema for Json<T> {
const NAME: Option<&'static str> = T::NAME;
fn name() -> Option<String> {
T::name()
}

fn raw_schema() -> DefaultSchemaRaw {
T::raw_schema()
Expand Down Expand Up @@ -361,7 +365,9 @@ impl OperationModifier for actix_files::NamedFile {
macro_rules! impl_param_extractor ({ $ty:ty => $container:ident } => {
#[cfg(feature = "nightly")]
impl<T> Apiv2Schema for $ty {
default const NAME: Option<&'static str> = None;
default fn name() -> Option<String> {
None
}

default fn raw_schema() -> DefaultSchemaRaw {
Default::default()
Expand Down Expand Up @@ -427,7 +433,9 @@ fn map_schema_to_items(schema: &DefaultSchemaRaw) -> Items {
/// `formData` can refer to the global definitions.
#[cfg(feature = "nightly")]
impl<T: Apiv2Schema> Apiv2Schema for Form<T> {
const NAME: Option<&'static str> = T::NAME;
fn name() -> Option<String> {
T::name()
}

fn raw_schema() -> DefaultSchemaRaw {
T::raw_schema()
Expand Down Expand Up @@ -505,7 +513,9 @@ pub struct ResponderWrapper<T>(pub T);

#[cfg(feature = "nightly")]
impl<T: Responder> Apiv2Schema for ResponderWrapper<T> {
default const NAME: Option<&'static str> = None;
default fn name() -> Option<String> {
None
}

default fn raw_schema() -> DefaultSchemaRaw {
DefaultSchemaRaw::default()
Expand Down Expand Up @@ -633,10 +643,10 @@ fn update_security<T>(op: &mut DefaultOperationRaw)
where
T: Apiv2Schema,
{
if let (Some(name), Some(scheme)) = (T::NAME, T::security_scheme()) {
if let (Some(name), Some(scheme)) = (T::name(), T::security_scheme()) {
let mut security_map = BTreeMap::new();
let scopes = scheme.scopes.keys().map(String::clone).collect();
security_map.insert(name.into(), scopes);
security_map.insert(name, scopes);
op.security.push(security_map);
}
}
Expand All @@ -646,8 +656,8 @@ fn update_security_definitions<T>(map: &mut BTreeMap<String, SecurityScheme>)
where
T: Apiv2Schema,
{
if let (Some(name), Some(new)) = (T::NAME, T::security_scheme()) {
new.update_definitions(name, map);
if let (Some(name), Some(new)) = (T::name(), T::security_scheme()) {
new.update_definitions(&name, map);
}
}

Expand Down Expand Up @@ -720,7 +730,9 @@ macro_rules! json_with_status {
where
T: Serialize + Apiv2Schema,
{
const NAME: Option<&'static str> = T::NAME;
fn name() -> Option<String> {
T::name()
}

fn raw_schema() -> DefaultSchemaRaw {
T::raw_schema()
Expand Down
50 changes: 36 additions & 14 deletions core/src/v2/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,19 @@ impl_type_simple!(std::net::Ipv6Addr, DataType::String, DataTypeFormat::IpV6);
/// This is implemented for primitive types by default.
pub trait Apiv2Schema {
/// Name of this schema. This is the name to which the definition of the object is mapped.
const NAME: Option<&'static str> = None;
fn name() -> Option<String> {
None
}

/// Description of this schema. In case the trait is derived, uses the documentation on the type.
const DESCRIPTION: &'static str = "";
fn description() -> &'static str {
""
}

/// Indicates the requirement of this schema.
const REQUIRED: bool = true;
fn required() -> bool {
true
}

/// Returns the raw schema for this object.
fn raw_schema() -> DefaultSchemaRaw {
Expand All @@ -286,13 +292,13 @@ pub trait Apiv2Schema {
/// so it won't affect the incoming requests at all.
fn schema_with_ref() -> DefaultSchemaRaw {
let mut def = Self::raw_schema();
if let Some(n) = Self::NAME {
def.reference = Some(String::from("#/definitions/") + n);
if let Some(n) = Self::name() {
def.reference = Some(String::from("#/definitions/") + &n);
} else if let Some(n) = def.name.as_ref() {
def.reference = Some(String::from("#/definitions/") + n);
}
if !Self::DESCRIPTION.is_empty() {
def.description = Some(Self::DESCRIPTION.to_owned());
if !Self::description().is_empty() {
def.description = Some(Self::description().to_owned());
}

def
Expand Down Expand Up @@ -320,8 +326,13 @@ impl<T: TypedData> Apiv2Schema for T {

#[cfg(feature = "nightly")]
impl<T> Apiv2Schema for Option<T> {
default const NAME: Option<&'static str> = None;
default const REQUIRED: bool = false;
default fn name() -> Option<String> {
None
}

default fn required() -> bool {
false
}

default fn raw_schema() -> DefaultSchemaRaw {
Default::default()
Expand All @@ -333,8 +344,13 @@ impl<T> Apiv2Schema for Option<T> {
}

impl<T: Apiv2Schema> Apiv2Schema for Option<T> {
const NAME: Option<&'static str> = T::NAME;
const REQUIRED: bool = false;
fn name() -> Option<String> {
T::name()
}

fn required() -> bool {
false
}

fn raw_schema() -> DefaultSchemaRaw {
T::raw_schema()
Expand All @@ -347,7 +363,9 @@ impl<T: Apiv2Schema> Apiv2Schema for Option<T> {

#[cfg(feature = "nightly")]
impl<T, E> Apiv2Schema for Result<T, E> {
default const NAME: Option<&'static str> = None;
default fn name() -> Option<String> {
None
}

default fn raw_schema() -> DefaultSchemaRaw {
Default::default()
Expand All @@ -359,7 +377,9 @@ impl<T, E> Apiv2Schema for Result<T, E> {
}

impl<T: Apiv2Schema, E> Apiv2Schema for Result<T, E> {
const NAME: Option<&'static str> = T::NAME;
fn name() -> Option<String> {
T::name()
}

fn raw_schema() -> DefaultSchemaRaw {
T::raw_schema()
Expand All @@ -371,7 +391,9 @@ impl<T: Apiv2Schema, E> Apiv2Schema for Result<T, E> {
}

impl<T: Apiv2Schema + Clone> Apiv2Schema for std::borrow::Cow<'_, T> {
const NAME: Option<&'static str> = T::NAME;
fn name() -> Option<String> {
T::name()
}

fn raw_schema() -> DefaultSchemaRaw {
T::raw_schema()
Expand Down
1 change: 1 addition & 0 deletions core/src/v3/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) fn invalid_referenceor<T>(message: String) -> openapiv3::ReferenceOr<
}

impl<T> From<v2::Reference> for openapiv3::ReferenceOr<T> {
#[allow(clippy::only_used_in_recursion)]
fn from(v2: v2::Reference) -> Self {
Self::from(&v2)
}
Expand Down
35 changes: 27 additions & 8 deletions macros/src/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,20 @@ pub fn emit_v2_definition(input: TokenStream) -> TokenStream {
),
};

let schema_name = extract_rename(&item_ast.attrs).unwrap_or_else(|| name.to_string());
let base_name = extract_rename(&item_ast.attrs).unwrap_or_else(|| name.to_string());
let type_params: Vec<&Ident> = generics.type_params().map(|p| &p.ident).collect();
let schema_name = if type_params.is_empty() {
quote! { #base_name }
} else {
let type_names = quote! {
vec![#(#type_params::name()),*]
.iter()
.filter_map(|n| n.to_owned())
.collect::<Vec<String>>()
.join(", ")
};
quote! { format!("{}<{}>", #base_name, #type_names) }
};
let props_gen_empty = props_gen.is_empty();

#[cfg(not(feature = "path-in-definition"))]
Expand Down Expand Up @@ -788,7 +801,9 @@ pub fn emit_v2_definition(input: TokenStream) -> TokenStream {

#[cfg(not(feature = "path-in-definition"))]
let const_name_def = quote! {
const NAME: Option<&'static str> = Some(#schema_name);
fn name() -> Option<String> {
Some(#schema_name.to_string())
}
};

#[cfg(feature = "path-in-definition")]
Expand All @@ -811,7 +826,9 @@ pub fn emit_v2_definition(input: TokenStream) -> TokenStream {

impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
#const_name_def
const DESCRIPTION: &'static str = #docs;
fn description() -> &'static str {
#docs
}

fn raw_schema() -> paperclip::v2::models::DefaultSchemaRaw {
use paperclip::v2::models::{DataType, DataTypeFormat, DefaultSchemaRaw};
Expand Down Expand Up @@ -997,7 +1014,7 @@ pub fn emit_v2_security(input: TokenStream) -> TokenStream {
description: #quoted_description,
})
}),
Some(quote!(Some(#alias))),
Some(quote!(Some(#alias.to_string()))),
)
}
(None, Some(parent)) => {
Expand All @@ -1012,7 +1029,7 @@ pub fn emit_v2_security(input: TokenStream) -> TokenStream {
scheme.scopes = oauth2_scopes;
Some(scheme)
}),
Some(quote!(<#parent_ident as paperclip::v2::schema::Apiv2Schema>::NAME)),
Some(quote!(<#parent_ident as paperclip::v2::schema::Apiv2Schema>::name())),
)
}
(Some(_), Some(_)) => {
Expand All @@ -1034,7 +1051,9 @@ pub fn emit_v2_security(input: TokenStream) -> TokenStream {
let gen = if let (Some(def_block), Some(def_name)) = (security_def, security_def_name) {
quote! {
impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
const NAME: Option<&'static str> = #def_name;
fn name() -> Option<String> {
#def_name
}

fn security_scheme() -> Option<paperclip::v2::models::SecurityScheme> {
#def_block
Expand Down Expand Up @@ -1138,7 +1157,7 @@ fn handle_unnamed_field_struct(
};

gen.extend(quote! {
if #ty_ref::REQUIRED {
if #ty_ref::required() {
schema.required.insert(#inner_field_id.to_string());
}
});
Expand Down Expand Up @@ -1251,7 +1270,7 @@ fn handle_field_struct(
};

gen.extend(quote! {
if #ty_ref::REQUIRED {
if #ty_ref::required() {
schema.required.insert(#field_name.into());
}
});
Expand Down
Loading