Allow disabling case conversion (#765)

This commit is contained in:
Igor Pashev 2020-10-13 18:34:36 +02:00 committed by GitHub
parent 68210f54ca
commit c964fd805c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 236 additions and 9 deletions

View file

@ -16,6 +16,13 @@ enum SomeEnum {
Full,
}
#[derive(juniper::GraphQLEnum, Debug, PartialEq)]
#[graphql(rename = "none")]
enum NoRenameEnum {
OneVariant,
AnotherVariant,
}
/// Enum doc.
#[derive(juniper::GraphQLEnum)]
enum DocEnum {
@ -64,6 +71,12 @@ fn test_derived_enum() {
assert_eq!(meta.name(), Some("Some"));
assert_eq!(meta.description(), Some(&"enum descr".to_string()));
// Test no rename variant.
assert_eq!(
<_ as ToInputValue>::to_input_value(&NoRenameEnum::AnotherVariant),
InputValue::scalar("AnotherVariant")
);
// Test Regular variant.
assert_eq!(
<_ as ToInputValue>::to_input_value(&SomeEnum::Regular),

View file

@ -20,6 +20,12 @@ struct Input {
other: Option<bool>,
}
#[derive(GraphQLInputObject, Debug, PartialEq)]
#[graphql(rename = "none")]
struct NoRenameInput {
regular_field: String,
}
/// Object comment.
#[derive(GraphQLInputObject, Debug, PartialEq)]
struct DocComment {
@ -147,6 +153,21 @@ fn test_derived_input_object() {
other: Some(true),
}
);
// Test disable renaming
let input: InputValue = ::serde_json::from_value(serde_json::json!({
"regular_field": "hello",
}))
.unwrap();
let output: NoRenameInput = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(
output,
NoRenameInput {
regular_field: "hello".into(),
}
);
}
#[test]

View file

@ -32,6 +32,7 @@ struct Nested {
}
struct Query;
struct NoRenameQuery;
/// Object comment.
#[derive(GraphQLObject, Debug, PartialEq)]
@ -72,6 +73,12 @@ struct SkippedFieldObj {
skipped: i32,
}
#[derive(GraphQLObject, Debug, PartialEq)]
#[graphql(rename = "none")]
struct NoRenameObj {
one_field: bool,
another_field: i32,
}
struct Context;
impl juniper::Context for Context {}
@ -123,6 +130,30 @@ impl Query {
skipped: 42,
}
}
fn no_rename_obj() -> NoRenameObj {
NoRenameObj {
one_field: true,
another_field: 146,
}
}
}
#[juniper::graphql_object(rename = "none")]
impl NoRenameQuery {
fn obj() -> Obj {
Obj {
regular_field: false,
c: 22,
}
}
fn no_rename_obj() -> NoRenameObj {
NoRenameObj {
one_field: true,
another_field: 146,
}
}
}
#[tokio::test]
@ -173,6 +204,98 @@ async fn test_doc_comment_override() {
.await;
}
#[tokio::test]
async fn test_no_rename_root() {
let doc = r#"
{
no_rename_obj {
one_field
another_field
}
obj {
regularField
}
}"#;
let schema = RootNode::new(
NoRenameQuery,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
assert_eq!(
execute(doc, None, &schema, &Variables::new(), &()).await,
Ok((
Value::object(
vec![
(
"no_rename_obj",
Value::object(
vec![
("one_field", Value::scalar(true)),
("another_field", Value::scalar(146)),
]
.into_iter()
.collect(),
),
),
(
"obj",
Value::object(
vec![("regularField", Value::scalar(false)),]
.into_iter()
.collect(),
),
)
]
.into_iter()
.collect()
),
vec![]
))
);
}
#[tokio::test]
async fn test_no_rename_obj() {
let doc = r#"
{
noRenameObj {
one_field
another_field
}
}"#;
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
assert_eq!(
execute(doc, None, &schema, &Variables::new(), &()).await,
Ok((
Value::object(
vec![(
"noRenameObj",
Value::object(
vec![
("one_field", Value::scalar(true)),
("another_field", Value::scalar(146)),
]
.into_iter()
.collect(),
),
)]
.into_iter()
.collect()
),
vec![]
))
);
}
#[tokio::test]
async fn test_derived_object() {
assert_eq!(

View file

@ -4,7 +4,7 @@ use syn::{ext::IdentExt, spanned::Spanned, Data, Fields};
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
util::{self, span_container::SpanContainer, RenameRule},
};
pub fn impl_enum(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Result<TokenStream> {
@ -48,7 +48,12 @@ pub fn impl_enum(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Result<Toke
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_upper_snake_case(&field_name.unraw().to_string()));
.unwrap_or_else(|| {
attrs
.rename
.unwrap_or(RenameRule::ScreamingSnakeCase)
.apply(&field_name.unraw().to_string())
});
let resolver_code = quote!( #ident::#field_name );

View file

@ -1,7 +1,7 @@
#![allow(clippy::match_wild_err_arm)]
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
util::{self, span_container::SpanContainer, RenameRule},
};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
@ -50,7 +50,10 @@ pub fn impl_input_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Res
let field_ident = field.ident.as_ref().unwrap();
let name = match field_attrs.name {
Some(ref name) => name.to_string(),
None => crate::util::to_camel_case(&field_ident.unraw().to_string()),
None => attrs
.rename
.unwrap_or(RenameRule::CamelCase)
.apply(&field_ident.unraw().to_string()),
};
if let Some(span) = field_attrs.skip {

View file

@ -1,6 +1,6 @@
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
util::{self, span_container::SpanContainer, RenameRule},
};
use proc_macro2::TokenStream;
use quote::quote;
@ -50,7 +50,12 @@ pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::R
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&field_name.unraw().to_string()));
.unwrap_or_else(|| {
attrs
.rename
.unwrap_or(RenameRule::CamelCase)
.apply(&field_name.unraw().to_string())
});
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {

View file

@ -2,7 +2,7 @@
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
util::{self, span_container::SpanContainer, RenameRule},
};
use proc_macro2::TokenStream;
use quote::quote;
@ -44,6 +44,8 @@ fn create(
.map(SpanContainer::into_inner)
.unwrap_or_else(|| _impl.type_ident.unraw().to_string());
let top_attrs = &_impl.attrs;
let fields = _impl
.methods
.iter()
@ -78,7 +80,12 @@ fn create(
let final_name = attrs
.argument(&arg_name)
.and_then(|attrs| attrs.rename.clone().map(|ident| ident.value()))
.unwrap_or_else(|| util::to_camel_case(&arg_name));
.unwrap_or_else(|| {
top_attrs
.rename
.unwrap_or(RenameRule::CamelCase)
.apply(&arg_name)
});
let expect_text = format!(
"Internal error: missing argument {} - validation must have failed",
@ -137,7 +144,12 @@ fn create(
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&ident.unraw().to_string()));
.unwrap_or_else(|| {
top_attrs
.rename
.unwrap_or(RenameRule::CamelCase)
.apply(&ident.unraw().to_string())
});
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = attrs.name {

View file

@ -5,6 +5,7 @@ pub mod parse_impl;
pub mod span_container;
use std::collections::HashMap;
use std::str::FromStr;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort;
@ -295,6 +296,40 @@ pub fn is_valid_name(field_name: &str) -> bool {
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
/// The different possible ways to change case of fields in a struct, or variants in an enum.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum RenameRule {
/// Don't apply a default rename rule.
None,
/// Rename to "camelCase" style.
CamelCase,
/// Rename to "SCREAMING_SNAKE_CASE" style
ScreamingSnakeCase,
}
impl RenameRule {
pub fn apply(&self, field: &str) -> String {
match self {
Self::None => field.to_owned(),
Self::CamelCase => to_camel_case(field),
Self::ScreamingSnakeCase => to_upper_snake_case(field),
}
}
}
impl FromStr for RenameRule {
type Err = ();
fn from_str(rule: &str) -> Result<Self, Self::Err> {
match rule {
"none" => Ok(Self::None),
"camelCase" => Ok(Self::CamelCase),
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
_ => Err(()),
}
}
}
#[derive(Default, Debug)]
pub struct ObjectAttributes {
pub name: Option<SpanContainer<String>>,
@ -304,6 +339,7 @@ pub struct ObjectAttributes {
pub interfaces: Vec<SpanContainer<syn::Type>>,
pub no_async: Option<SpanContainer<()>>,
pub is_internal: bool,
pub rename: Option<RenameRule>,
}
impl Parse for ObjectAttributes {
@ -365,6 +401,15 @@ impl Parse for ObjectAttributes {
"internal" => {
output.is_internal = true;
}
"rename" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?;
if let Ok(rename) = RenameRule::from_str(&val.value()) {
output.rename = Some(rename);
} else {
return Err(syn::Error::new(val.span(), "unknown rename rule"));
}
}
_ => {
return Err(syn::Error::new(ident.span(), "unknown attribute"));
}