paperclip_macros/
core.rs

1//! Convenience macros for paperclip (exposed by default).
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{spanned::Spanned, Data, DeriveInput, Fields, FieldsNamed, Ident};
6
7/// Actual parser and emitter for `api_v2_schema_struct` macro.
8pub fn emit_v2_schema_struct(input: TokenStream) -> TokenStream {
9    let mut item_ast = match crate::expect_struct_or_enum(input) {
10        Ok(i) => i,
11        Err(ts) => return ts,
12    };
13
14    let name = item_ast.ident.clone();
15    let generics = item_ast.generics.clone();
16    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
17
18    // Generate raw schema struct.
19    let mut raw_item_ast = item_ast.clone();
20    let raw_defaults = match raw_schema(&mut raw_item_ast) {
21        Ok(s) => s,
22        Err(ts) => return ts,
23    };
24
25    let raw_struct_name = &raw_item_ast.ident;
26    let mut gen = quote!(
27        /// Raw version of schema.
28        ///
29        /// **NOTE:** This doesn't have smart pointers to reuse definitions
30        /// throughout the spec. Instead, it contains the actual schema with
31        /// unresolvable `$ref` fields.
32        ///
33        #raw_item_ast
34    );
35
36    let defaults = match actual_schema(&mut item_ast) {
37        Ok(s) => s,
38        Err(ts) => return ts,
39    };
40
41    gen.extend(quote! {
42        #item_ast
43
44        impl Default for #name {
45            fn default() -> Self {
46                #name {
47                    #defaults
48                }
49            }
50        }
51
52        impl Default for #raw_struct_name {
53            fn default() -> Self {
54                #raw_struct_name {
55                    #raw_defaults
56                }
57            }
58        }
59
60        impl #raw_struct_name {
61            /// Recursively removes all `$ref` values in this schema.
62            pub fn remove_refs(&mut self) {
63                self.properties.values_mut().for_each(|s| s.remove_refs());
64                self.items.as_mut().map(|s| s.remove_refs());
65                self.extra_props.as_mut().and_then(|s| s.right_mut()).map(|s| s.remove_refs());
66                self.reference = None;
67            }
68
69            /// Recursively removes all properties other than `$ref` value
70            /// if the `$ref` is non-null.
71            pub fn retain_ref(&mut self) {
72                if self.reference.is_some() {
73                    let ref_ = self.reference.take();
74                    *self = Self::default();
75                    self.reference = ref_;
76                } else {
77                    self.properties.values_mut().for_each(|s| s.retain_ref());
78                    self.items.as_mut().map(|s| s.retain_ref());
79                    self.extra_props.as_mut().and_then(|s| s.right_mut()).map(|s| s.retain_ref());
80                }
81            }
82        }
83
84        impl #impl_generics paperclip::v2::Schema for #name #ty_generics #where_clause {
85            #[inline]
86            fn name(&self) -> Option<&str> {
87                self.name.as_ref().map(String::as_str)
88            }
89
90            #[inline]
91            fn set_name(&mut self, name: &str) {
92                self.name = Some(name.into());
93            }
94
95            #[inline]
96            fn set_cyclic(&mut self, cyclic: bool) {
97                self.cyclic = cyclic;
98            }
99
100            #[inline]
101            fn is_cyclic(&self) -> bool {
102                self.cyclic
103            }
104
105            #[inline]
106            fn description(&self) -> Option<&str> {
107                self.description.as_ref().map(String::as_str)
108            }
109
110            #[inline]
111            fn reference(&self) -> Option<&str> {
112                self.reference.as_ref().map(String::as_str)
113            }
114
115            #[inline]
116            fn set_reference(&mut self, ref_: String) {
117                self.reference = Some(ref_);
118            }
119
120            #[inline]
121            fn data_type(&self) -> Option<paperclip::v2::models::DataType> {
122                self.data_type
123            }
124
125            #[inline]
126            fn format(&self) -> Option<&paperclip::v2::models::DataTypeFormat> {
127                self.format.as_ref()
128            }
129
130            #[inline]
131            fn items(&self) -> Option<&paperclip::v2::models::Resolvable<Self>> {
132                self.items.as_ref()
133            }
134
135            #[inline]
136            fn items_mut(&mut self) -> Option<&mut paperclip::v2::models::Resolvable<Self>> {
137                self.items.as_mut()
138            }
139
140            #[inline]
141            fn additional_properties(&self) -> Option<&paperclip::v2::models::Either<bool, paperclip::v2::models::Resolvable<Self>>> {
142                self.extra_props.as_ref()
143            }
144
145            #[inline]
146            fn additional_properties_mut(&mut self) -> Option<&mut paperclip::v2::models::Either<bool, paperclip::v2::models::Resolvable<Self>>> {
147                self.extra_props.as_mut()
148            }
149
150            #[inline]
151            fn properties(&self) -> Option<&std::collections::BTreeMap<String, paperclip::v2::models::Resolvable<Self>>> {
152                if self.properties.is_empty() {
153                    None
154                } else {
155                    Some(&self.properties)
156                }
157            }
158
159            #[inline]
160            fn properties_mut(&mut self) -> Option<&mut std::collections::BTreeMap<String, paperclip::v2::models::Resolvable<Self>>> {
161                if self.properties.is_empty() {
162                    None
163                } else {
164                    Some(&mut self.properties)
165                }
166            }
167
168            #[inline]
169            fn required_properties(&self) -> Option<&std::collections::BTreeSet<String>> {
170                if self.required.is_empty() {
171                    None
172                } else {
173                    Some(&self.required)
174                }
175            }
176
177            #[inline]
178            fn enum_variants(&self) -> Option<&[paperclip::v2::serde_json::Value]> {
179                if self.enum_.is_empty() {
180                    return None
181                } else {
182                    Some(&self.enum_)
183                }
184            }
185        }
186    });
187
188    gen.into()
189}
190
191/// Generates a raw schema struct with suffix "{structName}Raw".
192fn raw_schema(item_ast: &mut DeriveInput) -> Result<proc_macro2::TokenStream, TokenStream> {
193    let ident = Ident::new(
194        &format!("{}Raw", item_ast.ident),
195        proc_macro2::Span::call_site(),
196    );
197    item_ast.ident = ident.clone();
198
199    let fields = named_fields(item_ast)?;
200    let default_fields: FieldsNamed =
201        syn::parse2(schema_fields(&ident, false)).expect("parsing schema fields?");
202    fields.named.extend(default_fields.named);
203
204    let mut defaults = quote!();
205    for field in &fields.named {
206        let f_name = field.ident.as_ref().expect("fields not named?");
207        defaults.extend(quote!(#f_name: Default::default(),));
208    }
209
210    Ok(defaults)
211}
212
213/// Generates the actual schema struct with the actual name.
214fn actual_schema(item_ast: &mut DeriveInput) -> Result<proc_macro2::TokenStream, TokenStream> {
215    let name = item_ast.ident.clone();
216    let fields = named_fields(item_ast)?;
217
218    let default_fields: FieldsNamed =
219        syn::parse2(schema_fields(&name, true)).expect("parsing schema fields?");
220    fields.named.extend(default_fields.named);
221
222    let mut defaults = quote!();
223    for field in &fields.named {
224        let f_name = field.ident.as_ref().expect("fields not named?");
225        defaults.extend(quote!(#f_name: Default::default(),));
226    }
227
228    Ok(defaults)
229}
230
231/// Extracts named fields from the given struct.
232fn named_fields(item_ast: &mut DeriveInput) -> Result<&mut FieldsNamed, TokenStream> {
233    let span = item_ast.span();
234    if let Data::Struct(s) = &mut item_ast.data {
235        match &mut s.fields {
236            Fields::Named(ref mut f) => Ok(f),
237            Fields::Unnamed(ref f) => Err(crate::span_error_with_msg(
238                f,
239                "expected struct with zero or more fields for schema",
240            )),
241            f @ Fields::Unit => {
242                *f = Fields::Named(syn::parse2(quote!({})).expect("parsing empty named fields"));
243                match f {
244                    Fields::Named(ref mut f) => Ok(f),
245                    _ => unreachable!(),
246                }
247            }
248        }
249    } else {
250        emit_error!(span.unwrap(), "expected struct for schema");
251        Err(quote!().into())
252    }
253}
254
255/// Generates fields for a schema struct using its name. Also takes a
256/// boolean to indicate whether this struct's fields hold references.
257fn schema_fields(name: &Ident, is_ref: bool) -> proc_macro2::TokenStream {
258    let mut gen = quote!();
259    let add_self = |gen: &mut proc_macro2::TokenStream| {
260        if is_ref {
261            gen.extend(quote!(paperclip::v2::models::Resolvable<#name>));
262        } else {
263            gen.extend(quote!(Box<#name>));
264        }
265    };
266
267    gen.extend(quote!(
268        #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
269        pub reference: Option<String>,
270    ));
271    gen.extend(quote!(
272        #[serde(skip_serializing_if = "Option::is_none")]
273        pub title: Option<String>,
274    ));
275    gen.extend(quote!(
276        #[serde(skip_serializing_if = "Option::is_none")]
277        pub description: Option<String>,
278    ));
279    gen.extend(quote!(
280        #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
281        pub data_type: Option<paperclip::v2::models::DataType>,
282    ));
283    gen.extend(quote!(
284        #[serde(skip_serializing_if = "Option::is_none")]
285        pub format: Option<paperclip::v2::models::DataTypeFormat>,
286    ));
287    gen.extend(quote!(
288        #[serde(skip_serializing_if = "Option::is_none")]
289        pub maximum: Option<f32>,
290    ));
291    gen.extend(quote!(
292        #[serde(skip_serializing_if = "Option::is_none")]
293        pub minimum: Option<f32>,
294    ));
295    gen.extend(quote!(
296        #[serde(skip_serializing_if = "Option::is_none")]
297        pub example: Option<paperclip::v2::serde_json::Value>,
298    ));
299
300    gen.extend(quote!(
301        #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
302        pub properties: std::collections::BTreeMap<String,
303    ));
304    add_self(&mut gen);
305    gen.extend(quote!(>,));
306
307    gen.extend(quote!(
308        #[serde(skip_serializing_if = "Option::is_none")]
309        pub items: Option<
310    ));
311    add_self(&mut gen);
312    gen.extend(quote!(>,));
313
314    gen.extend(quote!(
315        #[serde(default, rename = "enum", skip_serializing_if = "Vec::is_empty")]
316        pub enum_: Vec<paperclip::v2::serde_json::Value>,
317    ));
318
319    gen.extend(quote!(
320        #[serde(rename = "additionalProperties", skip_serializing_if = "Option::is_none")]
321        pub extra_props: Option<paperclip::v2::models::Either<bool,
322    ));
323    add_self(&mut gen);
324    gen.extend(quote!(>>,));
325
326    gen.extend(quote!(
327        #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
328        pub required: std::collections::BTreeSet<String>,
329    ));
330
331    if is_ref {
332        gen.extend(quote!(
333            #[serde(skip)]
334            cyclic: bool,
335        ));
336    }
337
338    quote!({
339        #[doc(hidden)]
340        #[serde(skip)]
341        pub name: Option<String>,
342        #gen
343    })
344}