Refactor attribute parsing to make it more extensible

This commit is contained in:
Graham Esau 2020-05-12 21:17:34 +01:00
parent b1ded882b7
commit 780c7286a6
4 changed files with 172 additions and 46 deletions

View file

@ -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<Option<syn::Type>, ()> {
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::<syn::Type>(&lit) {
Ok(t) => Ok(Some(t)),
Err(e) => {
errors.error_spanned_by(lit, e);
Err(())
}
})
#[derive(Debug, Default)]
pub struct Attrs {
pub with: Option<WithAttr>,
pub title: Option<String>,
pub description: Option<String>,
// TODO pub example: Option<syn::Path>,
}
fn get_with_from_attr(attr: &syn::Attribute) -> Option<syn::LitStr> {
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<Vec<syn::NestedMeta>, ()> {
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<syn::Type, ()> {
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<T>(s: &syn::LitStr) -> parse::Result<T>