From 9080448da23a991568062541cb3250ee7d261a42 Mon Sep 17 00:00:00 2001
From: piperRyan <35411044+piperRyan@users.noreply.github.com>
Date: Wed, 23 May 2018 01:25:20 -0600
Subject: [PATCH] Add Compile Time Check For "Invalid" Names (#170)

---
 changelog/master.md                        |  8 ++++++--
 juniper_codegen/Cargo.toml                 |  2 ++
 juniper_codegen/src/derive_enum.rs         | 18 ++++++++++++++----
 juniper_codegen/src/derive_input_object.rs | 18 ++++++++++++++----
 juniper_codegen/src/derive_object.rs       | 18 ++++++++++++++----
 juniper_codegen/src/lib.rs                 |  3 +++
 juniper_codegen/src/util.rs                | 22 ++++++++++++++++++++++
 juniper_tests/src/codegen/derive_enum.rs   |  7 +++----
 8 files changed, 78 insertions(+), 18 deletions(-)

diff --git a/changelog/master.md b/changelog/master.md
index 5aa87418..d64d5c74 100644
--- a/changelog/master.md
+++ b/changelog/master.md
@@ -7,6 +7,11 @@
 
   [#151](https://github.com/graphql-rust/juniper/pull/151)
 
+* The `GraphQLObject`, `GraphQLInputObject`, and `GraphQLEnum` custom derives will reject
+  invalid [names](http://facebook.github.io/graphql/October2016/#Name) at compile time. 
+
+  [#170](https://github.com/graphql-rust/juniper/pull/170)
+
 * Large integers (> signed 32bit) are now deserialized as floats. Previously,
   they produced the "integer out of range" error. For languages that do not
   have distinction between integer and floating point types (including
@@ -14,5 +19,4 @@
   fractional part could not be decoded (because they are represented without
   a decimal part `.0`).
 
-  [#179](https://github.com/graphql-rust/juniper/pull/179)
-
+  [#179](https://github.com/graphql-rust/juniper/pull/179)
\ No newline at end of file
diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml
index 13dbd08d..0695bc8f 100644
--- a/juniper_codegen/Cargo.toml
+++ b/juniper_codegen/Cargo.toml
@@ -16,6 +16,8 @@ proc-macro = true
 [dependencies]
 syn = { version = "0.13.*", features = ["full", "extra-traits"] }
 quote = "0.5.*"
+regex = "0.2.10"
+lazy_static = "1.0.0"
 
 [badges]
 travis-ci = { repository = "graphql-rust/juniper" }
diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs
index 83b9bf78..90ad866b 100644
--- a/juniper_codegen/src/derive_enum.rs
+++ b/juniper_codegen/src/derive_enum.rs
@@ -32,8 +32,13 @@ impl EnumAttrs {
         if let Some(items) = get_graphl_attr(&input.attrs) {
             for item in items {
                 if let Some(val) = keyed_item_value(&item, "name", true) {
-                    res.name = Some(val);
-                    continue;
+                    if is_valid_name(&*val) {
+                        res.name = Some(val);
+                        continue;
+                    } else {
+                        panic!("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
+                               &*val);
+                    }
                 }
                 if let Some(val) = keyed_item_value(&item, "description", true) {
                     res.description = Some(val);
@@ -73,8 +78,13 @@ impl EnumVariantAttrs {
         if let Some(items) = get_graphl_attr(&variant.attrs) {
             for item in items {
                 if let Some(val) = keyed_item_value(&item, "name", true) {
-                    res.name = Some(val);
-                    continue;
+                    if is_valid_name(&*val) {
+                        res.name = Some(val);
+                        continue;
+                    } else {
+                         panic!("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
+                                 &*val);
+                    }
                 }
                 if let Some(val) = keyed_item_value(&item, "description", true) {
                     res.description = Some(val);
diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs
index f645a3ec..c0f30eed 100644
--- a/juniper_codegen/src/derive_input_object.rs
+++ b/juniper_codegen/src/derive_input_object.rs
@@ -29,8 +29,13 @@ impl ObjAttrs {
         if let Some(items) = get_graphl_attr(&input.attrs) {
             for item in items {
                 if let Some(val) = keyed_item_value(&item, "name", true) {
-                    res.name = Some(val);
-                    continue;
+                    if is_valid_name(&*val) {
+                        res.name = Some(val);
+                        continue;
+                    } else {
+                         panic!("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
+                                 &*val);
+                    }
                 }
                 if let Some(val) = keyed_item_value(&item, "description", true) {
                     res.description = Some(val);
@@ -71,8 +76,13 @@ impl ObjFieldAttrs {
         if let Some(items) = get_graphl_attr(&variant.attrs) {
             for item in items {
                 if let Some(val) = keyed_item_value(&item, "name", true) {
-                    res.name = Some(val);
-                    continue;
+                    if is_valid_name(&*val) {
+                        res.name = Some(val);
+                        continue;
+                    } else {
+                         panic!("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
+                                 &*val);
+                    }
                 }
                 if let Some(val) = keyed_item_value(&item, "description", true) {
                     res.description = Some(val);
diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs
index 67133201..78417b60 100644
--- a/juniper_codegen/src/derive_object.rs
+++ b/juniper_codegen/src/derive_object.rs
@@ -23,8 +23,13 @@ impl ObjAttrs {
         if let Some(items) = get_graphl_attr(&input.attrs) {
             for item in items {
                 if let Some(val) = keyed_item_value(&item, "name", true) {
-                    res.name = Some(val);
-                    continue;
+                    if is_valid_name(&*val) {
+                        res.name = Some(val);
+                        continue;
+                    } else {
+                         panic!("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
+                                 &*val);
+                    }
                 }
                 if let Some(val) = keyed_item_value(&item, "description", true) {
                     res.description = Some(val);
@@ -55,8 +60,13 @@ impl ObjFieldAttrs {
         if let Some(items) = get_graphl_attr(&variant.attrs) {
             for item in items {
                 if let Some(val) = keyed_item_value(&item, "name", true) {
-                    res.name = Some(val);
-                    continue;
+                    if is_valid_name(&*val) {
+                        res.name = Some(val);
+                        continue;
+                    } else {
+                         panic!("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
+                                 &*val);
+                    }
                 }
                 if let Some(val) = keyed_item_value(&item, "description", true) {
                     res.description = Some(val);
diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs
index 55d7045f..97b6ddfd 100644
--- a/juniper_codegen/src/lib.rs
+++ b/juniper_codegen/src/lib.rs
@@ -10,6 +10,9 @@ extern crate proc_macro;
 #[macro_use]
 extern crate quote;
 extern crate syn;
+#[macro_use]
+extern crate lazy_static;
+extern crate regex;
 
 mod util;
 mod derive_enum;
diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util.rs
index ac1afefd..21f4fc66 100644
--- a/juniper_codegen/src/util.rs
+++ b/juniper_codegen/src/util.rs
@@ -4,6 +4,7 @@ use syn::{
     NestedMeta,
     Lit,
 };
+use regex::Regex;
 
 // Get the nested items of a a #[graphql(...)] attribute.
 pub fn get_graphl_attr(attrs: &Vec<Attribute>) -> Option<Vec<NestedMeta>> {
@@ -111,3 +112,24 @@ fn test_to_upper_snake_case() {
     assert_eq!(to_upper_snake_case("someINpuT"), "SOME_INPU_T");
     assert_eq!(to_upper_snake_case("some_INpuT"), "SOME_INPU_T");
 }
+
+#[doc(hidden)]
+pub fn is_valid_name(field_name: &str) -> bool {
+    lazy_static!{
+        static ref CAMELCASE: Regex = Regex::new("^[_A-Za-z][_0-9A-Za-z]*$").unwrap();
+    }
+    CAMELCASE.is_match(field_name)
+}
+
+#[test]
+fn test_is_valid_name(){
+    assert_eq!(is_valid_name("yesItIs"), true);
+    assert_eq!(is_valid_name("NoitIsnt"), true); 
+    assert_eq!(is_valid_name("iso6301"), true); 
+    assert_eq!(is_valid_name("thisIsATest"), true); 
+    assert_eq!(is_valid_name("i6Op"), true);
+    assert_eq!(is_valid_name("i!"), false);
+    assert_eq!(is_valid_name(""), false);   
+    assert_eq!(is_valid_name("aTest"), true);
+    assert_eq!(is_valid_name("__Atest90"), true);
+}
diff --git a/juniper_tests/src/codegen/derive_enum.rs b/juniper_tests/src/codegen/derive_enum.rs
index 6277ce09..2e1ea626 100644
--- a/juniper_tests/src/codegen/derive_enum.rs
+++ b/juniper_tests/src/codegen/derive_enum.rs
@@ -8,8 +8,7 @@ use juniper::{self, FromInputValue, GraphQLType, InputValue, ToInputValue};
 #[graphql(name = "Some", description = "enum descr")]
 enum SomeEnum {
     Regular,
-
-    #[graphql(name = "full", description = "field descr", deprecated = "depr")] Full,
+    #[graphql(name = "FULL", description = "field descr", deprecated = "depr")] Full,
 }
 
 #[test]
@@ -37,10 +36,10 @@ fn test_derived_enum() {
     // Test FULL variant.
     assert_eq!(
         SomeEnum::Full.to_input_value(),
-        InputValue::String("full".into())
+        InputValue::String("FULL".into())
     );
     assert_eq!(
-        FromInputValue::from_input_value(&InputValue::String("full".into())),
+        FromInputValue::from_input_value(&InputValue::String("FULL".into())),
         Some(SomeEnum::Full)
     );
 }