Skip to content

Commit c964fd8

Browse files
authored
Allow disabling case conversion (#765)
1 parent 68210f5 commit c964fd8

File tree

8 files changed

+236
-9
lines changed

8 files changed

+236
-9
lines changed

integration_tests/juniper_tests/src/codegen/derive_enum.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ enum SomeEnum {
1616
Full,
1717
}
1818

19+
#[derive(juniper::GraphQLEnum, Debug, PartialEq)]
20+
#[graphql(rename = "none")]
21+
enum NoRenameEnum {
22+
OneVariant,
23+
AnotherVariant,
24+
}
25+
1926
/// Enum doc.
2027
#[derive(juniper::GraphQLEnum)]
2128
enum DocEnum {
@@ -64,6 +71,12 @@ fn test_derived_enum() {
6471
assert_eq!(meta.name(), Some("Some"));
6572
assert_eq!(meta.description(), Some(&"enum descr".to_string()));
6673

74+
// Test no rename variant.
75+
assert_eq!(
76+
<_ as ToInputValue>::to_input_value(&NoRenameEnum::AnotherVariant),
77+
InputValue::scalar("AnotherVariant")
78+
);
79+
6780
// Test Regular variant.
6881
assert_eq!(
6982
<_ as ToInputValue>::to_input_value(&SomeEnum::Regular),

integration_tests/juniper_tests/src/codegen/derive_input_object.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ struct Input {
2020
other: Option<bool>,
2121
}
2222

23+
#[derive(GraphQLInputObject, Debug, PartialEq)]
24+
#[graphql(rename = "none")]
25+
struct NoRenameInput {
26+
regular_field: String,
27+
}
28+
2329
/// Object comment.
2430
#[derive(GraphQLInputObject, Debug, PartialEq)]
2531
struct DocComment {
@@ -147,6 +153,21 @@ fn test_derived_input_object() {
147153
other: Some(true),
148154
}
149155
);
156+
157+
// Test disable renaming
158+
159+
let input: InputValue = ::serde_json::from_value(serde_json::json!({
160+
"regular_field": "hello",
161+
}))
162+
.unwrap();
163+
164+
let output: NoRenameInput = FromInputValue::from_input_value(&input).unwrap();
165+
assert_eq!(
166+
output,
167+
NoRenameInput {
168+
regular_field: "hello".into(),
169+
}
170+
);
150171
}
151172

152173
#[test]

integration_tests/juniper_tests/src/codegen/derive_object.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct Nested {
3232
}
3333

3434
struct Query;
35+
struct NoRenameQuery;
3536

3637
/// Object comment.
3738
#[derive(GraphQLObject, Debug, PartialEq)]
@@ -72,6 +73,12 @@ struct SkippedFieldObj {
7273
skipped: i32,
7374
}
7475

76+
#[derive(GraphQLObject, Debug, PartialEq)]
77+
#[graphql(rename = "none")]
78+
struct NoRenameObj {
79+
one_field: bool,
80+
another_field: i32,
81+
}
7582
struct Context;
7683
impl juniper::Context for Context {}
7784

@@ -123,6 +130,30 @@ impl Query {
123130
skipped: 42,
124131
}
125132
}
133+
134+
fn no_rename_obj() -> NoRenameObj {
135+
NoRenameObj {
136+
one_field: true,
137+
another_field: 146,
138+
}
139+
}
140+
}
141+
142+
#[juniper::graphql_object(rename = "none")]
143+
impl NoRenameQuery {
144+
fn obj() -> Obj {
145+
Obj {
146+
regular_field: false,
147+
c: 22,
148+
}
149+
}
150+
151+
fn no_rename_obj() -> NoRenameObj {
152+
NoRenameObj {
153+
one_field: true,
154+
another_field: 146,
155+
}
156+
}
126157
}
127158

128159
#[tokio::test]
@@ -173,6 +204,98 @@ async fn test_doc_comment_override() {
173204
.await;
174205
}
175206

207+
#[tokio::test]
208+
async fn test_no_rename_root() {
209+
let doc = r#"
210+
{
211+
no_rename_obj {
212+
one_field
213+
another_field
214+
}
215+
216+
obj {
217+
regularField
218+
}
219+
}"#;
220+
221+
let schema = RootNode::new(
222+
NoRenameQuery,
223+
EmptyMutation::<()>::new(),
224+
EmptySubscription::<()>::new(),
225+
);
226+
227+
assert_eq!(
228+
execute(doc, None, &schema, &Variables::new(), &()).await,
229+
Ok((
230+
Value::object(
231+
vec![
232+
(
233+
"no_rename_obj",
234+
Value::object(
235+
vec![
236+
("one_field", Value::scalar(true)),
237+
("another_field", Value::scalar(146)),
238+
]
239+
.into_iter()
240+
.collect(),
241+
),
242+
),
243+
(
244+
"obj",
245+
Value::object(
246+
vec![("regularField", Value::scalar(false)),]
247+
.into_iter()
248+
.collect(),
249+
),
250+
)
251+
]
252+
.into_iter()
253+
.collect()
254+
),
255+
vec![]
256+
))
257+
);
258+
}
259+
260+
#[tokio::test]
261+
async fn test_no_rename_obj() {
262+
let doc = r#"
263+
{
264+
noRenameObj {
265+
one_field
266+
another_field
267+
}
268+
}"#;
269+
270+
let schema = RootNode::new(
271+
Query,
272+
EmptyMutation::<()>::new(),
273+
EmptySubscription::<()>::new(),
274+
);
275+
276+
assert_eq!(
277+
execute(doc, None, &schema, &Variables::new(), &()).await,
278+
Ok((
279+
Value::object(
280+
vec![(
281+
"noRenameObj",
282+
Value::object(
283+
vec![
284+
("one_field", Value::scalar(true)),
285+
("another_field", Value::scalar(146)),
286+
]
287+
.into_iter()
288+
.collect(),
289+
),
290+
)]
291+
.into_iter()
292+
.collect()
293+
),
294+
vec![]
295+
))
296+
);
297+
}
298+
176299
#[tokio::test]
177300
async fn test_derived_object() {
178301
assert_eq!(

juniper_codegen/src/derive_enum.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use syn::{ext::IdentExt, spanned::Spanned, Data, Fields};
44

55
use crate::{
66
result::{GraphQLScope, UnsupportedAttribute},
7-
util::{self, span_container::SpanContainer},
7+
util::{self, span_container::SpanContainer, RenameRule},
88
};
99

1010
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
4848
.name
4949
.clone()
5050
.map(SpanContainer::into_inner)
51-
.unwrap_or_else(|| util::to_upper_snake_case(&field_name.unraw().to_string()));
51+
.unwrap_or_else(|| {
52+
attrs
53+
.rename
54+
.unwrap_or(RenameRule::ScreamingSnakeCase)
55+
.apply(&field_name.unraw().to_string())
56+
});
5257

5358
let resolver_code = quote!( #ident::#field_name );
5459

juniper_codegen/src/derive_input_object.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(clippy::match_wild_err_arm)]
22
use crate::{
33
result::{GraphQLScope, UnsupportedAttribute},
4-
util::{self, span_container::SpanContainer},
4+
util::{self, span_container::SpanContainer, RenameRule},
55
};
66
use proc_macro2::TokenStream;
77
use quote::{quote, ToTokens};
@@ -50,7 +50,10 @@ pub fn impl_input_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Res
5050
let field_ident = field.ident.as_ref().unwrap();
5151
let name = match field_attrs.name {
5252
Some(ref name) => name.to_string(),
53-
None => crate::util::to_camel_case(&field_ident.unraw().to_string()),
53+
None => attrs
54+
.rename
55+
.unwrap_or(RenameRule::CamelCase)
56+
.apply(&field_ident.unraw().to_string()),
5457
};
5558

5659
if let Some(span) = field_attrs.skip {

juniper_codegen/src/derive_object.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
result::{GraphQLScope, UnsupportedAttribute},
3-
util::{self, span_container::SpanContainer},
3+
util::{self, span_container::SpanContainer, RenameRule},
44
};
55
use proc_macro2::TokenStream;
66
use quote::quote;
@@ -50,7 +50,12 @@ pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::R
5050
.name
5151
.clone()
5252
.map(SpanContainer::into_inner)
53-
.unwrap_or_else(|| util::to_camel_case(&field_name.unraw().to_string()));
53+
.unwrap_or_else(|| {
54+
attrs
55+
.rename
56+
.unwrap_or(RenameRule::CamelCase)
57+
.apply(&field_name.unraw().to_string())
58+
});
5459

5560
if name.starts_with("__") {
5661
error.no_double_underscore(if let Some(name) = field_attrs.name {

juniper_codegen/src/impl_object.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use crate::{
44
result::{GraphQLScope, UnsupportedAttribute},
5-
util::{self, span_container::SpanContainer},
5+
util::{self, span_container::SpanContainer, RenameRule},
66
};
77
use proc_macro2::TokenStream;
88
use quote::quote;
@@ -44,6 +44,8 @@ fn create(
4444
.map(SpanContainer::into_inner)
4545
.unwrap_or_else(|| _impl.type_ident.unraw().to_string());
4646

47+
let top_attrs = &_impl.attrs;
48+
4749
let fields = _impl
4850
.methods
4951
.iter()
@@ -78,7 +80,12 @@ fn create(
7880
let final_name = attrs
7981
.argument(&arg_name)
8082
.and_then(|attrs| attrs.rename.clone().map(|ident| ident.value()))
81-
.unwrap_or_else(|| util::to_camel_case(&arg_name));
83+
.unwrap_or_else(|| {
84+
top_attrs
85+
.rename
86+
.unwrap_or(RenameRule::CamelCase)
87+
.apply(&arg_name)
88+
});
8289

8390
let expect_text = format!(
8491
"Internal error: missing argument {} - validation must have failed",
@@ -137,7 +144,12 @@ fn create(
137144
.name
138145
.clone()
139146
.map(SpanContainer::into_inner)
140-
.unwrap_or_else(|| util::to_camel_case(&ident.unraw().to_string()));
147+
.unwrap_or_else(|| {
148+
top_attrs
149+
.rename
150+
.unwrap_or(RenameRule::CamelCase)
151+
.apply(&ident.unraw().to_string())
152+
});
141153

142154
if name.starts_with("__") {
143155
error.no_double_underscore(if let Some(name) = attrs.name {

juniper_codegen/src/util/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod parse_impl;
55
pub mod span_container;
66

77
use std::collections::HashMap;
8+
use std::str::FromStr;
89

910
use proc_macro2::{Span, TokenStream};
1011
use proc_macro_error::abort;
@@ -295,6 +296,40 @@ pub fn is_valid_name(field_name: &str) -> bool {
295296
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
296297
}
297298

299+
/// The different possible ways to change case of fields in a struct, or variants in an enum.
300+
#[derive(Copy, Clone, PartialEq, Debug)]
301+
pub enum RenameRule {
302+
/// Don't apply a default rename rule.
303+
None,
304+
/// Rename to "camelCase" style.
305+
CamelCase,
306+
/// Rename to "SCREAMING_SNAKE_CASE" style
307+
ScreamingSnakeCase,
308+
}
309+
310+
impl RenameRule {
311+
pub fn apply(&self, field: &str) -> String {
312+
match self {
313+
Self::None => field.to_owned(),
314+
Self::CamelCase => to_camel_case(field),
315+
Self::ScreamingSnakeCase => to_upper_snake_case(field),
316+
}
317+
}
318+
}
319+
320+
impl FromStr for RenameRule {
321+
type Err = ();
322+
323+
fn from_str(rule: &str) -> Result<Self, Self::Err> {
324+
match rule {
325+
"none" => Ok(Self::None),
326+
"camelCase" => Ok(Self::CamelCase),
327+
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
328+
_ => Err(()),
329+
}
330+
}
331+
}
332+
298333
#[derive(Default, Debug)]
299334
pub struct ObjectAttributes {
300335
pub name: Option<SpanContainer<String>>,
@@ -304,6 +339,7 @@ pub struct ObjectAttributes {
304339
pub interfaces: Vec<SpanContainer<syn::Type>>,
305340
pub no_async: Option<SpanContainer<()>>,
306341
pub is_internal: bool,
342+
pub rename: Option<RenameRule>,
307343
}
308344

309345
impl Parse for ObjectAttributes {
@@ -365,6 +401,15 @@ impl Parse for ObjectAttributes {
365401
"internal" => {
366402
output.is_internal = true;
367403
}
404+
"rename" => {
405+
input.parse::<syn::Token![=]>()?;
406+
let val = input.parse::<syn::LitStr>()?;
407+
if let Ok(rename) = RenameRule::from_str(&val.value()) {
408+
output.rename = Some(rename);
409+
} else {
410+
return Err(syn::Error::new(val.span(), "unknown rename rule"));
411+
}
412+
}
368413
_ => {
369414
return Err(syn::Error::new(ident.span(), "unknown attribute"));
370415
}

0 commit comments

Comments
 (0)