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

Add support for auto-populating field default values #533

Merged
merged 4 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
21 changes: 17 additions & 4 deletions utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ impl Feature {
impl ToTokens for Feature {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let feature = match &self {
Feature::Default(default) => quote! { .default(Some(#default)) },
Feature::Default(default) => quote! { .default(#default) },
Feature::Example(example) => quote! { .example(Some(#example)) },
Feature::XmlAttr(xml) => quote! { .xml(Some(#xml)) },
Feature::Format(format) => quote! { .format(Some(#format)) },
Expand Down Expand Up @@ -414,17 +414,30 @@ name!(Example = "example");

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Default(AnyValue);
pub struct Default(Option<AnyValue>);

impl Default {
pub fn new_default_trait(struct_ident: Ident, field_ident: Ident) -> Self {
Self(Some(AnyValue::new_default_trait(struct_ident, field_ident)))
}
}

impl Parse for Default {
fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> {
parse_utils::parse_next(input, || AnyValue::parse_any(input)).map(Self)
if input.peek(syn::Token![=]) {
parse_utils::parse_next(input, || AnyValue::parse_any(input)).map(|any| Self(Some(any)))
} else {
Ok(Self(None))
}
}
}

impl ToTokens for Default {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(self.0.to_token_stream())
match &self.0 {
Some(inner) => tokens.extend(quote! {Some(#inner)}),
None => tokens.extend(quote! {None}),
}
}
}

Expand Down
30 changes: 29 additions & 1 deletion utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::borrow::Cow;

use proc_macro2::{Ident, Span, TokenStream};
use proc_macro_error::{abort, ResultExt};
use quote::{quote, ToTokens};
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::Parse, punctuated::Punctuated, token::Comma, Attribute, Data, Field, Fields,
FieldsNamed, FieldsUnnamed, GenericParam, Generics, Lifetime, LifetimeDef, Path, PathArguments,
Expand Down Expand Up @@ -269,6 +269,7 @@ impl NamedStructSchema<'_> {
fn field_as_schema_property<R>(
&self,
field: &Field,
container_rules: &Option<SerdeContainer>,
yield_: impl FnOnce(NamedStructFieldOptions<'_>) -> R,
) -> R {
let type_tree = &mut TypeTree::from_type(&field.ty);
Expand All @@ -278,6 +279,31 @@ impl NamedStructSchema<'_> {
.parse_features::<NamedFieldFeatures>()
.into_inner();

let schema_default = self
.features
.as_ref()
.map(|features| features.iter().any(|f| matches!(f, Feature::Default(_))))
.unwrap_or(false);
let serde_default = container_rules
.as_ref()
.map(|rules| rules.default)
.unwrap_or(false);

if schema_default || serde_default {
let field_ident = field.ident.as_ref().unwrap().to_owned();
let struct_ident = format_ident!("{}", &self.struct_name);
let default_feature = Feature::Default(
crate::component::features::Default::new_default_trait(struct_ident, field_ident),
);
let features_inner = field_features.get_or_insert(vec![default_feature.clone()]);
if !features_inner
.iter()
.any(|f| matches!(f, Feature::Default(_)))
{
features_inner.push(default_feature);
}
}

let rename_field =
pop_feature!(field_features => Feature::Rename(_)).and_then(|feature| match feature {
Feature::Rename(rename) => Some(Cow::Owned(rename.into_value())),
Expand Down Expand Up @@ -362,6 +388,7 @@ impl ToTokens for NamedStructSchema<'_> {

self.field_as_schema_property(
field,
&container_rules,
|NamedStructFieldOptions {
property,
rename_field_value,
Expand Down Expand Up @@ -431,6 +458,7 @@ impl ToTokens for NamedStructSchema<'_> {
for field in flatten_fields {
self.field_as_schema_property(
field,
&container_rules,
|NamedStructFieldOptions { property, .. }| {
tokens.extend(quote! { .item(#property) });
},
Expand Down
3 changes: 2 additions & 1 deletion utoipa-gen/src/component/schema/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ impl Parse for NamedFieldStructFeatures {
RenameAll,
MaxProperties,
MinProperties,
As
As,
Default
)))
}
}
Expand Down
19 changes: 19 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ use self::{
/// * `as = ...` Can be used to define alternative path and name for the schema what will be used in
/// the OpenAPI. E.g _`as = path::to::Pet`_. This would make the schema appear in the generated
/// OpenAPI spec as _`path.to.Pet`_.
/// * `default` Can be used to populate default values on all fields using the struct's
/// [`Default`](std::default::Default) implementation.
///
/// # Enum Optional Configuration Options for `#[schema(...)]`
/// * `example = ...` Can be method reference or _`json!(...)`_.
Expand Down Expand Up @@ -2496,6 +2498,10 @@ impl ToTokens for ExternalDocs {
pub(self) enum AnyValue {
String(TokenStream2),
Json(TokenStream2),
DefaultTrait {
struct_ident: Ident,
field_ident: Ident,
},
}

impl AnyValue {
Expand Down Expand Up @@ -2550,6 +2556,13 @@ impl AnyValue {
Ok(AnyValue::Json(parse_utils::parse_json_token_stream(input)?))
}
}

fn new_default_trait(struct_ident: Ident, field_ident: Ident) -> Self {
Self::DefaultTrait {
struct_ident,
field_ident,
}
}
}

impl ToTokens for AnyValue {
Expand All @@ -2559,6 +2572,12 @@ impl ToTokens for AnyValue {
serde_json::json!(#json)
}),
Self::String(string) => string.to_tokens(tokens),
Self::DefaultTrait {
struct_ident,
field_ident,
} => tokens.extend(
quote! {::serde_json::to_value(#struct_ident::default().#field_ident).unwrap()},
),
}
}
}
Expand Down
68 changes: 67 additions & 1 deletion utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,71 @@ fn derive_struct_with_custom_properties_success() {
};
}

#[test]
fn derive_struct_with_default_attr() {
let book = api_doc! {
#[schema(default)]
struct Book {
name: String,
#[schema(default = 0)]
id: u64,
year: u64,
hash: String,
}

impl Default for Book {
fn default() -> Self {
Self {
name: "No name".to_string(),
id: 999,
year: 2020,
hash: "Test hash".to_string(),
}
}
}
};

assert_value! { book =>
"properties.name.default" = r#""No name""#, "Book name default"
"properties.id.default" = r#"0"#, "Book id default"
"properties.year.default" = r#"2020"#, "Book year default"
"properties.hash.default" = r#""Test hash""#, "Book hash default"
};
}

#[test]
fn derive_struct_with_serde_default_attr() {
let book = api_doc! {
#[derive(serde::Deserialize)]
#[serde(default)]
struct Book {
name: String,
#[schema(default = 0)]
id: u64,
year: u64,
hash: String,
}

impl Default for Book {
fn default() -> Self {
Self {
name: "No name".to_string(),
id: 999,
year: 2020,
hash: "Test hash".to_string(),
}
}
}
};

assert_value! { book =>
"properties.name.default" = r#""No name""#, "Book name default"
"properties.id.default" = r#"0"#, "Book id default"
"properties.year.default" = r#"2020"#, "Book year default"
"properties.hash.default" = r#""Test hash""#, "Book hash default"
};
}

#[test]
fn derive_struct_with_optional_properties() {
struct Book;
Expand Down Expand Up @@ -3279,7 +3344,8 @@ fn derive_schema_with_default_struct() {
json!({
"properties": {
"field": {
"type": "string"
"type": "string",
"default": ""
}
},
"type": "object"
Expand Down