schemars_derive/attr/
mod.rs

1mod doc;
2mod schemars_to_serde;
3mod validation;
4
5pub use schemars_to_serde::process_serde_attrs;
6pub use validation::ValidationAttrs;
7
8use crate::metadata::SchemaMetadata;
9use proc_macro2::{Group, Span, TokenStream, TokenTree};
10use quote::ToTokens;
11use serde_derive_internals::Ctxt;
12use syn::parse::{self, Parse};
13use syn::{Meta, MetaNameValue};
14
15// FIXME using the same struct for containers+variants+fields means that
16//  with/schema_with are accepted (but ignored) on containers, and
17//  repr/crate_name are accepted (but ignored) on variants and fields etc.
18
19#[derive(Debug, Default)]
20pub struct Attrs {
21    pub with: Option<WithAttr>,
22    pub title: Option<String>,
23    pub description: Option<String>,
24    pub deprecated: bool,
25    pub examples: Vec<syn::Path>,
26    pub repr: Option<syn::Type>,
27    pub crate_name: Option<syn::Path>,
28    pub is_renamed: bool,
29}
30
31#[derive(Debug)]
32pub enum WithAttr {
33    Type(syn::Type),
34    Function(syn::Path),
35}
36
37impl Attrs {
38    pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
39        let mut result = Attrs::default()
40            .populate(attrs, "schemars", false, errors)
41            .populate(attrs, "serde", true, errors);
42
43        result.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated"));
44        result.repr = attrs
45            .iter()
46            .find(|a| a.path().is_ident("repr"))
47            .and_then(|a| a.parse_args().ok());
48
49        let (doc_title, doc_description) = doc::get_title_and_desc_from_doc(attrs);
50        result.title = result.title.or(doc_title);
51        result.description = result.description.or(doc_description);
52
53        result
54    }
55
56    pub fn as_metadata(&self) -> SchemaMetadata<'_> {
57        #[allow(clippy::ptr_arg)]
58        fn none_if_empty(s: &String) -> Option<&str> {
59            if s.is_empty() {
60                None
61            } else {
62                Some(s)
63            }
64        }
65
66        SchemaMetadata {
67            title: self.title.as_ref().and_then(none_if_empty),
68            description: self.description.as_ref().and_then(none_if_empty),
69            deprecated: self.deprecated,
70            examples: &self.examples,
71            read_only: false,
72            write_only: false,
73            default: None,
74        }
75    }
76
77    fn populate(
78        mut self,
79        attrs: &[syn::Attribute],
80        attr_type: &'static str,
81        ignore_errors: bool,
82        errors: &Ctxt,
83    ) -> Self {
84        let duplicate_error = |meta: &MetaNameValue| {
85            if !ignore_errors {
86                let msg = format!(
87                    "duplicate schemars attribute `{}`",
88                    meta.path.get_ident().unwrap()
89                );
90                errors.error_spanned_by(meta, msg)
91            }
92        };
93        let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| {
94            if !ignore_errors {
95                let msg = format!(
96                    "schemars attribute cannot contain both `{}` and `{}`",
97                    meta.path.get_ident().unwrap(),
98                    other,
99                );
100                errors.error_spanned_by(meta, msg)
101            }
102        };
103
104        for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) {
105            match &meta_item {
106                Meta::NameValue(m) if m.path.is_ident("with") => {
107                    if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.value) {
108                        match self.with {
109                            Some(WithAttr::Type(_)) => duplicate_error(m),
110                            Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"),
111                            None => self.with = Some(WithAttr::Type(ty)),
112                        }
113                    }
114                }
115
116                Meta::NameValue(m) if m.path.is_ident("schema_with") => {
117                    if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.value)
118                    {
119                        match self.with {
120                            Some(WithAttr::Function(_)) => duplicate_error(m),
121                            Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"),
122                            None => self.with = Some(WithAttr::Function(fun)),
123                        }
124                    }
125                }
126
127                Meta::NameValue(m) if m.path.is_ident("title") => {
128                    if let Ok(title) = expr_as_lit_str(errors, attr_type, "title", &m.value) {
129                        match self.title {
130                            Some(_) => duplicate_error(m),
131                            None => self.title = Some(title.value()),
132                        }
133                    }
134                }
135
136                Meta::NameValue(m) if m.path.is_ident("description") => {
137                    if let Ok(description) =
138                        expr_as_lit_str(errors, attr_type, "description", &m.value)
139                    {
140                        match self.description {
141                            Some(_) => duplicate_error(m),
142                            None => self.description = Some(description.value()),
143                        }
144                    }
145                }
146
147                Meta::NameValue(m) if m.path.is_ident("example") => {
148                    if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.value) {
149                        self.examples.push(fun)
150                    }
151                }
152
153                Meta::NameValue(m) if m.path.is_ident("rename") => self.is_renamed = true,
154
155                Meta::NameValue(m) if m.path.is_ident("crate") && attr_type == "schemars" => {
156                    if let Ok(p) = parse_lit_into_path(errors, attr_type, "crate", &m.value) {
157                        if self.crate_name.is_some() {
158                            duplicate_error(m)
159                        } else {
160                            self.crate_name = Some(p)
161                        }
162                    }
163                }
164
165                _ if ignore_errors => {}
166
167                Meta::List(m) if m.path.is_ident("inner") && attr_type == "schemars" => {
168                    // This will be processed with the validation attributes.
169                    // It's allowed only for the schemars attribute because the
170                    // validator crate doesn't support it yet.
171                }
172
173                _ => {
174                    if !is_known_serde_or_validation_keyword(&meta_item) {
175                        let path = meta_item
176                            .path()
177                            .into_token_stream()
178                            .to_string()
179                            .replace(' ', "");
180                        errors.error_spanned_by(
181                            meta_item.path(),
182                            format!("unknown schemars attribute `{}`", path),
183                        );
184                    }
185                }
186            }
187        }
188        self
189    }
190
191    pub fn is_default(&self) -> bool {
192        matches!(self, Self {
193                with: None,
194                title: None,
195                description: None,
196                deprecated: false,
197                examples,
198                repr: None,
199                crate_name: None,
200                is_renamed: _,
201            } if examples.is_empty())
202    }
203}
204
205fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool {
206    let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS
207        .iter()
208        .chain(validation::VALIDATION_KEYWORDS);
209    meta.path()
210        .get_ident()
211        .map(|i| known_keywords.any(|k| i == k))
212        .unwrap_or(false)
213}
214
215fn get_meta_items(
216    attrs: &[syn::Attribute],
217    attr_type: &'static str,
218    errors: &Ctxt,
219    ignore_errors: bool,
220) -> Vec<Meta> {
221    let mut result = vec![];
222    for attr in attrs.iter().filter(|a| a.path().is_ident(attr_type)) {
223        match attr.parse_args_with(syn::punctuated::Punctuated::<Meta, Token![,]>::parse_terminated)
224        {
225            Ok(list) => result.extend(list),
226            Err(err) if !ignore_errors => errors.syn_error(err),
227            Err(_) => {}
228        }
229    }
230
231    result
232}
233
234fn expr_as_lit_str<'a>(
235    cx: &Ctxt,
236    attr_type: &'static str,
237    meta_item_name: &'static str,
238    expr: &'a syn::Expr,
239) -> Result<&'a syn::LitStr, ()> {
240    if let syn::Expr::Lit(syn::ExprLit {
241        lit: syn::Lit::Str(lit_str),
242        ..
243    }) = expr
244    {
245        Ok(lit_str)
246    } else {
247        cx.error_spanned_by(
248            expr,
249            format!(
250                "expected {} {} attribute to be a string: `{} = \"...\"`",
251                attr_type, meta_item_name, meta_item_name
252            ),
253        );
254        Err(())
255    }
256}
257
258fn parse_lit_into_ty(
259    cx: &Ctxt,
260    attr_type: &'static str,
261    meta_item_name: &'static str,
262    lit: &syn::Expr,
263) -> Result<syn::Type, ()> {
264    let string = expr_as_lit_str(cx, attr_type, meta_item_name, lit)?;
265
266    parse_lit_str(string).map_err(|_| {
267        cx.error_spanned_by(
268            lit,
269            format!(
270                "failed to parse type: `{} = {:?}`",
271                meta_item_name,
272                string.value()
273            ),
274        )
275    })
276}
277
278fn parse_lit_into_path(
279    cx: &Ctxt,
280    attr_type: &'static str,
281    meta_item_name: &'static str,
282    expr: &syn::Expr,
283) -> Result<syn::Path, ()> {
284    let lit_str = expr_as_lit_str(cx, attr_type, meta_item_name, expr)?;
285
286    parse_lit_str(lit_str).map_err(|_| {
287        cx.error_spanned_by(
288            expr,
289            format!(
290                "failed to parse path: `{} = {:?}`",
291                meta_item_name,
292                lit_str.value()
293            ),
294        )
295    })
296}
297
298fn parse_lit_str<T>(s: &syn::LitStr) -> parse::Result<T>
299where
300    T: Parse,
301{
302    let tokens = spanned_tokens(s)?;
303    syn::parse2(tokens)
304}
305
306fn spanned_tokens(s: &syn::LitStr) -> parse::Result<TokenStream> {
307    let stream = syn::parse_str(&s.value())?;
308    Ok(respan_token_stream(stream, s.span()))
309}
310
311fn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream {
312    stream
313        .into_iter()
314        .map(|token| respan_token_tree(token, span))
315        .collect()
316}
317
318fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree {
319    if let TokenTree::Group(g) = &mut token {
320        *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span));
321    }
322    token.set_span(span);
323    token
324}