diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs index 416f209..60d2898 100644 --- a/schemars_derive/src/ast/from_serde.rs +++ b/schemars_derive/src/ast/from_serde.rs @@ -1,5 +1,5 @@ use super::*; -use crate::attr::get_with_from_attrs; +use crate::attr::Attrs; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::Ctxt; @@ -56,7 +56,7 @@ impl<'a> FromSerde for Variant<'a> { style: serde.style, fields: Field::vec_from_serde(errors, serde.fields)?, original: serde.original, - with: get_with_from_attrs(&serde.original.attrs, errors)?, + attrs: Attrs::new(&serde.original.attrs, errors), }) } } @@ -70,7 +70,7 @@ impl<'a> FromSerde for Field<'a> { serde_attrs: serde.attrs, ty: serde.ty, original: serde.original, - with: get_with_from_attrs(&serde.original.attrs, errors)?, + attrs: Attrs::new(&serde.original.attrs, errors), }) } } diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 14b9959..9688878 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -1,5 +1,6 @@ mod from_serde; +use crate::attr::{Attrs, WithAttr}; use from_serde::FromSerde; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::{Ctxt, Derive}; @@ -23,7 +24,7 @@ pub struct Variant<'a> { pub style: serde_ast::Style, pub fields: Vec>, pub original: &'a syn::Variant, - pub with: Option, + pub attrs: Attrs, } pub struct Field<'a> { @@ -31,7 +32,7 @@ pub struct Field<'a> { pub serde_attrs: serde_derive_internals::attr::Field, pub ty: &'a syn::Type, pub original: &'a syn::Field, - pub with: Option, + pub attrs: Attrs, } impl<'a> Container<'a> { @@ -69,6 +70,10 @@ impl<'a> Field<'a> { } pub fn type_for_schema(&self) -> &syn::Type { - self.with.as_ref().unwrap_or(self.ty) + match &self.attrs.with { + None => self.ty, + Some(WithAttr::Type(ty)) => ty, + Some(WithAttr::_Function(_)) => todo!(), + } } } diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 841228b..ebcd7bd 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -7,48 +7,169 @@ pub use schemars_to_serde::process_serde_attrs; use proc_macro2::{Group, Span, TokenStream, TokenTree}; use serde_derive_internals::Ctxt; use syn::parse::{self, Parse}; +use syn::Meta::{List, NameValue}; +use syn::MetaNameValue; +use syn::NestedMeta::{Lit, Meta}; -pub fn get_with_from_attrs( - attrs: &[syn::Attribute], - errors: &Ctxt, -) -> Result, ()> { - attrs - .iter() - .filter(|at| match at.path.get_ident() { - // FIXME this is relying on order of attributes (schemars before serde) from schemars_to_serde.rs - Some(i) => i == "schemars" || i == "serde", - None => false, - }) - .filter_map(get_with_from_attr) - .next() - .map_or(Ok(None), |lit| match parse_lit_str::(&lit) { - Ok(t) => Ok(Some(t)), - Err(e) => { - errors.error_spanned_by(lit, e); - Err(()) - } - }) +#[derive(Debug, Default)] +pub struct Attrs { + pub with: Option, + pub title: Option, + pub description: Option, + // TODO pub example: Option, } -fn get_with_from_attr(attr: &syn::Attribute) -> Option { - use syn::*; - let nested_metas = match attr.parse_meta() { - Ok(Meta::List(meta)) => meta.nested, - _ => return None, - }; - for nm in nested_metas { - if let NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit: Lit::Str(with), - .. - })) = nm +#[derive(Debug)] +pub enum WithAttr { + Type(syn::Type), + _Function(syn::Path), +} + +impl Attrs { + pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { + let (title, description) = doc::get_title_and_desc_from_doc(attrs); + Attrs { + title, + description, + ..Attrs::default() + } + .populate(attrs, "schemars", false, errors) + .populate(attrs, "serde", true, errors) + } + + fn populate( + mut self, + attrs: &[syn::Attribute], + attr_type: &'static str, + ignore_errors: bool, + errors: &Ctxt, + ) -> Self { + let duplicate_error = |meta: &MetaNameValue| { + if !ignore_errors { + let msg = format!( + "duplicate schemars attribute `{}`", + meta.path.get_ident().unwrap() + ); + errors.error_spanned_by(meta, msg) + } + }; + let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| { + if !ignore_errors { + let msg = format!( + "schemars attribute cannot contain both `{}` and `{}`", + meta.path.get_ident().unwrap(), + other, + ); + errors.error_spanned_by(meta, msg) + } + }; + + for meta_item in attrs + .iter() + .flat_map(|attr| get_meta_items(attr, attr_type, errors)) + .flatten() { - if path.is_ident("with") { - return Some(with); + match &meta_item { + Meta(NameValue(m)) if m.path.is_ident("with") => { + if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) { + match self.with { + Some(WithAttr::Type(_)) => duplicate_error(m), + Some(WithAttr::_Function(_)) => { + mutual_exclusive_error(m, "schema_with") + } + None => self.with = Some(WithAttr::Type(ty)), + } + } + } + + Meta(_meta_item) => { + // TODO uncomment this for 0.8.0 (breaking change) + // https://github.com/GREsau/schemars/issues/18 + // if !ignore_errors { + // let path = meta_item + // .path() + // .into_token_stream() + // .to_string() + // .replace(' ', ""); + // errors.error_spanned_by( + // meta_item.path(), + // format!("unknown schemars container attribute `{}`", path), + // ); + // } + } + + Lit(_lit) => { + // TODO uncomment this for 0.8.0 (breaking change) + // https://github.com/GREsau/schemars/issues/18 + // if !ignore_errors { + // errors.error_spanned_by( + // lit, + // "unexpected literal in schemars container attribute", + // ); + // } + } } } + self } - None +} + +fn get_meta_items( + attr: &syn::Attribute, + attr_type: &'static str, + errors: &Ctxt, +) -> Result, ()> { + if !attr.path.is_ident(attr_type) { + return Ok(Vec::new()); + } + + match attr.parse_meta() { + Ok(List(meta)) => Ok(meta.nested.into_iter().collect()), + Ok(other) => { + errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type)); + Err(()) + } + Err(err) => { + errors.error_spanned_by(attr, err); + Err(()) + } + } +} + +fn get_lit_str<'a>( + cx: &Ctxt, + attr_type: &'static str, + meta_item_name: &'static str, + lit: &'a syn::Lit, +) -> Result<&'a syn::LitStr, ()> { + if let syn::Lit::Str(lit) = lit { + Ok(lit) + } else { + cx.error_spanned_by( + lit, + format!( + "expected {} attribute to be a string: `{} = \"...\"`", + attr_type, meta_item_name + ), + ); + Err(()) + } +} + +fn parse_lit_into_ty( + cx: &Ctxt, + attr_type: &'static str, + meta_item_name: &'static str, + lit: &syn::Lit, +) -> Result { + let string = get_lit_str(cx, attr_type, meta_item_name, lit)?; + + parse_lit_str(string).map_err(|_| { + cx.error_spanned_by( + lit, + format!("failed to parse type: {} = {:?}", attr_type, string.value()), + ) + }) } fn parse_lit_str(s: &syn::LitStr) -> parse::Result diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 13ea944..e6b8728 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -1,4 +1,4 @@ -use crate::{ast::*, metadata::SchemaMetadata}; +use crate::{ast::*, attr::WithAttr, metadata::SchemaMetadata}; use proc_macro2::TokenStream; use serde_derive_internals::ast::Style; use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault, TagType}; @@ -33,7 +33,7 @@ fn expr_for_external_tagged_enum<'a>( variants: impl Iterator>, ) -> TokenStream { let (unit_variants, complex_variants): (Vec<_>, Vec<_>) = - variants.partition(|v| v.is_unit() && v.with.is_none()); + variants.partition(|v| v.is_unit() && v.attrs.with.is_none()); let unit_names = unit_variants.iter().map(|v| v.name()); let unit_schema = schema_object(quote! { @@ -147,7 +147,7 @@ fn expr_for_adjacent_tagged_enum<'a>( content_name: &str, ) -> TokenStream { let schemas = variants.map(|variant| { - let content_schema = if variant.is_unit() && variant.with.is_none() { + let content_schema = if variant.is_unit() && variant.attrs.with.is_none() { None } else { Some(expr_for_untagged_enum_variant(variant)) @@ -200,7 +200,7 @@ fn expr_for_adjacent_tagged_enum<'a>( } fn expr_for_untagged_enum_variant(variant: &Variant) -> TokenStream { - if let Some(with) = &variant.with { + if let Some(WithAttr::Type(with)) = &variant.attrs.with { return quote_spanned! {variant.original.span()=> gen.subschema_for::<#with>() }; @@ -215,7 +215,7 @@ fn expr_for_untagged_enum_variant(variant: &Variant) -> TokenStream { } fn expr_for_untagged_enum_variant_for_flatten(variant: &Variant) -> Option { - if let Some(with) = &variant.with { + if let Some(WithAttr::Type(with)) = &variant.attrs.with { return Some(quote_spanned! {variant.original.span()=> <#with>::json_schema(gen) });