use std::borrow::Cow;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{Attribute, Field, Generics, Token};
use super::feature::{FromAttributes, NamedFieldFeatures, parse_schema_features_with};
use super::{
ComponentSchema, FieldRename, FlattenedMapSchema, Property, is_flatten, is_not_skipped,
};
use crate::component::{ComponentDescription, ComponentSchemaProps};
use crate::doc_comment::CommentAttributes;
use crate::feature::attributes::{
self, Alias, Bound, Default, Ignore, Name, NoRecursion, RenameAll, Required, SkipBound,
};
use crate::feature::{
Feature, FeaturesExt, IsSkipped, TryToTokensExt, parse_features, pop_feature,
pop_feature_as_inner,
};
use crate::schema::{Description, Inline};
use crate::type_tree::TypeTree;
use crate::{
Deprecated, DiagLevel, DiagResult, Diagnostic, IntoInner, SerdeContainer, SerdeValue,
TryToTokens, serde_util,
};
#[derive(Debug)]
pub(crate) struct NamedStructSchema<'a> {
pub(crate) struct_name: Cow<'a, str>,
pub(crate) fields: &'a Punctuated<Field, Token![,]>,
pub(crate) attributes: &'a [Attribute],
pub(crate) description: Option<Description>,
pub(crate) features: Option<Vec<Feature>>,
pub(crate) rename_all: Option<RenameAll>,
#[allow(dead_code)]
pub(crate) generics: Option<&'a Generics>,
pub(crate) name: Option<Name>,
pub(crate) aliases: Option<Punctuated<Alias, Token![,]>>,
pub(crate) inline: Option<Inline>,
pub(crate) compose_context: Option<crate::component::ComposeContext>,
}
struct NamedStructFieldOptions<'a> {
property: Property,
rename_field_value: Option<Cow<'a, str>>,
required: Option<Required>,
is_option: bool,
ignore: Option<crate::parse_utils::LitBoolOrExprPath>,
}
impl NamedStructSchema<'_> {
pub(crate) fn pop_skip_bound(&mut self) -> Option<SkipBound> {
pop_feature_as_inner!(self.features => Feature::SkipBound(_v))
}
pub(crate) fn pop_bound(&mut self) -> Option<Bound> {
pop_feature_as_inner!(self.features => Feature::Bound(_v))
}
fn field_as_schema_property(
&self,
field: &Field,
flatten: bool,
container_rules: Option<&SerdeContainer>,
) -> DiagResult<NamedStructFieldOptions<'_>> {
let type_tree = &mut TypeTree::from_type(&field.ty)?;
let mut field_features = field
.attrs
.parse_features::<NamedFieldFeatures>()?
.into_inner();
if self
.features
.as_ref()
.map(|features| {
features
.iter()
.any(|f| matches!(f, Feature::NoRecursion(_)))
})
.unwrap_or(false)
{
let features_inner = field_features.get_or_insert(vec![]);
if !features_inner
.iter()
.any(|f| matches!(f, Feature::NoRecursion(_)))
{
features_inner.push(Feature::NoRecursion(NoRecursion));
}
}
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.is_default)
.unwrap_or(false);
if (schema_default || serde_default) && !flatten {
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()
.expect("field ident should be exist")
.to_owned();
let struct_ident = format_ident!("{}", &self.struct_name);
features_inner.push(Feature::Default(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())),
_ => None,
});
let deprecated = crate::get_deprecated(&field.attrs).or_else(|| {
pop_feature!(field_features => Feature::Deprecated(_)).and_then(|feature| match feature
{
Feature::Deprecated(_) => Some(Deprecated::True),
_ => None,
})
});
let value_type = field_features
.as_mut()
.and_then(|features| features.pop_value_type_feature());
let override_type_tree = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree())
.transpose()?;
let comments = CommentAttributes::from_attributes(&field.attrs);
let description = &ComponentDescription::CommentAttributes(&comments);
let with_schema = pop_feature!(field_features => Feature::SchemaWith(_));
let required = pop_feature_as_inner!(field_features => Feature::Required(_v));
let ignore = match pop_feature!(field_features => Feature::Ignore(_)) {
Some(Feature::Ignore(Ignore(bool_or_exp))) => Some(bool_or_exp),
_ => None,
};
let type_tree = override_type_tree.as_ref().unwrap_or(type_tree);
let is_option = type_tree.is_option();
Ok(NamedStructFieldOptions {
property: if let Some(with_schema) = with_schema {
Property::SchemaWith(with_schema)
} else {
let cs = ComponentSchemaProps {
type_tree,
features: field_features,
description: Some(description),
deprecated: deprecated.as_ref(),
object_name: self.struct_name.as_ref(),
compose_context: self.compose_context.as_ref(),
};
if flatten && type_tree.is_map() {
Property::FlattenedMap(FlattenedMapSchema::new(cs)?)
} else {
Property::Schema(ComponentSchema::new(cs)?)
}
},
rename_field_value: rename_field,
required,
is_option,
ignore,
})
}
}
impl TryToTokens for NamedStructSchema<'_> {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> DiagResult<()> {
let oapi = crate::oapi_crate();
let container_rules =
serde_util::parse_container(self.attributes).map_err(Diagnostic::from)?;
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.is_default)
.unwrap_or(false);
let field_values = self
.fields
.iter()
.map(|field| {
let is_skipped = field
.attrs
.parse_features::<NamedFieldFeatures>()?
.into_inner()
.map(|features| features.is_skipped())
.unwrap_or(false);
if is_skipped {
return Ok(None);
}
let field_rule = serde_util::parse_value(&field.attrs).map_err(Diagnostic::from)?;
if is_not_skipped(field_rule.as_ref()) && !is_flatten(field_rule.as_ref()) {
Ok(Some((field, field_rule)))
} else {
Ok(None)
}
})
.collect::<DiagResult<Vec<Option<(&Field, Option<SerdeValue>)>>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let mut object_tokens = quote! { #oapi::oapi::Object::new() };
for (field, field_rule) in field_values {
let mut field_name = &*field
.ident
.as_ref()
.expect("field ident should be exists")
.to_string();
if field_name.starts_with("r#") {
field_name = &field_name[2..];
}
let NamedStructFieldOptions {
property,
rename_field_value,
required,
is_option,
ignore,
} = self.field_as_schema_property(field, false, container_rules.as_ref())?;
match &ignore {
Some(crate::parse_utils::LitBoolOrExprPath::LitBool(lit)) if lit.value() => {
continue;
}
_ => {}
}
let rename_to = field_rule
.as_ref()
.and_then(|field_rule| field_rule.rename.as_deref().map(Cow::Borrowed))
.or(rename_field_value);
let rename_all = container_rules
.as_ref()
.and_then(|container_rule| container_rule.rename_all)
.or_else(|| {
self.rename_all
.as_ref()
.map(|rename_all| rename_all.to_rename_rule())
});
let name = crate::rename::<FieldRename>(field_name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(field_name));
let property = property.try_to_token_stream()?;
let component_required =
!is_option && crate::is_required(field_rule.as_ref(), container_rules.as_ref());
let required = match (required, component_required) {
(Some(required), _) => required.is_true(),
(None, component_required) => component_required,
};
let mut property_tokens = quote! {
.property(#name, #property)
};
if required {
property_tokens.extend(quote! {
.required(#name)
})
}
match &ignore {
Some(crate::parse_utils::LitBoolOrExprPath::ExprPath(path)) => {
object_tokens = quote! {
{
let __obj = #object_tokens;
if !#path() {
__obj #property_tokens
} else {
__obj
}
}
};
}
_ => {
object_tokens.extend(property_tokens);
}
}
}
let flatten_fields: Vec<&Field> = self
.fields
.iter()
.map(|field| {
let field_rule = serde_util::parse_value(&field.attrs).map_err(Diagnostic::from)?;
Ok((field, field_rule))
})
.collect::<DiagResult<Vec<_>>>()?
.into_iter()
.filter(|(_, field_rule)| is_flatten(field_rule.as_ref()))
.map(|(field, _)| field)
.collect();
let all_of = if !flatten_fields.is_empty() {
let mut flattened_tokens = TokenStream::new();
let mut flattened_map_field = None;
for field in flatten_fields {
let NamedStructFieldOptions { property, .. } =
self.field_as_schema_property(field, true, container_rules.as_ref())?;
match property {
Property::Schema(_) | Property::SchemaWith(_) => {
let property = property.try_to_token_stream()?;
flattened_tokens.extend(quote! { .item(#property) })
}
Property::FlattenedMap(_) => match flattened_map_field {
None => {
let property = property.try_to_token_stream()?;
object_tokens
.extend(quote! { .additional_properties(Some(#property)) });
flattened_map_field = Some(field);
}
Some(flattened_map_field) => {
return Err(Diagnostic::spanned(
self.fields.span(),
DiagLevel::Error,
format!(
"The structure `{}` contains multiple flattened map fields.",
self.struct_name
),
)
.note(format!(
"first flattened map field was declared here as `{:?}`",
flattened_map_field.ident
))
.note(format!(
"second flattened map field was declared here as `{:?}`",
field.ident
)));
}
},
}
}
if flattened_tokens.is_empty() {
tokens.extend(object_tokens);
false
} else {
tokens.extend(quote! {
#oapi::oapi::schema::AllOf::new()
#flattened_tokens
.item(#object_tokens)
});
true
}
} else {
tokens.extend(object_tokens);
false
};
if all_of && (schema_default || serde_default) {
let struct_ident = format_ident!("{}", &self.struct_name);
let mut default_value_tokens = TokenStream::new();
for field in self.fields {
let field_rule = serde_util::parse_value(&field.attrs).map_err(Diagnostic::from)?;
if !is_not_skipped(field_rule.as_ref()) {
continue;
}
let field_ident = field.ident.as_ref().expect("field ident should be exists");
let mut field_name = field_ident.to_string();
if field_name.starts_with("r#") {
field_name = field_name[2..].to_owned();
}
let NamedStructFieldOptions {
rename_field_value,
ignore,
..
} = self.field_as_schema_property(
field,
is_flatten(field_rule.as_ref()),
container_rules.as_ref(),
)?;
let maybe_guard = match &ignore {
Some(crate::parse_utils::LitBoolOrExprPath::LitBool(lit)) if lit.value() => {
continue;
}
Some(crate::parse_utils::LitBoolOrExprPath::ExprPath(path)) => {
Some(quote! { if !#path() })
}
_ => None,
};
if is_flatten(field_rule.as_ref()) {
let insert_tokens = quote! {
if let #oapi::oapi::__private::serde_json::Value::Object(__flattened) =
#oapi::oapi::__private::serde_json::to_value(#struct_ident::default().#field_ident).unwrap()
{
__default.extend(__flattened);
}
};
if let Some(guard_tokens) = maybe_guard {
default_value_tokens.extend(quote! {
#guard_tokens {
#insert_tokens
}
});
} else {
default_value_tokens.extend(insert_tokens);
}
} else {
let rename_to = field_rule
.as_ref()
.and_then(|field_rule| field_rule.rename.as_deref().map(Cow::Borrowed))
.or(rename_field_value);
let rename_all = container_rules
.as_ref()
.and_then(|container_rule| container_rule.rename_all)
.or_else(|| {
self.rename_all
.as_ref()
.map(|rename_all| rename_all.to_rename_rule())
});
let name = crate::rename::<FieldRename>(&field_name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(field_name.as_str()))
.into_owned();
let insert_tokens = quote! {
__default.insert(
#name.to_owned(),
#oapi::oapi::__private::serde_json::to_value(#struct_ident::default().#field_ident).unwrap(),
);
};
if let Some(guard_tokens) = maybe_guard {
default_value_tokens.extend(quote! {
#guard_tokens {
#insert_tokens
}
});
} else {
default_value_tokens.extend(insert_tokens);
}
}
}
tokens.extend(quote! {
.default_value({
let mut __default = #oapi::oapi::__private::serde_json::Map::new();
#default_value_tokens
#oapi::oapi::__private::serde_json::Value::Object(__default)
})
});
}
if !all_of
&& container_rules
.as_ref()
.map(|container_rule| container_rule.deny_unknown_fields)
.unwrap_or(false)
{
tokens.extend(quote! {
.additional_properties(#oapi::oapi::schema::AdditionalProperties::FreeForm(false))
});
}
if let Some(deprecated) = crate::get_deprecated(self.attributes) {
tokens.extend(quote! { .deprecated(#deprecated) });
}
if let Some(struct_features) = self.features.as_ref() {
tokens.extend(struct_features.try_to_token_stream()?)
}
let comments = CommentAttributes::from_attributes(self.attributes);
let description = self
.description
.as_ref()
.map(ComponentDescription::Description)
.or(Some(ComponentDescription::CommentAttributes(&comments)));
description.to_tokens(tokens);
Ok(())
}
}
#[derive(Debug)]
pub(super) struct UnnamedStructSchema<'a> {
pub(super) struct_name: Cow<'a, str>,
pub(super) fields: &'a Punctuated<Field, Token![,]>,
pub(super) description: Option<Description>,
pub(super) attributes: &'a [Attribute],
pub(super) features: Option<Vec<Feature>>,
pub(super) name: Option<Name>,
pub(super) aliases: Option<Punctuated<Alias, Comma>>,
pub(super) inline: Option<Inline>,
pub(super) compose_context: Option<crate::component::ComposeContext>,
}
impl UnnamedStructSchema<'_> {
pub(crate) fn pop_skip_bound(&mut self) -> Option<SkipBound> {
pop_feature_as_inner!(self.features => Feature::SkipBound(_v))
}
pub(crate) fn pop_bound(&mut self) -> Option<Bound> {
pop_feature_as_inner!(self.features => Feature::Bound(_v))
}
}
impl TryToTokens for UnnamedStructSchema<'_> {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> DiagResult<()> {
let oapi = crate::oapi_crate();
let fields_len = self.fields.len();
let first_field = self.fields.first().expect("fields should not be empty");
let first_part = &TypeTree::from_type(&first_field.ty)?;
let all_fields_are_same = fields_len == 1
|| self
.fields
.iter()
.skip(1)
.map(|field| TypeTree::from_type(&field.ty))
.collect::<Result<Vec<TypeTree>, Diagnostic>>()?
.iter()
.all(|schema_part| first_part == schema_part);
let deprecated = crate::get_deprecated(self.attributes);
if all_fields_are_same {
let mut unnamed_struct_features = self.features.clone();
let value_type = unnamed_struct_features
.as_mut()
.and_then(|features| features.pop_value_type_feature());
let override_type_tree = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree())
.transpose()?;
if fields_len == 1
&& let Some(ref mut features) = unnamed_struct_features
{
let inline = parse_schema_features_with(&first_field.attrs, |input| {
Ok(parse_features!(input as attributes::Inline))
})?
.unwrap_or_default();
features.extend(inline);
if pop_feature!(features => Feature::Default(Default(None))).is_some() {
let struct_ident = format_ident!("{}", &self.struct_name);
let index: syn::Index = 0.into();
features.push(Feature::Default(Default::new_default_trait(
struct_ident,
index.into(),
)));
}
}
if fields_len > 1
&& let Some(ref mut features) = unnamed_struct_features
&& let Some((name, span)) = features.iter().find_map(|feature| match feature {
Feature::MultipleOf(attr) => Some(("multiple_of", attr.span())),
Feature::Maximum(attr) => Some(("maximum", attr.span())),
Feature::Minimum(attr) => Some(("minimum", attr.span())),
Feature::ExclusiveMaximum(attr) => Some(("exclusive_maximum", attr.span())),
Feature::ExclusiveMinimum(attr) => Some(("exclusive_minimum", attr.span())),
Feature::MaxLength(attr) => Some(("max_length", attr.span())),
Feature::MinLength(attr) => Some(("min_length", attr.span())),
Feature::Pattern(attr) => Some(("pattern", attr.span())),
Feature::MaxItems(attr) => Some(("max_items", attr.span())),
Feature::MinItems(attr) => Some(("min_items", attr.span())),
_ => None,
})
{
return Err(Diagnostic::spanned(
span,
DiagLevel::Error,
format!(
"{name} attribute is not allowed for unnamed structs with multiple fields",
),
));
}
let comments = CommentAttributes::from_attributes(self.attributes);
let description = self
.description
.as_ref()
.map(ComponentDescription::Description)
.or(Some(ComponentDescription::CommentAttributes(&comments)));
tokens.extend(
ComponentSchema::new(ComponentSchemaProps {
type_tree: override_type_tree.as_ref().unwrap_or(first_part),
features: unnamed_struct_features,
description: description.as_ref(),
deprecated: deprecated.as_ref(),
object_name: self.struct_name.as_ref(),
compose_context: self.compose_context.as_ref(),
})?
.to_token_stream(),
);
} else {
tokens.extend(quote! {
#oapi::oapi::Object::new()
});
if let Some(deprecated) = deprecated {
tokens.extend(quote! { .deprecated(#deprecated) });
}
if let Some(ref attrs) = self.features {
let attrs = attrs
.iter()
.map(TryToTokens::try_to_token_stream)
.collect::<DiagResult<TokenStream>>()?;
tokens.extend(attrs)
}
};
if fields_len > 1 {
let comments = CommentAttributes::from_attributes(self.attributes);
let description = self
.description
.as_ref()
.map(ComponentDescription::Description)
.or(Some(ComponentDescription::CommentAttributes(&comments)));
tokens.extend(quote! {
.to_array()
.max_items(#fields_len)
.min_items(#fields_len)
#description
})
}
Ok(())
}
}