Skip to content

Commit

Permalink
Add support for schema_with custon fn reference
Browse files Browse the repository at this point in the history
Add support for `schema_with` custom fn reference that can be used
alternatively via builders to alter the compile time schema if schema
attributes are not enough nor does not fit to the purpose.

Implement `schema_with` for `ToSchema` and `IntoParams` named struct
fields.

Supported syntax:
```rust
fn custom_type() -> Object {
    ObjectBuilder::new()
        .schema_type(utoipa::openapi::SchemaType::String)
        .format(Some(utoipa::openapi::SchemaFormat::Custom(
            "email".to_string(),
        )))
        .description(Some("this is email"))
        .build()
}

struct Value {
    #[schema(with_schema = custom_type)]
    email: String,
}

struct Value {
    #[param(with_schema = custom_type)]
    email: String,
}
```
  • Loading branch information
juhaku committed Dec 6, 2022
1 parent 391daef commit a736a56
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 48 deletions.
33 changes: 30 additions & 3 deletions utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use quote::{quote, ToTokens};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
LitFloat, LitInt, LitStr,
LitFloat, LitInt, LitStr, TypePath,
};

use crate::{
Expand Down Expand Up @@ -115,13 +115,14 @@ pub enum Feature {
MinItems(MinItems),
MaxProperties(MaxProperties),
MinProperties(MinProperties),
SchemaWith(SchemaWith),
}

impl Feature {
pub fn parse_named<T: Name>(input: syn::parse::ParseStream, ident: Ident) -> syn::Result<Self> {
let name = T::get_name();

const ALLOWED_NAMES: [&str; 29] = [
const ALLOWED_NAMES: [&str; 30] = [
"default",
"example",
"inline",
Expand Down Expand Up @@ -151,6 +152,7 @@ impl Feature {
"min_items",
"max_properties",
"min_properties",
"schema_with",
];

match name {
Expand Down Expand Up @@ -191,6 +193,7 @@ impl Feature {
"min_properties" => {
MinProperties::parse_with_ident(input, ident).map(Self::MinProperties)
}
"schema_with" => SchemaWith::parse(input).map(Self::SchemaWith),
_unexpected => Err(syn::Error::new(
ident.span(),
format!(
Expand Down Expand Up @@ -289,6 +292,7 @@ impl ToTokens for Feature {
Feature::MinProperties(min_properties) => {
quote! { .max_properties(Some(#min_properties)) }
}
Feature::SchemaWith(with_schema) => with_schema.to_token_stream(),
Feature::RenameAll(_) => {
abort! {
Span::call_site(),
Expand Down Expand Up @@ -353,6 +357,7 @@ impl Display for Feature {
Feature::MinItems(min_items) => min_items.fmt(f),
Feature::MaxProperties(max_properties) => max_properties.fmt(f),
Feature::MinProperties(min_properties) => min_properties.fmt(f),
Feature::SchemaWith(with_schema) => with_schema.fmt(f),
}
}
}
Expand Down Expand Up @@ -389,6 +394,7 @@ impl Validatable for Feature {
Feature::MinItems(min_items) => min_items.is_validatable(),
Feature::MaxProperties(max_properties) => max_properties.is_validatable(),
Feature::MinProperties(min_properties) => min_properties.is_validatable(),
Feature::SchemaWith(with_schema) => with_schema.is_validatable(),
}
}
}
Expand Down Expand Up @@ -434,7 +440,8 @@ is_validatable! {
MaxItems => true,
MinItems => true,
MaxProperties => false,
MinProperties => false
MinProperties => false,
SchemaWith => false
}

#[derive(Clone)]
Expand Down Expand Up @@ -1142,6 +1149,26 @@ impl ToTokens for MinProperties {
}

name!(MinProperties = "min_properties");
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct SchemaWith(TypePath);

impl Parse for SchemaWith {
fn parse(input: ParseStream) -> syn::Result<Self> {
parse_utils::parse_next(input, || input.parse::<TypePath>().map(Self))
}
}

impl ToTokens for SchemaWith {
fn to_tokens(&self, tokens: &mut TokenStream) {
let path = &self.0;
tokens.extend(quote! {
#path()
})
}
}

name!(SchemaWith = "schema_with");

pub trait Validator {
fn is_valid(&self) -> Result<(), &'static str>;
Expand Down
62 changes: 35 additions & 27 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
features::{
self, AllowReserved, Example, ExclusiveMaximum, ExclusiveMinimum, Explode, Format,
Inline, MaxItems, MaxLength, Maximum, MinItems, MinLength, Minimum, MultipleOf, Names,
Nullable, Pattern, ReadOnly, Rename, RenameAll, Style, WriteOnly, XmlAttr,
Nullable, Pattern, ReadOnly, Rename, RenameAll, SchemaWith, Style, WriteOnly, XmlAttr,
},
FieldRename,
},
Expand Down Expand Up @@ -225,6 +225,7 @@ impl Parse for FieldFeatures {
AllowReserved,
Example,
Explode,
SchemaWith,
// param schema features
Inline,
Format,
Expand Down Expand Up @@ -377,40 +378,47 @@ impl ToTokens for Param<'_> {
tokens.extend(quote! { .deprecated(Some(#deprecated)) });
}

let description = CommentAttributes::from_attributes(&field.attrs).as_formatted_string();
if !description.is_empty() {
tokens.extend(quote! { .description(Some(#description))})
}
let schema_with = pop_feature!(param_features => Feature::SchemaWith(_));
if let Some(schema_with) = schema_with {
tokens.extend(quote! { .schema(Some(#schema_with)).build() });
} else {
let description =
CommentAttributes::from_attributes(&field.attrs).as_formatted_string();
if !description.is_empty() {
tokens.extend(quote! { .description(Some(#description))})
}

let value_type = param_features.pop_value_type_feature();
let component = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree())
.unwrap_or(type_tree);
let value_type = param_features.pop_value_type_feature();
let component = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree())
.unwrap_or(type_tree);

let is_default = super::is_default(&self.serde_container, &field_param_serde.as_ref());
let required: Required =
(!(matches!(&component.generic_type, Some(GenericType::Option)) || is_default)).into();
let is_default = super::is_default(&self.serde_container, &field_param_serde.as_ref());
let required: Required =
(!(matches!(&component.generic_type, Some(GenericType::Option)) || is_default))
.into();

tokens.extend(quote! {
.required(#required)
});
tokens.extend(param_features.to_token_stream());
tokens.extend(quote! {
.required(#required)
});
tokens.extend(param_features.to_token_stream());

let schema = ParamType {
component: &component,
schema_features: &schema_features,
};
tokens.extend(quote! { .schema(Some(#schema)).build() });
let schema = ParamSchema {
component: &component,
schema_features: &schema_features,
};
tokens.extend(quote! { .schema(Some(#schema)).build() });
}
}
}

struct ParamType<'a> {
struct ParamSchema<'a> {
component: &'a TypeTree<'a>,
schema_features: &'a Vec<Feature>,
}

impl ToTokens for ParamType<'_> {
impl ToTokens for ParamSchema<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let component = self.component;

Expand All @@ -421,7 +429,7 @@ impl ToTokens for ParamType<'_> {
let max_items = pop_feature!(features => Feature::MaxItems(_));
let min_items = pop_feature!(features => Feature::MinItems(_));

let param_type = ParamType {
let param_type = ParamSchema {
component: component
.children
.as_ref()
Expand Down Expand Up @@ -460,7 +468,7 @@ impl ToTokens for ParamType<'_> {
| Some(GenericType::Cow)
| Some(GenericType::Box)
| Some(GenericType::RefCell) => {
let param_type = ParamType {
let param_type = ParamSchema {
component: component
.children
.as_ref()
Expand All @@ -477,7 +485,7 @@ impl ToTokens for ParamType<'_> {
// Maps are treated as generic objects with no named properties and
// additionalProperties denoting the type

let component_property = ParamType {
let component_property = ParamSchema {
component: component
.children
.as_ref()
Expand Down
56 changes: 40 additions & 16 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ impl NamedStructSchema<'_> {
fn field_as_schema_property<R>(
&self,
field: &Field,
yield_: impl FnOnce(SchemaProperty<'_>, Option<Cow<'_, str>>) -> R,
yield_: impl FnOnce(Property<'_>, Option<Cow<'_, str>>) -> R,
) -> R {
let type_tree = &mut TypeTree::from_type(&field.ty);

Expand Down Expand Up @@ -257,15 +257,20 @@ impl NamedStructSchema<'_> {
.as_ref()
.map(|value_type| value_type.as_type_tree());
let comments = CommentAttributes::from_attributes(&field.attrs);
let with_schema = pop_feature!(field_features => Feature::SchemaWith(_));

yield_(
SchemaProperty::new(
override_type_tree.as_ref().unwrap_or(type_tree),
Some(&comments),
field_features.as_ref(),
deprecated.as_ref(),
self.struct_name.as_ref(),
),
if let Some(with_schema) = with_schema {
Property::WithSchema(with_schema)
} else {
Property::Schema(SchemaProperty::new(
override_type_tree.as_ref().unwrap_or(type_tree),
Some(&comments),
field_features.as_ref(),
deprecated.as_ref(),
self.struct_name.as_ref(),
))
},
rename_field,
)
}
Expand Down Expand Up @@ -296,7 +301,7 @@ impl ToTokens for NamedStructSchema<'_> {
field_name = &field_name[2..];
}

self.field_as_schema_property(field, |schema_property, rename| {
self.field_as_schema_property(field, |property, rename| {
let rename_to = field_rule
.as_ref()
.and_then(|field_rule| field_rule.rename.as_deref().map(Cow::Borrowed))
Expand All @@ -314,15 +319,20 @@ impl ToTokens for NamedStructSchema<'_> {
.unwrap_or(Cow::Borrowed(field_name));

object_tokens.extend(quote! {
.property(#name, #schema_property)
.property(#name, #property)
});

if !schema_property.is_option()
&& !super::is_default(&container_rules.as_ref(), &field_rule.as_ref())
{
object_tokens.extend(quote! {
.required(#name)
})
if let Property::Schema(schema_property) = property {
if !schema_property.is_option()
&& !super::is_default(
&container_rules.as_ref(),
&field_rule.as_ref(),
)
{
object_tokens.extend(quote! {
.required(#name)
})
}
}

object_tokens
Expand Down Expand Up @@ -1031,6 +1041,20 @@ impl ToTokens for ComplexEnum<'_> {
#[derive(PartialEq)]
struct TypeTuple<'a, T>(T, &'a Ident);

enum Property<'a> {
Schema(SchemaProperty<'a>),
WithSchema(Feature),
}

impl ToTokens for Property<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Schema(schema) => schema.to_tokens(tokens),
Self::WithSchema(with_schema) => with_schema.to_tokens(tokens),
}
}
}

#[cfg_attr(feature = "debug", derive(Debug))]
struct SchemaProperty<'a> {
type_tree: &'a TypeTree<'a>,
Expand Down
5 changes: 3 additions & 2 deletions utoipa-gen/src/component/schema/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::component::features::{
impl_into_inner, parse_features, Default, Example, ExclusiveMaximum, ExclusiveMinimum, Feature,
Format, Inline, MaxItems, MaxLength, MaxProperties, Maximum, MinItems, MinLength,
MinProperties, Minimum, MultipleOf, Nullable, Pattern, ReadOnly, Rename, RenameAll, Title,
ValueType, WriteOnly, XmlAttr,
ValueType, SchemaWith, WriteOnly, XmlAttr,
};

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down Expand Up @@ -99,7 +99,8 @@ impl Parse for NamedFieldFeatures {
MinLength,
Pattern,
MaxItems,
MinItems
MinItems,
SchemaWith
)))
}
}
Expand Down
Loading

0 comments on commit a736a56

Please sign in to comment.