Refactoring of schemars_derive
This commit is contained in:
		
							parent
							
								
									dca9e2d920
								
							
						
					
					
						commit
						3fb625e08c
					
				
					 5 changed files with 155 additions and 141 deletions
				
			
		
							
								
								
									
										65
									
								
								schemars_derive/src/attr/doc.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								schemars_derive/src/attr/doc.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| use syn::{Attribute, Lit::Str, Meta::NameValue, MetaNameValue}; | ||||
| 
 | ||||
| pub fn get_title_and_desc_from_doc(attrs: &[Attribute]) -> (Option<String>, Option<String>) { | ||||
|     let doc = match get_doc(attrs) { | ||||
|         None => return (None, None), | ||||
|         Some(doc) => doc, | ||||
|     }; | ||||
| 
 | ||||
|     if doc.starts_with('#') { | ||||
|         let mut split = doc.splitn(2, '\n'); | ||||
|         let title = split | ||||
|             .next() | ||||
|             .unwrap() | ||||
|             .trim_start_matches('#') | ||||
|             .trim() | ||||
|             .to_owned(); | ||||
|         let maybe_desc = split.next().and_then(merge_description_lines); | ||||
|         (none_if_empty(title), maybe_desc) | ||||
|     } else { | ||||
|         (None, merge_description_lines(&doc)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn merge_description_lines(doc: &str) -> Option<String> { | ||||
|     let desc = doc | ||||
|         .trim() | ||||
|         .split("\n\n") | ||||
|         .filter_map(|line| none_if_empty(line.trim().replace('\n', " "))) | ||||
|         .collect::<Vec<_>>() | ||||
|         .join("\n\n"); | ||||
|     none_if_empty(desc) | ||||
| } | ||||
| 
 | ||||
| fn get_doc(attrs: &[Attribute]) -> Option<String> { | ||||
|     let doc = attrs | ||||
|         .iter() | ||||
|         .filter_map(|attr| { | ||||
|             if !attr.path.is_ident("doc") { | ||||
|                 return None; | ||||
|             } | ||||
| 
 | ||||
|             let meta = attr.parse_meta().ok()?; | ||||
|             if let NameValue(MetaNameValue { lit: Str(s), .. }) = meta { | ||||
|                 return Some(s.value()); | ||||
|             } | ||||
| 
 | ||||
|             None | ||||
|         }) | ||||
|         .collect::<Vec<_>>() | ||||
|         .iter() | ||||
|         .flat_map(|a| a.split('\n')) | ||||
|         .map(str::trim) | ||||
|         .skip_while(|s| *s == "") | ||||
|         .collect::<Vec<_>>() | ||||
|         .join("\n"); | ||||
|     none_if_empty(doc) | ||||
| } | ||||
| 
 | ||||
| fn none_if_empty(s: String) -> Option<String> { | ||||
|     if s.is_empty() { | ||||
|         None | ||||
|     } else { | ||||
|         Some(s) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								schemars_derive/src/attr/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								schemars_derive/src/attr/mod.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| mod doc; | ||||
| mod schemars_to_serde; | ||||
| 
 | ||||
| pub use doc::get_title_and_desc_from_doc; | ||||
| pub use schemars_to_serde::process_serde_attrs; | ||||
| 
 | ||||
| use proc_macro2::{Group, Span, TokenStream, TokenTree}; | ||||
| use syn::parse::{self, Parse}; | ||||
| 
 | ||||
| pub fn get_with_from_attrs(field: &syn::Field) -> Option<syn::Result<syn::ExprPath>> { | ||||
|     field | ||||
|         .attrs | ||||
|         .iter() | ||||
|         .filter(|at| match at.path.get_ident() { | ||||
|             // FIXME this is relying on order of attributes (schemars before serde) from preprocess.rs
 | ||||
|             Some(i) => i == "schemars" || i == "serde", | ||||
|             None => false, | ||||
|         }) | ||||
|         .filter_map(get_with_from_attr) | ||||
|         .next() | ||||
|         .map(|lit| parse_lit_str(&lit)) | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
|         { | ||||
|             if path.is_ident("with") { | ||||
|                 return Some(with); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     None | ||||
| } | ||||
| 
 | ||||
| fn parse_lit_str<T>(s: &syn::LitStr) -> parse::Result<T> | ||||
| where | ||||
|     T: Parse, | ||||
| { | ||||
|     let tokens = spanned_tokens(s)?; | ||||
|     syn::parse2(tokens) | ||||
| } | ||||
| 
 | ||||
| fn spanned_tokens(s: &syn::LitStr) -> parse::Result<TokenStream> { | ||||
|     let stream = syn::parse_str(&s.value())?; | ||||
|     Ok(respan_token_stream(stream, s.span())) | ||||
| } | ||||
| 
 | ||||
| fn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream { | ||||
|     stream | ||||
|         .into_iter() | ||||
|         .map(|token| respan_token_tree(token, span)) | ||||
|         .collect() | ||||
| } | ||||
| 
 | ||||
| fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree { | ||||
|     if let TokenTree::Group(g) = &mut token { | ||||
|         *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span)); | ||||
|     } | ||||
|     token.set_span(span); | ||||
|     token | ||||
| } | ||||
							
								
								
									
										199
									
								
								schemars_derive/src/attr/schemars_to_serde.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								schemars_derive/src/attr/schemars_to_serde.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,199 @@ | |||
| use quote::ToTokens; | ||||
| use serde_derive_internals::Ctxt; | ||||
| use std::collections::HashSet; | ||||
| use syn::parse::Parser; | ||||
| use syn::{Attribute, Data, Field, Meta, NestedMeta, Variant}; | ||||
| 
 | ||||
| // List of keywords that can appear in #[serde(...)]/#[schemars(...)] attributes which we want serde_derive_internals to parse for us.
 | ||||
| static SERDE_KEYWORDS: &[&str] = &[ | ||||
|     "rename", | ||||
|     "rename_all", | ||||
|     // TODO: for structs with `deny_unknown_fields`, set schema's `additionalProperties` to false.
 | ||||
|     // "deny_unknown_fields",
 | ||||
|     "tag", | ||||
|     // TODO: support adjecently tagged enums (https://github.com/GREsau/schemars/issues/4)
 | ||||
|     // "content",
 | ||||
|     "untagged", | ||||
|     "default", | ||||
|     "skip", | ||||
|     "skip_serializing", | ||||
|     "skip_serializing_if", | ||||
|     "skip_deserializing", | ||||
|     "flatten", | ||||
|     // Special cases - `with`/`serialize_with` are passed to serde but not copied from schemars attrs to serde attrs.
 | ||||
|     // This is because we want to preserve any serde attribute's `serialize_with` value to determine whether the field's
 | ||||
|     // default value should be serialized. We also check the `with` value on schemars/serde attrs e.g. to support deriving
 | ||||
|     // JsonSchema on remote types, but we parse that ourselves rather than using serde_derive_internals.
 | ||||
|     "serialize_with", | ||||
|     "with", | ||||
| ]; | ||||
| 
 | ||||
| // If a struct/variant/field has any #[schemars] attributes, then create copies of them
 | ||||
| // as #[serde] attributes so that serde_derive_internals will parse them for us.
 | ||||
| pub fn process_serde_attrs(input: &mut syn::DeriveInput) -> Result<(), Vec<syn::Error>> { | ||||
|     let ctxt = Ctxt::new(); | ||||
|     process_attrs(&ctxt, &mut input.attrs); | ||||
|     match input.data { | ||||
|         Data::Struct(ref mut s) => process_serde_field_attrs(&ctxt, s.fields.iter_mut()), | ||||
|         Data::Enum(ref mut e) => process_serde_variant_attrs(&ctxt, e.variants.iter_mut()), | ||||
|         Data::Union(ref mut u) => process_serde_field_attrs(&ctxt, u.fields.named.iter_mut()), | ||||
|     }; | ||||
| 
 | ||||
|     ctxt.check() | ||||
| } | ||||
| 
 | ||||
| fn process_serde_variant_attrs<'a>(ctxt: &Ctxt, variants: impl Iterator<Item = &'a mut Variant>) { | ||||
|     for v in variants { | ||||
|         process_attrs(&ctxt, &mut v.attrs); | ||||
|         process_serde_field_attrs(&ctxt, v.fields.iter_mut()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn process_serde_field_attrs<'a>(ctxt: &Ctxt, fields: impl Iterator<Item = &'a mut Field>) { | ||||
|     for f in fields { | ||||
|         process_attrs(&ctxt, &mut f.attrs); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) { | ||||
|     let (serde_attrs, other_attrs): (Vec<_>, Vec<_>) = | ||||
|         attrs.drain(..).partition(|at| at.path.is_ident("serde")); | ||||
| 
 | ||||
|     *attrs = other_attrs; | ||||
| 
 | ||||
|     let schemars_attrs: Vec<_> = attrs | ||||
|         .iter() | ||||
|         .filter(|at| at.path.is_ident("schemars")) | ||||
|         .collect(); | ||||
| 
 | ||||
|     let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = schemars_attrs | ||||
|         .iter() | ||||
|         .flat_map(|at| get_meta_items(&ctxt, at)) | ||||
|         .flatten() | ||||
|         .filter_map(|meta| { | ||||
|             let keyword = get_meta_ident(&ctxt, &meta).ok()?; | ||||
|             if keyword.ends_with("with") || !SERDE_KEYWORDS.contains(&keyword.as_ref()) { | ||||
|                 None | ||||
|             } else { | ||||
|                 Some((meta, keyword)) | ||||
|             } | ||||
|         }) | ||||
|         .unzip(); | ||||
| 
 | ||||
|     if schemars_meta_names.contains("skip") { | ||||
|         schemars_meta_names.insert("skip_serializing".to_string()); | ||||
|         schemars_meta_names.insert("skip_deserializing".to_string()); | ||||
|     } | ||||
| 
 | ||||
|     for meta in serde_attrs | ||||
|         .into_iter() | ||||
|         .flat_map(|at| get_meta_items(&ctxt, &at)) | ||||
|         .flatten() | ||||
|     { | ||||
|         if let Ok(i) = get_meta_ident(&ctxt, &meta) { | ||||
|             if !schemars_meta_names.contains(&i) && SERDE_KEYWORDS.contains(&i.as_ref()) { | ||||
|                 serde_meta.push(meta); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if !serde_meta.is_empty() { | ||||
|         let new_serde_attr = quote! { | ||||
|             #[serde(#(#serde_meta),*)] | ||||
|         }; | ||||
| 
 | ||||
|         let parser = Attribute::parse_outer; | ||||
|         match parser.parse2(new_serde_attr) { | ||||
|             Ok(ref mut parsed) => attrs.append(parsed), | ||||
|             Err(e) => ctxt.error_spanned_by(to_tokens(attrs), e), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn to_tokens(attrs: &[Attribute]) -> impl ToTokens { | ||||
|     let mut tokens = proc_macro2::TokenStream::new(); | ||||
|     for attr in attrs { | ||||
|         attr.to_tokens(&mut tokens); | ||||
|     } | ||||
|     tokens | ||||
| } | ||||
| 
 | ||||
| fn get_meta_items(ctxt: &Ctxt, attr: &Attribute) -> Result<Vec<NestedMeta>, ()> { | ||||
|     match attr.parse_meta() { | ||||
|         Ok(Meta::List(meta)) => Ok(meta.nested.into_iter().collect()), | ||||
|         Ok(_) => { | ||||
|             ctxt.error_spanned_by(attr, "expected #[schemars(...)] or #[serde(...)]"); | ||||
|             Err(()) | ||||
|         } | ||||
|         Err(err) => { | ||||
|             ctxt.error_spanned_by(attr, err); | ||||
|             Err(()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn get_meta_ident(ctxt: &Ctxt, meta: &NestedMeta) -> Result<String, ()> { | ||||
|     match meta { | ||||
|         NestedMeta::Meta(m) => m.path().get_ident().map(|i| i.to_string()).ok_or(()), | ||||
|         NestedMeta::Lit(lit) => { | ||||
|             ctxt.error_spanned_by( | ||||
|                 meta, | ||||
|                 format!( | ||||
|                     "unexpected literal in attribute: {}", | ||||
|                     lit.into_token_stream() | ||||
|                 ), | ||||
|             ); | ||||
|             Err(()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use pretty_assertions::assert_eq; | ||||
|     use syn::DeriveInput; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_process_serde_attrs() { | ||||
|         let mut input: DeriveInput = parse_quote! { | ||||
|             #[serde(rename(serialize = "ser_name"), rename_all = "camelCase")] | ||||
|             #[serde(default, unknown_word)] | ||||
|             #[schemars(rename = "overriden", another_unknown_word)] | ||||
|             #[misc] | ||||
|             struct MyStruct { | ||||
|                 /// blah blah blah
 | ||||
|                 #[serde(skip_serializing_if = "some_fn")] | ||||
|                 field1: i32, | ||||
|                 #[serde(serialize_with = "se", deserialize_with = "de")] | ||||
|                 #[schemars(with = "with")] | ||||
|                 field2: i32, | ||||
|                 #[schemars(skip)] | ||||
|                 #[serde(skip_serializing)] | ||||
|                 field3: i32, | ||||
|             } | ||||
|         }; | ||||
|         let expected: DeriveInput = parse_quote! { | ||||
|             #[schemars(rename = "overriden", another_unknown_word)] | ||||
|             #[misc] | ||||
|             #[serde(rename = "overriden", rename_all = "camelCase", default)] | ||||
|             struct MyStruct { | ||||
|                 #[doc = r" blah blah blah"] | ||||
|                 #[serde(skip_serializing_if = "some_fn")] | ||||
|                 field1: i32, | ||||
|                 #[schemars(with = "with")] | ||||
|                 #[serde(serialize_with = "se")] | ||||
|                 field2: i32, | ||||
|                 #[schemars(skip)] | ||||
|                 #[serde(skip)] | ||||
|                 field3: i32, | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         if let Err(e) = process_serde_attrs(&mut input) { | ||||
|             panic!("process_serde_attrs returned error: {}", e[0]) | ||||
|         }; | ||||
| 
 | ||||
|         assert_eq!(input, expected); | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Graham Esau
						Graham Esau