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 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
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(pub(crate) Option<AnyValue>);

impl Default {
pub fn new_default_trait(struct_ident: Ident, field_ident: syn::Member) -> 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
43 changes: 42 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,30 @@ 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 features_inner = field_features.get_or_insert(vec![]);
if !features_inner
.iter()
.any(|f| matches!(f, Feature::Default(_)))
{
let field_ident = field.ident.as_ref().unwrap().to_owned();
let struct_ident = format_ident!("{}", &self.struct_name);
features_inner.push(Feature::Default(
crate::features::Default::new_default_trait(struct_ident, field_ident.into()),
));
}
}

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 +387,7 @@ impl ToTokens for NamedStructSchema<'_> {

self.field_as_schema_property(
field,
&container_rules,
|NamedStructFieldOptions {
property,
rename_field_value,
Expand Down Expand Up @@ -431,6 +457,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 Expand Up @@ -502,6 +529,20 @@ impl ToTokens for UnnamedStructSchema<'_> {
.unwrap_or_default();
}

if fields_len == 1 {
if let Some(ref mut features) = unnamed_struct_features {
if pop_feature!(features => Feature::Default(crate::features::Default(None)))
.is_some()
{
let struct_ident = format_ident!("{}", &self.struct_name);
let index: syn::Index = 0.into();
features.push(Feature::Default(
crate::features::Default::new_default_trait(struct_ident, index.into()),
));
}
}
}

tokens.extend(
ComponentSchema::new(super::ComponentSchemaProps {
type_tree: override_type_tree.as_ref().unwrap_or(first_part),
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
25 changes: 23 additions & 2 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use proc_macro2::{Group, Ident, Punct, TokenStream as TokenStream2};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
DeriveInput, ExprPath, ItemFn, Lit, LitStr, Token,
DeriveInput, ExprPath, ItemFn, Lit, LitStr, Member, Token,
};

mod component;
Expand Down 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 All @@ -108,7 +110,9 @@ use self::{
///
/// # Unnamed Field Struct Optional Configuration Options for `#[schema(...)]`
/// * `example = ...` Can be method reference or _`json!(...)`_.
/// * `default = ...` Can be method reference or _`json!(...)`_.
/// * `default = ...` Can be method reference or _`json!(...)`_. If no value is specified, and the struct has
/// only one field, the field's default value in the schema will be set from the struct's
/// [`Default`](std::default::Default) implementation.
/// * `format = ...` May either be variant of the [`KnownFormat`][known_format] enum, or otherwise
/// an open value as a string. By default the format is derived from the type of the property
/// according OpenApi spec.
Expand Down Expand Up @@ -2496,6 +2500,10 @@ impl ToTokens for ExternalDocs {
pub(self) enum AnyValue {
String(TokenStream2),
Json(TokenStream2),
DefaultTrait {
struct_ident: Ident,
field_ident: Member,
},
}

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

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

impl ToTokens for AnyValue {
Expand All @@ -2559,6 +2574,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
105 changes: 104 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 @@ -471,6 +536,43 @@ fn derive_struct_unnamed_field_vec_type_success() {
}
}

#[test]
fn derive_struct_unnamed_field_single_value_default_success() {
let point = api_doc! {
#[schema(default)]
struct Point(f32);

impl Default for Point {
fn default() -> Self {
Self(3.5)
}
}
};

assert_value! {point=>
"type" = r#""number""#, "Point type"
"format" = r#""float""#, "Point format"
"default" = r#"3.5"#, "Point default"
}
}

#[test]
fn derive_struct_unnamed_field_multiple_value_default_ignored() {
let point = api_doc! {
#[schema(default)]
struct Point(f32, f32);

impl Default for Point {
fn default() -> Self {
Self(3.5, 6.4)
}
}
};
// Default values shouldn't be assigned as the struct is represented
// as an array
assert!(!point.to_string().contains("default"))
}

#[test]
fn derive_struct_nested_vec_success() {
let vecs = api_doc! {
Expand Down Expand Up @@ -3279,7 +3381,8 @@ fn derive_schema_with_default_struct() {
json!({
"properties": {
"field": {
"type": "string"
"type": "string",
"default": ""
}
},
"type": "object"
Expand Down