Skip to content

Commit

Permalink
feat: implement rename_all in derive(ValueDeserialize) (#1094)
Browse files Browse the repository at this point in the history
  • Loading branch information
obmarg authored Nov 12, 2024
1 parent ee0ced7 commit 1695b40
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 4 deletions.
20 changes: 19 additions & 1 deletion cynic-parser-deser-macros/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use syn::{Attribute, Expr, Field, LitStr, Path};

#[derive(Default)]
use crate::renames::RenameAll;

#[derive(Default, Debug)]
pub struct StructAttribute {
pub default: Option<()>,
pub rename_all: Option<RenameAll>,
}

impl StructAttribute {
Expand All @@ -13,9 +16,21 @@ impl StructAttribute {
.map(|attr| {
let mut output = StructAttribute::default();
attr.parse_nested_meta(|meta| {
// Note: If adding an attribute in here don't forget to add it to
// the merge function below
if meta.path.is_ident("default") {
output.default = Some(());
Ok(())
} else if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let rename = value.parse::<LitStr>()?;
output.rename_all = Some(
rename
.value()
.parse()
.map_err(|e| syn::Error::new(rename.span(), e))?,
);
Ok(())
} else {
Err(meta.error("unsupported attribute"))
}
Expand All @@ -28,6 +43,7 @@ impl StructAttribute {

fn merge(mut self, other: Self) -> Self {
self.default = self.default.or(other.default);
self.rename_all = self.rename_all.or(other.rename_all);
self
}

Expand Down Expand Up @@ -59,6 +75,8 @@ impl FieldAttributes {
.iter()
.filter(|attr| attr.path().is_ident("deser"))
.map(|attr| {
// Note: If adding an attribute in here don't forget to add it to
// the merge function below
let mut output = FieldAttributes::default();
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
Expand Down
8 changes: 5 additions & 3 deletions cynic-parser-deser-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod attributes;
mod renames;

use attributes::{FieldAttributes, FieldDefault, StructAttribute};
use proc_macro2::TokenStream;
Expand Down Expand Up @@ -75,9 +76,10 @@ fn value_deser_impl(ast: syn::DeriveInput) -> Result<TokenStream, ()> {
let field_name_strings = fields
.iter()
.map(|(field, attrs)| {
proc_macro2::Literal::string(&match &attrs.rename {
Some(rename) => rename.to_string(),
None => field.ident.as_ref().unwrap().to_string(),
proc_macro2::Literal::string(&match (&attrs.rename, struct_attrs.rename_all) {
(Some(rename), _) => rename.to_string(),
(None, Some(rule)) => rule.apply(field.ident.as_ref().unwrap().to_string()),
_ => field.ident.as_ref().unwrap().to_string(),
})
})
.collect::<Vec<_>>();
Expand Down
201 changes: 201 additions & 0 deletions cynic-parser-deser-macros/src/renames.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use std::str::FromStr;

#[derive(Debug, Clone, Copy)]
/// Rules to rename all fields in an InputObject or variants in an Enum
/// as GraphQL naming conventions usually don't match rust
pub enum RenameAll {
None,
/// For names that are entirely lowercase in GraphQL: `myfield`
Lowercase,
/// For names that are entirely uppercase in GraphQL: `MYFIELD`
Uppercase,
/// For names that are entirely pascal case in GraphQL: `MyField`
PascalCase,
/// For names that are entirely camel case in GraphQL: `myField`
CamelCase,
/// For names that are entirely snake case in GraphQL: `my_field`
SnakeCase,
/// For names that are entirely snake case in GraphQL: `MY_FIELD`
ScreamingSnakeCase,
}

impl RenameAll {
pub(super) fn apply(&self, string: impl AsRef<str>) -> String {
match self {
RenameAll::Lowercase => string.as_ref().to_lowercase(),
RenameAll::Uppercase => string.as_ref().to_uppercase(),
RenameAll::PascalCase => to_pascal_case(string.as_ref()),
RenameAll::CamelCase => to_camel_case(string.as_ref()),
RenameAll::SnakeCase => to_snake_case(string.as_ref()),
RenameAll::ScreamingSnakeCase => to_snake_case(string.as_ref()).to_uppercase(),
RenameAll::None => string.as_ref().to_string(),
}
}
}

impl FromStr for RenameAll {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"none" => Ok(RenameAll::None),
"lowercase" => Ok(RenameAll::Lowercase),
"uppercase" => Ok(RenameAll::Uppercase),
"pascalcase" => Ok(RenameAll::PascalCase),
"camelcase" => Ok(RenameAll::CamelCase),
"snake_case" => Ok(RenameAll::SnakeCase),
"screaming_snake_case" => Ok(RenameAll::ScreamingSnakeCase),
case => {
Err(format!("unknown case: {case}. expected one of lowercase, UPPERCASE, PascalCAse, camelCase, snake_case, SCREAMING_SNAKE_CASE"))
}
}
}
}

pub fn to_snake_case(s: &str) -> String {
let mut buf = String::with_capacity(s.len());
// Setting this to true to avoid adding underscores at the beginning
let mut prev_is_upper = true;
for c in s.chars() {
if c.is_uppercase() && !prev_is_upper {
buf.push('_');
buf.extend(c.to_lowercase());
prev_is_upper = true;
} else if c.is_uppercase() {
buf.extend(c.to_lowercase());
} else {
prev_is_upper = false;
buf.push(c);
}
}
buf
}

pub fn to_pascal_case(s: &str) -> String {
let mut buf = String::with_capacity(s.len());
let mut first_char = true;
let mut prev_is_upper = false;
let mut prev_is_underscore = false;
let mut chars = s.chars().peekable();
loop {
let c = chars.next();
if c.is_none() {
break;
}
let c = c.unwrap();
if first_char {
if c == '_' {
// keep leading underscores
buf.push('_');
while let Some('_') = chars.peek() {
buf.push(chars.next().unwrap());
}
} else if c.is_uppercase() {
prev_is_upper = true;
buf.push(c);
} else {
buf.extend(c.to_uppercase());
}
first_char = false;
continue;
}

if c.is_uppercase() {
if prev_is_upper {
buf.extend(c.to_lowercase());
} else {
buf.push(c);
}
prev_is_upper = true;
} else if c == '_' {
prev_is_underscore = true;
prev_is_upper = false;
} else {
if prev_is_upper {
buf.extend(c.to_lowercase())
} else if prev_is_underscore {
buf.extend(c.to_uppercase());
} else {
buf.push(c);
}
prev_is_upper = false;
prev_is_underscore = false;
}
}

buf
}

pub(super) fn to_camel_case(s: &str) -> String {
let s = to_pascal_case(s);

let mut buf = String::with_capacity(s.len());
let mut chars = s.chars();

if let Some(first_char) = chars.next() {
buf.extend(first_char.to_lowercase());
}

buf.extend(chars);

buf
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_underscore() {
assert_eq!(to_snake_case("_hello"), "_hello");
assert_eq!(to_snake_case("_"), "_");
}

#[test]
fn test_to_snake_case() {
assert_eq!(to_snake_case("aString"), "a_string");
assert_eq!(to_snake_case("MyString"), "my_string");
assert_eq!(to_snake_case("my_string"), "my_string");
assert_eq!(to_snake_case("_another_one"), "_another_one");
assert_eq!(to_snake_case("RepeatedUPPERCASE"), "repeated_uppercase");
assert_eq!(to_snake_case("UUID"), "uuid");
}

#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("aString"), "aString");
assert_eq!(to_camel_case("MyString"), "myString");
assert_eq!(to_camel_case("my_string"), "myString");
assert_eq!(to_camel_case("_another_one"), "_anotherOne");
assert_eq!(to_camel_case("RepeatedUPPERCASE"), "repeatedUppercase");
assert_eq!(to_camel_case("UUID"), "uuid");
assert_eq!(to_camel_case("__typename"), "__typename");
}

#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("aString"), "AString");
assert_eq!(to_pascal_case("MyString"), "MyString");
assert_eq!(to_pascal_case("my_string"), "MyString");
assert_eq!(to_pascal_case("_another_one"), "_anotherOne");
assert_eq!(to_pascal_case("RepeatedUPPERCASE"), "RepeatedUppercase");
assert_eq!(to_pascal_case("UUID"), "Uuid");
assert_eq!(to_pascal_case("CREATED_AT"), "CreatedAt");
assert_eq!(to_pascal_case("__typename"), "__typename");
}

#[test]
fn casings_are_not_lossy_where_possible() {
for s in ["snake_case_thing", "snake"] {
assert_eq!(to_snake_case(&to_pascal_case(s)), s);
}

for s in ["PascalCase", "Pascal"] {
assert_eq!(to_pascal_case(&to_snake_case(s)), s);
}

for s in ["camelCase", "camel"] {
assert_eq!(to_camel_case(&to_snake_case(s)), s);
}
}
}
22 changes: 22 additions & 0 deletions cynic-parser-deser/tests/deser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,28 @@ fn test_option_defaults() {
);
}

#[derive(ValueDeserialize)]
struct RenameField {
#[deser(rename = "fooBar")]
foo_bar: usize,
}

#[test]
fn test_field_rename() {
assert_eq!(deser::<RenameField>("@id(fooBar: 1)").unwrap().foo_bar, 1);
}

#[derive(ValueDeserialize)]
#[deser(rename_all = "camelCase")]
struct RenameRule {
foo_bar: usize,
}

#[test]
fn test_rename_rule() {
assert_eq!(deser::<RenameRule>("@id(fooBar: 1)").unwrap().foo_bar, 1);
}

fn deser<T>(input: &str) -> Result<T, cynic_parser_deser::Error>
where
T: ValueDeserializeOwned,
Expand Down

0 comments on commit 1695b40

Please sign in to comment.