diff --git a/serde_codegen_internals/src/ast.rs b/serde_codegen_internals/src/ast.rs index ea598d80..0a50691e 100644 --- a/serde_codegen_internals/src/ast.rs +++ b/serde_codegen_internals/src/ast.rs @@ -38,7 +38,7 @@ impl<'a> Item<'a> { pub fn from_ast(cx: &Ctxt, item: &'a syn::MacroInput) -> Item<'a> { let attrs = attr::Item::from_ast(cx, item); - let body = match item.body { + let mut body = match item.body { syn::Body::Enum(ref variants) => Body::Enum(enum_from_ast(cx, variants)), syn::Body::Struct(ref variant_data) => { let (style, fields) = struct_from_ast(cx, variant_data); @@ -46,6 +46,22 @@ impl<'a> Item<'a> { } }; + match body { + Body::Enum(ref mut variants) => { + for ref mut variant in variants { + variant.attrs.rename_by_rule(attrs.rename_all()); + for ref mut field in &mut variant.fields { + field.attrs.rename_by_rule(variant.attrs.rename_all()); + } + } + } + Body::Struct(_, ref mut fields) => { + for field in fields { + field.attrs.rename_by_rule(attrs.rename_all()); + } + } + } + Item { ident: item.ident.clone(), attrs: attrs, diff --git a/serde_codegen_internals/src/attr.rs b/serde_codegen_internals/src/attr.rs index de7a997d..92a183c4 100644 --- a/serde_codegen_internals/src/attr.rs +++ b/serde_codegen_internals/src/attr.rs @@ -2,6 +2,7 @@ use Ctxt; use syn; use syn::MetaItem::{List, NameValue, Word}; use syn::NestedMetaItem::{Literal, MetaItem}; +use std::str::FromStr; // This module handles parsing of `#[serde(...)]` attributes. The entrypoints // are `attr::Item::from_ast`, `attr::Variant::from_ast`, and @@ -11,6 +12,8 @@ use syn::NestedMetaItem::{Literal, MetaItem}; // user will see errors simultaneously for all bad attributes in the crate // rather than just the first. +pub use case::RenameRule; + struct Attr<'c, T> { cx: &'c Ctxt, name: &'static str, @@ -91,6 +94,7 @@ pub struct Item { name: Name, deny_unknown_fields: bool, default: Default, + rename_all: RenameRule, ser_bound: Option>, de_bound: Option>, tag: EnumTag, @@ -135,6 +139,7 @@ impl Item { let mut de_name = Attr::none(cx, "rename"); let mut deny_unknown_fields = BoolAttr::none(cx, "deny_unknown_fields"); let mut default = Attr::none(cx, "default"); + let mut rename_all = Attr::none(cx, "rename_all"); let mut ser_bound = Attr::none(cx, "bound"); let mut de_bound = Attr::none(cx, "bound"); let mut untagged = BoolAttr::none(cx, "untagged"); @@ -160,6 +165,20 @@ impl Item { } } + // Parse `#[serde(rename_all="foo")]` + MetaItem(NameValue(ref name, ref lit)) if name == "rename_all" => { + if let Ok(s) = get_string_from_lit(cx, name.as_ref(), name.as_ref(), lit) { + match RenameRule::from_str(&s) { + Ok(rename_rule) => rename_all.set(rename_rule), + Err(()) => { + cx.error(format!("unknown rename rule for #[serde(rename_all \ + = {:?})]", + s)) + } + } + } + } + // Parse `#[serde(deny_unknown_fields)]` MetaItem(Word(ref name)) if name == "deny_unknown_fields" => { deny_unknown_fields.set_true(); @@ -244,7 +263,8 @@ impl Item { content.set(s); } syn::Body::Struct(_) => { - cx.error("#[serde(content = \"...\")] can only be used on enums") + cx.error("#[serde(content = \"...\")] can only be used on \ + enums") } } } @@ -297,7 +317,10 @@ impl Item { EnumTag::External } (false, Some(tag), Some(content)) => { - EnumTag::Adjacent { tag: tag, content: content } + EnumTag::Adjacent { + tag: tag, + content: content, + } } (true, Some(_), Some(_)) => { cx.error("untagged enum cannot have #[serde(tag = \"...\", content = \"...\")]"); @@ -312,6 +335,7 @@ impl Item { }, deny_unknown_fields: deny_unknown_fields.get(), default: default.get().unwrap_or(Default::None), + rename_all: rename_all.get().unwrap_or(RenameRule::None), ser_bound: ser_bound.get(), de_bound: de_bound.get(), tag: tag, @@ -322,6 +346,10 @@ impl Item { &self.name } + pub fn rename_all(&self) -> &RenameRule { + &self.rename_all + } + pub fn deny_unknown_fields(&self) -> bool { self.deny_unknown_fields } @@ -347,6 +375,9 @@ impl Item { #[derive(Debug)] pub struct Variant { name: Name, + ser_renamed: bool, + de_renamed: bool, + rename_all: RenameRule, skip_deserializing: bool, skip_serializing: bool, } @@ -357,6 +388,7 @@ impl Variant { let mut de_name = Attr::none(cx, "rename"); let mut skip_deserializing = BoolAttr::none(cx, "skip_deserializing"); let mut skip_serializing = BoolAttr::none(cx, "skip_serializing"); + let mut rename_all = Attr::none(cx, "rename_all"); for meta_items in variant.attrs.iter().filter_map(get_serde_meta_items) { for meta_item in meta_items { @@ -376,6 +408,21 @@ impl Variant { de_name.set_opt(de); } } + + // Parse `#[serde(rename_all="foo")]` + MetaItem(NameValue(ref name, ref lit)) if name == "rename_all" => { + if let Ok(s) = get_string_from_lit(cx, name.as_ref(), name.as_ref(), lit) { + match RenameRule::from_str(&s) { + Ok(rename_rule) => rename_all.set(rename_rule), + Err(()) => { + cx.error(format!("unknown rename rule for #[serde(rename_all \ + = {:?})]", + s)) + } + } + } + } + // Parse `#[serde(skip_deserializing)]` MetaItem(Word(ref name)) if name == "skip_deserializing" => { skip_deserializing.set_true(); @@ -396,11 +443,18 @@ impl Variant { } } + let ser_name = ser_name.get(); + let ser_renamed = ser_name.is_some(); + let de_name = de_name.get(); + let de_renamed = de_name.is_some(); Variant { name: Name { - serialize: ser_name.get().unwrap_or_else(|| variant.ident.to_string()), - deserialize: de_name.get().unwrap_or_else(|| variant.ident.to_string()), + serialize: ser_name.unwrap_or_else(|| variant.ident.to_string()), + deserialize: de_name.unwrap_or_else(|| variant.ident.to_string()), }, + ser_renamed: ser_renamed, + de_renamed: de_renamed, + rename_all: rename_all.get().unwrap_or(RenameRule::None), skip_deserializing: skip_deserializing.get(), skip_serializing: skip_serializing.get(), } @@ -410,6 +464,19 @@ impl Variant { &self.name } + pub fn rename_by_rule(&mut self, rule: &RenameRule) { + if !self.ser_renamed { + self.name.serialize = rule.apply_to_variant(&self.name.serialize); + } + if !self.de_renamed { + self.name.deserialize = rule.apply_to_variant(&self.name.deserialize); + } + } + + pub fn rename_all(&self) -> &RenameRule { + &self.rename_all + } + pub fn skip_deserializing(&self) -> bool { self.skip_deserializing } @@ -423,6 +490,8 @@ impl Variant { #[derive(Debug)] pub struct Field { name: Name, + ser_renamed: bool, + de_renamed: bool, skip_serializing: bool, skip_deserializing: bool, skip_serializing_if: Option, @@ -571,11 +640,17 @@ impl Field { default.set_if_none(Default::Default); } + let ser_name = ser_name.get(); + let ser_renamed = ser_name.is_some(); + let de_name = de_name.get(); + let de_renamed = de_name.is_some(); Field { name: Name { - serialize: ser_name.get().unwrap_or_else(|| ident.clone()), - deserialize: de_name.get().unwrap_or(ident), + serialize: ser_name.unwrap_or_else(|| ident.clone()), + deserialize: de_name.unwrap_or(ident), }, + ser_renamed: ser_renamed, + de_renamed: de_renamed, skip_serializing: skip_serializing.get(), skip_deserializing: skip_deserializing.get(), skip_serializing_if: skip_serializing_if.get(), @@ -591,6 +666,15 @@ impl Field { &self.name } + pub fn rename_by_rule(&mut self, rule: &RenameRule) { + if !self.ser_renamed { + self.name.serialize = rule.apply_to_field(&self.name.serialize); + } + if !self.de_renamed { + self.name.deserialize = rule.apply_to_field(&self.name.deserialize); + } + } + pub fn skip_serializing(&self) -> bool { self.skip_serializing } diff --git a/serde_codegen_internals/src/case.rs b/serde_codegen_internals/src/case.rs new file mode 100644 index 00000000..d25d98df --- /dev/null +++ b/serde_codegen_internals/src/case.rs @@ -0,0 +1,115 @@ +use std::ascii::AsciiExt; +use std::str::FromStr; + +use self::RenameRule::*; + +#[derive(Debug, PartialEq)] +pub enum RenameRule { + /// Don't apply a default rename rule. + None, + /// Rename direct children to "PascalCase" style, as typically used for enum variants. + PascalCase, + /// Rename direct children to "camelCase" style. + CamelCase, + /// Rename direct children to "snake_case" style, as commonly used for fields. + SnakeCase, + /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly used for constants. + ScreamingSnakeCase, + /// Rename direct children to "kebab-case" style. + KebabCase, +} + +impl RenameRule { + pub fn apply_to_variant(&self, variant: &str) -> String { + match *self { + None | PascalCase => variant.to_owned(), + CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..], + SnakeCase => { + let mut snake = String::new(); + for (i, ch) in variant.char_indices() { + if i > 0 && ch.is_uppercase() { + snake.push('_'); + } + snake.push(ch.to_ascii_lowercase()); + } + snake + } + ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(), + KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"), + } + } + + pub fn apply_to_field(&self, field: &str) -> String { + match *self { + None | SnakeCase => field.to_owned(), + PascalCase => { + let mut pascal = String::new(); + let mut capitalize = true; + for ch in field.chars() { + if ch == '_' { + capitalize = true; + } else if capitalize { + pascal.push(ch.to_ascii_uppercase()); + capitalize = false; + } else { + pascal.push(ch); + } + } + pascal + } + CamelCase => { + let pascal = PascalCase.apply_to_field(field); + pascal[..1].to_ascii_lowercase() + &pascal[1..] + } + ScreamingSnakeCase => field.to_ascii_uppercase(), + KebabCase => field.replace('_', "-"), + } + } +} + +impl FromStr for RenameRule { + type Err = (); + + fn from_str(rename_all_str: &str) -> Result { + match rename_all_str { + "PascalCase" => Ok(PascalCase), + "camelCase" => Ok(CamelCase), + "snake_case" => Ok(SnakeCase), + "SCREAMING_SNAKE_CASE" => Ok(ScreamingSnakeCase), + "kebab-case" => Ok(KebabCase), + _ => Err(()), + } + } +} + +#[test] +fn rename_variants() { + for &(original, camel, snake, screaming, kebab) in + &[("Outcome", "outcome", "outcome", "OUTCOME", "outcome"), + ("VeryTasty", "veryTasty", "very_tasty", "VERY_TASTY", "very-tasty"), + ("A", "a", "a", "A", "a"), + ("Z42", "z42", "z42", "Z42", "z42")] { + assert_eq!(None.apply_to_variant(original), original); + assert_eq!(PascalCase.apply_to_variant(original), original); + assert_eq!(CamelCase.apply_to_variant(original), camel); + assert_eq!(SnakeCase.apply_to_variant(original), snake); + assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming); + assert_eq!(KebabCase.apply_to_variant(original), kebab); + } +} + +#[test] +fn rename_fields() { + for &(original, pascal, camel, screaming, kebab) in + &[("outcome", "Outcome", "outcome", "OUTCOME", "outcome"), + ("very_tasty", "VeryTasty", "veryTasty", "VERY_TASTY", "very-tasty"), + ("a", "A", "a", "A", "a"), + ("z42", "Z42", "z42", "Z42", "z42")] { + assert_eq!(None.apply_to_field(original), original); + assert_eq!(PascalCase.apply_to_field(original), pascal); + assert_eq!(CamelCase.apply_to_field(original), camel); + assert_eq!(SnakeCase.apply_to_field(original), original); + assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming); + assert_eq!(KebabCase.apply_to_field(original), kebab); + } +} diff --git a/serde_codegen_internals/src/lib.rs b/serde_codegen_internals/src/lib.rs index cd998deb..c5e8885c 100644 --- a/serde_codegen_internals/src/lib.rs +++ b/serde_codegen_internals/src/lib.rs @@ -5,3 +5,5 @@ pub mod attr; mod ctxt; pub use ctxt::Ctxt; + +mod case;