From 141292ad914e05016a897c87e91e628cdae5ed22 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 02:27:41 +0100 Subject: [PATCH 01/26] Export codegen from main juniper crate Use a trick for re-exporting proc-macros to supply juniper_derive as a dependency of juniper. Users will now just have to depend on juniper directly. --- juniper/Cargo.toml | 13 +++++++++++-- juniper/src/lib.rs | 11 +++++++++++ juniper_codegen/Cargo.toml | 4 ++-- juniper_tests/Cargo.toml | 3 +-- juniper_tests/src/lib.rs | 2 -- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 7450bd9b..689b6a68 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -20,16 +20,25 @@ harness = false path = "benches/bench.rs" [features] +codegen = ["juniper_codegen"] nightly = [] expose-test-schema = [] -default = ["chrono", "url", "uuid"] +default = [ + "codegen", + "chrono", + "url", + "uuid" +] [dependencies] +juniper_codegen = { version = "0.8.1", path = "../juniper_codegen" , optional = true} + fnv = "1.0.3" -chrono = { version = "^0.4.0", optional = true } ordermap = { version = "^0.2.11", features = ["serde-1"] } serde = { version = "^1.0.8" } serde_derive = {version="^1.0.8" } + +chrono = { version = "^0.4.0", optional = true } serde_json = { version="^1.0.2", optional = true } url = { version = "^1.5.1", optional = true } uuid = { version = "0.5.1", optional = true } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index da27c845..dfbe8965 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -136,6 +136,17 @@ extern crate url; #[cfg(any(test, feature = "uuid"))] extern crate uuid; +// If the "codegen" feature is enabled, depend on juniper_codegen and re-export everything in it. +// This allows users to just depend on juniper and get the derive funcationality automatically. +#[cfg(feature = "codegen")] +#[allow(unused_imports)] +#[macro_use] +extern crate juniper_codegen; + +#[cfg(feature = "codegen")] +#[doc(hidden)] +pub use juniper_codegen::*; + use std::borrow::Cow; #[macro_use] diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index b7b13fa8..bb235925 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -11,8 +11,8 @@ repository = "https://github.com/graphql-rust/juniper" proc-macro = true [dependencies] -syn = "0.11.11" -quote = "0.3.15" +syn = "0.11" +quote = "0.3" [badges] travis-ci = { repository = "graphql-rust/juniper" } diff --git a/juniper_tests/Cargo.toml b/juniper_tests/Cargo.toml index dcfa9f1c..7b87fa78 100644 --- a/juniper_tests/Cargo.toml +++ b/juniper_tests/Cargo.toml @@ -4,8 +4,7 @@ version = "0.1.0" [dependencies] juniper = { path = "../juniper" } -juniper_codegen = { path = "../juniper_codegen" } -serde_json = { version = "^1.0.2" } +serde_json = { version = "1" } [dev-dependencies] fnv = "1.0.3" diff --git a/juniper_tests/src/lib.rs b/juniper_tests/src/lib.rs index 6f0a6e98..c4fc104c 100644 --- a/juniper_tests/src/lib.rs +++ b/juniper_tests/src/lib.rs @@ -1,7 +1,5 @@ #[macro_use] extern crate juniper; -#[macro_use] -extern crate juniper_codegen; extern crate serde_json; #[cfg(test)] From 3750349e71a69973075f07e14452d474e12edb90 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 02:31:56 +0100 Subject: [PATCH 02/26] Bump version to 0.9.0 --- juniper/Cargo.toml | 20 ++++++++++---------- juniper_codegen/Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 689b6a68..e51a2932 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "juniper" -version = "0.8.1" +version = "0.9.0" authors = ["Magnus Hallin "] description = "GraphQL server library" license = "BSD-2-Clause" @@ -31,18 +31,18 @@ default = [ ] [dependencies] -juniper_codegen = { version = "0.8.1", path = "../juniper_codegen" , optional = true} +juniper_codegen = { version = "0.9.0", path = "../juniper_codegen" , optional = true} fnv = "1.0.3" -ordermap = { version = "^0.2.11", features = ["serde-1"] } -serde = { version = "^1.0.8" } -serde_derive = {version="^1.0.8" } +ordermap = { version = "0.2.11", features = ["serde-1"] } +serde = { version = "1.0.8" } +serde_derive = {version="1.0.8" } -chrono = { version = "^0.4.0", optional = true } -serde_json = { version="^1.0.2", optional = true } -url = { version = "^1.5.1", optional = true } +chrono = { version = "0.4.0", optional = true } +serde_json = { version="1.0.2", optional = true } +url = { version = "1.5.1", optional = true } uuid = { version = "0.5.1", optional = true } [dev-dependencies] -bencher = "^0.1.2" -serde_json = { version = "^1.0.2" } +bencher = "0.1.2" +serde_json = { version = "1.0.2" } diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index bb235925..756f9e85 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "juniper_codegen" -version = "0.8.1" +version = "0.9.0" authors = ["Magnus Hallin "] description = "Internal custom derive trait for Juniper GraphQL" license = "BSD-2-Clause" From 7b066ef1f90f7af1eb23234a5c129a3afecb8bc0 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 02:35:08 +0100 Subject: [PATCH 03/26] (docs) Add short crate docs to codegen. --- juniper_codegen/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index e157445e..301db226 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -1,3 +1,9 @@ +//! This crate supplies custom derive implementations for the +//! [juniper](https://github.com/graphql-rust/juniper) crate. +//! +//! You should not depend on juniper_codegen directly. +//! You only need the `juniper` crate. + #![recursion_limit = "1024"] extern crate proc_macro; From a7b6056b370982148141802a221c3d683000c097 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 03:12:47 +0100 Subject: [PATCH 04/26] (test) Add a nested object test for derive(GraphQLObject) --- juniper_tests/src/codegen/derive_object.rs | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/juniper_tests/src/codegen/derive_object.rs b/juniper_tests/src/codegen/derive_object.rs index 84a9580b..bc0428bc 100644 --- a/juniper_tests/src/codegen/derive_object.rs +++ b/juniper_tests/src/codegen/derive_object.rs @@ -12,6 +12,11 @@ struct Obj { c: i32, } +#[derive(GraphQLObject, Debug, PartialEq)] +struct Nested { + obj: Obj, +} + struct Query; graphql_object!(Query: () |&self| { @@ -21,6 +26,15 @@ graphql_object!(Query: () |&self| { c: 22, } } + + field nested() -> Nested { + Nested{ + obj: Obj{ + regular_field: false, + c: 333, + } + } + } }); #[test] @@ -54,3 +68,30 @@ fn test_derived_object() { ].into_iter().collect()), vec![]))); } + +#[test] +fn test_derived_object_nested() { + let doc = r#" + { + nested { + obj { + regularField + renamedField + } + } + }"#; + + let schema = RootNode::new(Query, EmptyMutation::<()>::new()); + + assert_eq!( + execute(doc, None, &schema, &Variables::new(), &()), + Ok((Value::object(vec![ + ("nested", Value::object(vec![ + ("obj", Value::object(vec![ + ("regularField", Value::boolean(false)), + ("renamedField", Value::int(333)), + ].into_iter().collect()) + )].into_iter().collect())), + ].into_iter().collect()), + vec![]))); +} From f3626c57c60376d5b9f2ab85cd8dc851f592c293 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 03:36:07 +0100 Subject: [PATCH 05/26] Move to_camel_case helper to util module. --- juniper/src/lib.rs | 57 +++++++++------------------------------------ juniper/src/util.rs | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 juniper/src/util.rs diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index dfbe8965..3e8610ed 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -14,24 +14,22 @@ schema, as well as an optional integration into the [Iron framework][Iron] and [Rocket]. It tries to keep the number of dynamic operations to a minimum, and give you as the schema developer the control of the query execution path. -Juniper only depends on `serde` and `serde_derive` by default, making it -lightweight and easy to drop into any project. - ## Exposing data types The `GraphQLType` trait is the primary interface towards application developers. -By deriving this trait, you can expose your types as either objects, enums, +By implementing this trait, you can expose your types as either objects, enums, interfaces, unions, or scalars. -However, due to the dynamic nature of GraphQL's type system, deriving this trait +However, due to the dynamic nature of GraphQL's type system, doing this manually is a bit tedious, especially in order to do it in a fully type safe -manner. To help with this task, this library provides a couple of macros; the -most common one being `graphql_object!`. Use this macro to expose your already -existing object types as GraphQL objects: +manner. + +The library provides two methods of mapping your Rust data types to GraphQL schemas: custom derive +implementations and macros. ```rust -#[macro_use] extern crate juniper; # use std::collections::HashMap; +# #[macro_use] extern crate juniper; use juniper::{Context, FieldResult}; struct User { id: String, name: String, friend_ids: Vec } @@ -147,7 +145,6 @@ extern crate juniper_codegen; #[doc(hidden)] pub use juniper_codegen::*; -use std::borrow::Cow; #[macro_use] mod value; @@ -158,6 +155,7 @@ pub mod parser; mod types; mod schema; mod validation; +mod util; mod executor; // This needs to be public until docs have support for private modules: // https://github.com/rust-lang/cargo/issues/1520 @@ -175,6 +173,9 @@ pub mod tests; #[cfg(test)] mod executor_tests; +// Needs to be public because macros use it. +pub use util::to_camel_case; + use parser::{parse_document_source, ParseError, Spanning}; use validation::{validate_input_values, visit_all_rules, ValidatorContext}; use executor::execute_validated_query; @@ -243,40 +244,4 @@ impl<'a> From>> for GraphQLError<'a> { } } -#[doc(hidden)] -pub fn to_camel_case<'a>(s: &'a str) -> Cow<'a, str> { - let mut dest = Cow::Borrowed(s); - for (i, part) in s.split('_').enumerate() { - if i > 0 && part.len() == 1 { - dest += Cow::Owned(part.to_uppercase()); - } else if i > 0 && part.len() > 1 { - let first = part.chars() - .next() - .unwrap() - .to_uppercase() - .collect::(); - let second = &part[1..]; - - dest += Cow::Owned(first); - dest += second; - } else if i == 0 { - dest = Cow::Borrowed(part); - } - } - - dest -} - -#[test] -fn test_to_camel_case() { - assert_eq!(&to_camel_case("test")[..], "test"); - assert_eq!(&to_camel_case("_test")[..], "Test"); - assert_eq!(&to_camel_case("first_second")[..], "firstSecond"); - assert_eq!(&to_camel_case("first_")[..], "first"); - assert_eq!(&to_camel_case("a_b_c")[..], "aBC"); - assert_eq!(&to_camel_case("a_bc")[..], "aBc"); - assert_eq!(&to_camel_case("a_b")[..], "aB"); - assert_eq!(&to_camel_case("a")[..], "a"); - assert_eq!(&to_camel_case("")[..], ""); -} diff --git a/juniper/src/util.rs b/juniper/src/util.rs new file mode 100644 index 00000000..972f1b25 --- /dev/null +++ b/juniper/src/util.rs @@ -0,0 +1,42 @@ +use std::borrow::Cow; + +/// Convert string to camel case. +/// +/// Note: needs to be public because several macros use it. +#[doc(hidden)] +pub fn to_camel_case<'a>(s: &'a str) -> Cow<'a, str> { + let mut dest = Cow::Borrowed(s); + + for (i, part) in s.split('_').enumerate() { + if i > 0 && part.len() == 1 { + dest += Cow::Owned(part.to_uppercase()); + } else if i > 0 && part.len() > 1 { + let first = part.chars() + .next() + .unwrap() + .to_uppercase() + .collect::(); + let second = &part[1..]; + + dest += Cow::Owned(first); + dest += second; + } else if i == 0 { + dest = Cow::Borrowed(part); + } + } + + dest +} + +#[test] +fn test_to_camel_case() { + assert_eq!(&to_camel_case("test")[..], "test"); + assert_eq!(&to_camel_case("_test")[..], "Test"); + assert_eq!(&to_camel_case("first_second")[..], "firstSecond"); + assert_eq!(&to_camel_case("first_")[..], "first"); + assert_eq!(&to_camel_case("a_b_c")[..], "aBC"); + assert_eq!(&to_camel_case("a_bc")[..], "aBc"); + assert_eq!(&to_camel_case("a_b")[..], "aB"); + assert_eq!(&to_camel_case("a")[..], "a"); + assert_eq!(&to_camel_case("")[..], ""); +} From 40d8f905e9660123ac85a49dfc4d23fee455d69b Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 03:51:28 +0100 Subject: [PATCH 06/26] Move executor.rs in subfolder Preparation for splitting up executor.rs --- juniper/src/{executor.rs => executor/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename juniper/src/{executor.rs => executor/mod.rs} (100%) diff --git a/juniper/src/executor.rs b/juniper/src/executor/mod.rs similarity index 100% rename from juniper/src/executor.rs rename to juniper/src/executor/mod.rs From 709231dc46d0bc49e260fc8144a276d5c6d95425 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 03:55:14 +0100 Subject: [PATCH 07/26] Make juniper_codegen dependency non-optional. --- juniper/Cargo.toml | 4 +--- juniper/src/lib.rs | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index e51a2932..6c28d2b0 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -20,18 +20,16 @@ harness = false path = "benches/bench.rs" [features] -codegen = ["juniper_codegen"] nightly = [] expose-test-schema = [] default = [ - "codegen", "chrono", "url", "uuid" ] [dependencies] -juniper_codegen = { version = "0.9.0", path = "../juniper_codegen" , optional = true} +juniper_codegen = { version = "0.9.0", path = "../juniper_codegen" } fnv = "1.0.3" ordermap = { version = "0.2.11", features = ["serde-1"] } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 3e8610ed..e034d3c7 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -134,18 +134,14 @@ extern crate url; #[cfg(any(test, feature = "uuid"))] extern crate uuid; -// If the "codegen" feature is enabled, depend on juniper_codegen and re-export everything in it. -// This allows users to just depend on juniper and get the derive funcationality automatically. -#[cfg(feature = "codegen")] +// Depend on juniper_codegen and re-export everything in it. +// This allows users to just depend on juniper and get the derive functionality automatically. #[allow(unused_imports)] #[macro_use] extern crate juniper_codegen; - -#[cfg(feature = "codegen")] #[doc(hidden)] pub use juniper_codegen::*; - #[macro_use] mod value; #[macro_use] From b1a62d68e9e867221eb2f96618d3c1ecf542cbe9 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 05:00:20 +0100 Subject: [PATCH 08/26] (tests) Fix derive_enum test to actually test custom names --- juniper_tests/src/codegen/derive_enum.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/juniper_tests/src/codegen/derive_enum.rs b/juniper_tests/src/codegen/derive_enum.rs index b0f02363..ac31a53a 100644 --- a/juniper_tests/src/codegen/derive_enum.rs +++ b/juniper_tests/src/codegen/derive_enum.rs @@ -9,7 +9,7 @@ use juniper::{self, FromInputValue, GraphQLType, InputValue, ToInputValue}; enum SomeEnum { Regular, - #[graphql(name = "FULL", description = "field descr", deprecated = "depr")] + #[graphql(name = "full", description = "field descr", deprecated = "depr")] Full, } @@ -33,9 +33,9 @@ fn test_derived_enum() { ); // Test FULL variant. - assert_eq!(SomeEnum::Full.to_input_value(), InputValue::String("FULL".into())); + assert_eq!(SomeEnum::Full.to_input_value(), 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) ); } From f858f416b8e188e7366ba8cee48499a62a17d8f4 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 12:42:07 +0100 Subject: [PATCH 09/26] Remove graphql_enum! macro in favor of custom derive * Extend derive for enums to allow deriving inside the juniper crate itself. Note: this is a rather ugly hack right now, FIXME is in the code * Remove the graphql_enum! macro and replace all internal use with derive * Refactor introspection tests to use derive --- juniper/src/ast.rs | 6 +- juniper/src/executor_tests/enums.rs | 9 +- juniper/src/executor_tests/introspection.rs | 7 +- juniper/src/macros/enums.rs | 193 -------------------- juniper/src/macros/mod.rs | 2 - juniper/src/macros/tests/enums.rs | 94 +++++----- juniper/src/macros/tests/mod.rs | 2 +- juniper/src/schema/model.rs | 6 +- juniper/src/schema/schema.rs | 19 -- juniper/src/tests/model.rs | 4 +- juniper/src/tests/schema.rs | 6 - juniper/src/types/base.rs | 6 +- juniper_codegen/src/derive_enum.rs | 78 ++++++-- 13 files changed, 127 insertions(+), 305 deletions(-) delete mode 100644 juniper/src/macros/enums.rs diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index 38b9ece9..1ae53929 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -150,9 +150,9 @@ pub type Document<'a> = Vec>; /// Parse an unstructured input value into a Rust data type. /// /// The conversion _can_ fail, and must in that case return None. Implemented -/// automatically by the convenience macros `graphql_enum!` and -/// `graphql_scalar!`. Must be implemented manually when manually exposing new -/// enums or scalars. +/// automatically by the convenience macro `graphql_scalar!` or by deriving GraphQLEnum. +/// +/// Must be implemented manually when manually exposing new enums or scalars. pub trait FromInputValue: Sized { /// Performs the conversion. fn from_input_value(v: &InputValue) -> Option; diff --git a/juniper/src/executor_tests/enums.rs b/juniper/src/executor_tests/enums.rs index 5081bf48..5258edfa 100644 --- a/juniper/src/executor_tests/enums.rs +++ b/juniper/src/executor_tests/enums.rs @@ -9,7 +9,8 @@ use validation::RuleError; use parser::SourcePosition; use types::scalars::EmptyMutation; -#[derive(Debug)] +#[derive(GraphQLEnum, Debug)] +#[graphql(_internal)] enum Color { Red, Green, @@ -17,12 +18,6 @@ enum Color { } struct TestType; -graphql_enum!(Color { - Color::Red => "RED", - Color::Green => "GREEN", - Color::Blue => "BLUE", -}); - graphql_object!(TestType: () |&self| { field to_string(color: Color) -> String { format!("Color::{:?}", color) diff --git a/juniper/src/executor_tests/introspection.rs b/juniper/src/executor_tests/introspection.rs index 0832654b..1480ca17 100644 --- a/juniper/src/executor_tests/introspection.rs +++ b/juniper/src/executor_tests/introspection.rs @@ -3,6 +3,8 @@ use value::Value; use schema::model::RootNode; use types::scalars::EmptyMutation; +#[derive(GraphQLEnum)] +#[graphql(name = "SampleEnum", _internal)] enum Sample { One, Two, @@ -24,11 +26,6 @@ graphql_scalar!(Scalar as "SampleScalar" { } }); -graphql_enum!(Sample as "SampleEnum" { - Sample::One => "ONE", - Sample::Two => "TWO", -}); - graphql_interface!(Interface: () as "SampleInterface" |&self| { description: "A sample interface" diff --git a/juniper/src/macros/enums.rs b/juniper/src/macros/enums.rs deleted file mode 100644 index daa1cbfa..00000000 --- a/juniper/src/macros/enums.rs +++ /dev/null @@ -1,193 +0,0 @@ -/** -Expose simple enums - -GraphQL enums are similar to enums classes C++ - more like grouped constants -with type safety than what Rust enums offer. This macro can be used to export -non-data carrying Rust enums to GraphQL: - -```rust -# #[macro_use] extern crate juniper; -enum Color { - Red, - Orange, - Green, - Blue, - Black, -} - -graphql_enum!(Color { - Color::Red => "RED" as "The color red", - Color::Orange => "ORANGE", - Color::Green => "GREEN", - Color::Blue => "BLUE", - Color::Black => "BLACK" deprecated "Superseded by ORANGE", -}); - -# fn main() { } -``` - -The macro expands to a `match` statement which will result in a compilation -error if not all enum variants are covered. It also creates an implementation -for `FromInputValue` and `ToInputValue`, making it usable in arguments and -default values. - -If you want to expose the enum under a different name than the Rust type, -you can write `graphql_enum!(Color as "MyColor" { ...`. - -*/ -#[macro_export] -macro_rules! graphql_enum { - ( @as_expr, $e:expr) => { $e }; - ( @as_pattern, $p:pat) => { $p }; - ( @as_path, $p:path) => { $p }; - - // Calls $val.$func($arg) if $arg is not None - ( @maybe_apply, None, $func:ident, $val:expr ) => { $val }; - ( @maybe_apply, $arg:tt, $func:ident, $val:expr ) => { $val.$func($arg) }; - - // Each of the @parse match arms accumulates data up to a call to @generate. - // - // ( $name, $outname, $descr ): the name of the Rust enum, the name of the - // GraphQL enum (as a string), and the description of the enum (as a string or None) - // - // [ ( $eval, $ename, $edescr, $edepr ) , ] the value of the Rust enum, - // the value of the GraphQL enum (as a string), the description of the enum - // value (as a string or None), and the deprecation reason of the enum value - // (as a string or None). - ( - @generate, - ( $name:path, $outname:tt, $descr:tt ), - [ $( ( $eval:tt, $ename:tt, $edescr:tt, $edepr:tt ) , )* ] - ) => { - impl $crate::GraphQLType for $name { - type Context = (); - type TypeInfo = (); - - fn name(_: &()) -> Option<&str> { - Some(graphql_enum!(@as_expr, $outname)) - } - - fn meta<'r>(info: &(), registry: &mut $crate::Registry<'r>) -> $crate::meta::MetaType<'r> { - graphql_enum!( - @maybe_apply, $descr, description, - registry.build_enum_type::<$name>(info, &[ - $( - graphql_enum!( - @maybe_apply, - $edepr, deprecated, - graphql_enum!( - @maybe_apply, - $edescr, description, - $crate::meta::EnumValue::new(graphql_enum!(@as_expr, $ename)))) - ),* - ])) - .into_meta() - } - - fn resolve(&self, _: &(), _: Option<&[$crate::Selection]>, _: &$crate::Executor) -> $crate::Value { - match *self { - $( - graphql_enum!(@as_pattern, $eval) => - $crate::Value::string(graphql_enum!(@as_expr, $ename)) ),* - } - } - } - - impl $crate::FromInputValue for $name { - fn from_input_value(v: &$crate::InputValue) -> Option<$name> { - match v.as_enum_value().or_else(|| v.as_string_value()) { - $( - Some(graphql_enum!(@as_pattern, $ename)) - => Some(graphql_enum!(@as_expr, $eval)), )* - _ => None, - } - } - } - - impl $crate::ToInputValue for $name { - fn to_input_value(&self) -> $crate::InputValue { - match *self { - $( - graphql_enum!(@as_pattern, $eval) => - $crate::InputValue::string(graphql_enum!(@as_expr, $ename)) ),* - } - } - } - }; - - // No more items to parse - ( @parse, $meta:tt, $acc:tt, ) => { - graphql_enum!( @generate, $meta, $acc ); - }; - - // Remove extraneous commas - ( @parse, $meta:tt, $acc:tt, , $($rest:tt)* ) => { - graphql_enum!( @parse, $meta, $acc, $($rest)* ); - }; - - // description: - ( - @parse, - ( $name:tt, $outname:tt, $_ignore:tt ), - $acc:tt, - description: $descr:tt $($items:tt)* - ) => { - graphql_enum!( @parse, ( $name, $outname, $descr ), $acc, $($items)* ); - }; - - // RustEnumValue => "GraphQL enum value" deprecated - ( - @parse, - $meta:tt, - [ $($acc:tt ,)* ], - $eval:path => $ename:tt deprecated $depr:tt $($rest:tt)* - ) => { - graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, None, $depr ), ], $($rest)* ); - }; - - // RustEnumValue => "GraphQL enum value" as deprecated - ( - @parse, - $meta:tt, - [ $($acc:tt ,)* ], - $eval:path => $ename:tt as $descr:tt deprecated $depr:tt $($rest:tt)* - ) => { - graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, $descr, $depr ), ], $($rest)* ); - }; - - // RustEnumValue => "GraphQL enum value" as - ( - @parse, - $meta:tt, - [ $($acc:tt ,)* ], - $eval:path => $ename:tt as $descr:tt $($rest:tt)* - ) => { - graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, $descr, None ), ], $($rest)* ); - }; - - // RustEnumValue => "GraphQL enum value" - ( - @parse, - $meta:tt, - [ $($acc:tt ,)* ], - $eval:path => $ename:tt $($rest:tt)* - ) => { - graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval , $ename , None , None ), ], $($rest)* ); - }; - - // Entry point: - // RustEnumName as "GraphQLEnumName" { ... } - ( - $name:path as $outname:tt { $($items:tt)* } - ) => { - graphql_enum!( @parse, ( $name, $outname, None ), [ ], $($items)* ); - }; - - // Entry point - // RustEnumName { ... } - ( - $name:path { $($items:tt)* } - ) => { - graphql_enum!( @parse, ( $name, (stringify!($name)), None ), [ ], $($items)* ); - }; -} diff --git a/juniper/src/macros/mod.rs b/juniper/src/macros/mod.rs index d62970ea..c41f6be2 100644 --- a/juniper/src/macros/mod.rs +++ b/juniper/src/macros/mod.rs @@ -1,6 +1,4 @@ #[macro_use] -mod enums; -#[macro_use] mod object; #[macro_use] mod interface; diff --git a/juniper/src/macros/tests/enums.rs b/juniper/src/macros/tests/enums.rs index f0fcec22..9ffc5c9f 100644 --- a/juniper/src/macros/tests/enums.rs +++ b/juniper/src/macros/tests/enums.rs @@ -5,34 +5,6 @@ use value::Value; use schema::model::RootNode; use types::scalars::EmptyMutation; - -enum DefaultName { - Foo, - Bar, -} -enum Named { - Foo, - Bar, -} -enum NoTrailingComma { - Foo, - Bar, -} -enum EnumDescription { - Foo, - Bar, -} -enum EnumValueDescription { - Foo, - Bar, -} -enum EnumDeprecation { - Foo, - Bar, -} - -struct Root; - /* Syntax to validate: @@ -45,37 +17,53 @@ Syntax to validate: */ -graphql_enum!(DefaultName { - DefaultName::Foo => "FOO", - DefaultName::Bar => "BAR", -}); +#[derive(GraphQLEnum)] +#[graphql(_internal)] +enum DefaultName { + Foo, + Bar, +} -graphql_enum!(Named as "ANamedEnum" { - Named::Foo => "FOO", - Named::Bar => "BAR", -}); +#[derive(GraphQLEnum)] +#[graphql(name = "ANamedEnum", _internal)] +enum Named { + Foo, + Bar, +} -graphql_enum!(NoTrailingComma { - NoTrailingComma::Foo => "FOO", - NoTrailingComma::Bar => "BAR" -}); +#[derive(GraphQLEnum)] +#[graphql(_internal)] +enum NoTrailingComma { + Foo, + Bar, +} -graphql_enum!(EnumDescription { - description: "A description of the enum itself" +#[derive(GraphQLEnum)] +#[graphql(description = "A description of the enum itself", _internal)] +enum EnumDescription { + Foo, + Bar, +} - EnumDescription::Foo => "FOO", - EnumDescription::Bar => "BAR", -}); +#[derive(GraphQLEnum)] +#[graphql(_internal)] +enum EnumValueDescription { + #[graphql(description = "The FOO value")] + Foo, + #[graphql(description = "The BAR value")] + Bar, +} -graphql_enum!(EnumValueDescription { - EnumValueDescription::Foo => "FOO" as "The FOO value", - EnumValueDescription::Bar => "BAR" as "The BAR value", -}); +#[derive(GraphQLEnum)] +#[graphql(_internal)] +enum EnumDeprecation { + #[graphql(deprecated = "Please don't use FOO any more")] + Foo, + #[graphql(description = "The BAR value", deprecated = "Please don't use BAR any more")] + Bar, +} -graphql_enum!(EnumDeprecation { - EnumDeprecation::Foo => "FOO" deprecated "Please don't use FOO any more", - EnumDeprecation::Bar => "BAR" as "The BAR value" deprecated "Please don't use BAR any more", -}); +struct Root; graphql_object!(Root: () |&self| { field default_name() -> DefaultName { DefaultName::Foo } diff --git a/juniper/src/macros/tests/mod.rs b/juniper/src/macros/tests/mod.rs index a4d4273d..5b458865 100644 --- a/juniper/src/macros/tests/mod.rs +++ b/juniper/src/macros/tests/mod.rs @@ -1,4 +1,3 @@ -mod enums; mod scalar; #[allow(dead_code)] mod input_object; @@ -7,6 +6,7 @@ mod field; mod object; mod interface; mod union; +mod enums; // This asserts that the input objects defined public actually became public diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index eb2b17ee..249b7382 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -49,13 +49,17 @@ pub struct DirectiveType<'a> { pub arguments: Vec>, } -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(GraphQLEnum, Clone, PartialEq, Eq, Debug)] +#[graphql(name = "__DirectiveLocation", _internal)] pub enum DirectiveLocation { Query, Mutation, Field, + #[graphql(name = "FRAGMENT_DEFINITION")] FragmentDefinition, + #[graphql(name = "FRAGMENT_SPREAD")] FragmentSpread, + #[graphql(name = "INLINE_SPREAD")] InlineFragment, } diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index 2c29dcca..0d0491aa 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -232,17 +232,6 @@ graphql_object!(EnumValue: () as "__EnumValue" |&self| { } }); -graphql_enum!(TypeKind as "__TypeKind" { - TypeKind::Scalar => "SCALAR", - TypeKind::Object => "OBJECT", - TypeKind::Interface => "INTERFACE", - TypeKind::Union => "UNION", - TypeKind::Enum => "ENUM", - TypeKind::InputObject => "INPUT_OBJECT", - TypeKind::List => "LIST", - TypeKind::NonNull => "NON_NULL", -}); - graphql_object!(<'a> DirectiveType<'a>: SchemaType<'a> as "__Directive" |&self| { field name() -> &String { @@ -282,11 +271,3 @@ graphql_object!(<'a> DirectiveType<'a>: SchemaType<'a> as "__Directive" |&self| } }); -graphql_enum!(DirectiveLocation as "__DirectiveLocation" { - DirectiveLocation::Query => "QUERY", - DirectiveLocation::Mutation => "MUTATION", - DirectiveLocation::Field => "FIELD", - DirectiveLocation::FragmentDefinition => "FRAGMENT_DEFINITION", - DirectiveLocation::FragmentSpread => "FRAGMENT_SPREAD", - DirectiveLocation::InlineFragment => "INLINE_FRAGMENT", -}); diff --git a/juniper/src/tests/model.rs b/juniper/src/tests/model.rs index eff453de..b0be619c 100644 --- a/juniper/src/tests/model.rs +++ b/juniper/src/tests/model.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; -#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[derive(GraphQLEnum, Copy, Clone, Eq, PartialEq, Debug)] +#[graphql(_internal)] pub enum Episode { + #[graphql(name = "NEW_HOPE")] NewHope, Empire, Jedi, diff --git a/juniper/src/tests/schema.rs b/juniper/src/tests/schema.rs index 607c918a..0f32d266 100644 --- a/juniper/src/tests/schema.rs +++ b/juniper/src/tests/schema.rs @@ -3,12 +3,6 @@ use executor::Context; impl Context for Database {} -graphql_enum!(Episode { - Episode::NewHope => "NEW_HOPE", - Episode::Empire => "EMPIRE", - Episode::Jedi => "JEDI", -}); - graphql_interface!(<'a> &'a Character: Database as "Character" |&self| { description: "A character in the Star Wars Trilogy" diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index b1d29a70..f1deca83 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -13,7 +13,9 @@ use parser::Spanning; /// /// The GraphQL specification defines a number of type kinds - the meta type /// of a type. -#[derive(Clone, Eq, PartialEq, Debug)] +#[derive(GraphQLEnum, Clone, Eq, PartialEq, Debug)] +// Note: _internal flag needed to make derive work in juniper crate itself. +#[graphql(name = "__TypeKind", _internal)] pub enum TypeKind { /// ## Scalar types /// @@ -48,6 +50,7 @@ pub enum TypeKind { /// ## Input objects /// /// Represents complex values provided in queries _into_ the system. + #[graphql(name = "INPUT_OBJECT")] InputObject, /// ## List types @@ -61,6 +64,7 @@ pub enum TypeKind { /// /// In GraphQL, nullable types are the default. By putting a `!` after a /// type, it becomes non-nullable. + #[graphql(name = "NON_NULL")] NonNull, } diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 2b92844e..40951d9b 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -9,11 +9,17 @@ use util::*; struct EnumAttrs { name: Option, description: Option, + internal: bool, } impl EnumAttrs { fn from_input(input: &DeriveInput) -> EnumAttrs { - let mut res = EnumAttrs::default(); + let mut res = EnumAttrs{ + name: None, + description: None, + /// Flag to specify whether the calling crate is the "juniper" crate itself. + internal: false, + }; // Check attributes for name and description. if let Some(items) = get_graphl_attr(&input.attrs) { @@ -26,6 +32,15 @@ impl EnumAttrs { res.description = Some(val); continue; } + match item { + &NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => { + if ident == "_internal" { + res.internal = true; + continue; + } + }, + _ => {}, + } panic!(format!( "Unknown attribute for #[derive(GraphQLEnum)]: {:?}", item @@ -119,7 +134,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { None => quote!{ None }, }; let value = quote!{ - ::juniper::meta::EnumValue{ + _juniper::meta::EnumValue{ name: #name.to_string(), description: #descr, deprecation_reason: #depr, @@ -129,7 +144,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { // Build resolve match clause. let resolve = quote!{ - &#ident::#var_ident => ::juniper::Value::String(#name.to_string()), + &#ident::#var_ident => _juniper::Value::String(#name.to_string()), }; resolves.push(resolve); @@ -142,13 +157,13 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { // Buil to_input clause. let to_input = quote!{ &#ident::#var_ident => - ::juniper::InputValue::string(#name.to_string()), + _juniper::InputValue::string(#name.to_string()), }; to_inputs.push(to_input); } - quote! { - impl ::juniper::GraphQLType for #ident { + let body = quote! { + impl _juniper::GraphQLType for #ident { type Context = (); type TypeInfo = (); @@ -156,7 +171,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { Some(#name) } - fn meta<'r>(_: &(), registry: &mut ::juniper::Registry<'r>) -> ::juniper::meta::MetaType<'r> { + fn meta<'r>(_: &(), registry: &mut _juniper::Registry<'r>) -> _juniper::meta::MetaType<'r> { let meta = registry.build_enum_type::<#ident>(&(), &[ #(#values)* ]); @@ -164,15 +179,15 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { meta.into_meta() } - fn resolve(&self, _: &(), _: Option<&[::juniper::Selection]>, _: &::juniper::Executor) -> ::juniper::Value { + fn resolve(&self, _: &(), _: Option<&[_juniper::Selection]>, _: &_juniper::Executor) -> _juniper::Value { match self { #(#resolves)* } } } - impl ::juniper::FromInputValue for #ident { - fn from_input_value(v: &::juniper::InputValue) -> Option<#ident> { + impl _juniper::FromInputValue for #ident { + fn from_input_value(v: &_juniper::InputValue) -> Option<#ident> { match v.as_enum_value().or_else(|| v.as_string_value()) { #(#from_inputs)* _ => None, @@ -180,12 +195,49 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { } } - impl ::juniper::ToInputValue for #ident { - fn to_input_value(&self) -> ::juniper::InputValue { + impl _juniper::ToInputValue for #ident { + fn to_input_value(&self) -> _juniper::InputValue { match self { #(#to_inputs)* } } } - } + }; + + let dummy_const = Ident::new(format!("_IMPL_GRAPHQLENUM_FOR_{}", ident)); + + // This ugly hack makes it possible to use the derive inside juniper itself. + // FIXME: Figure out a better way to do this! + let crate_reference = if attrs.internal { + quote! { + #[doc(hidden)] + mod _juniper { + pub use ::{ + InputValue, + Value, + ToInputValue, + FromInputValue, + Executor, + Selection, + Registry, + GraphQLType, + meta + }; + } + } + } else { + quote! { + extern crate juniper as _juniper; + } + }; + let generated = quote! { + #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] + #[doc(hidden)] + const #dummy_const : () = { + #crate_reference + #body + }; + }; + + generated } From 6ff3f1fba4324f62f8919001c24cd7eac35fcb83 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 13:11:38 +0100 Subject: [PATCH 10/26] (codegen) Improve enum field name generation Introduce a to_upper_snake_case() helper to properly convert enum names to GraphQL compatible names. Previously, "SomeEnum" would be converted to "SOMEENUM" and not "SOME_ENUM". --- juniper_codegen/src/derive_enum.rs | 2 +- juniper_codegen/src/util.rs | 35 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 40951d9b..934f1de5 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -124,7 +124,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens { // Build value. let name = var_attrs .name - .unwrap_or(variant.ident.as_ref().to_uppercase()); + .unwrap_or(::util::to_upper_snake_case(variant.ident.as_ref())); let descr = match var_attrs.description { Some(s) => quote!{ Some(#s.to_string()) }, None => quote!{ None }, diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util.rs index a3096c7b..9ae094c2 100644 --- a/juniper_codegen/src/util.rs +++ b/juniper_codegen/src/util.rs @@ -80,3 +80,38 @@ fn test_to_camel_case() { assert_eq!(&to_camel_case("a")[..], "a"); assert_eq!(&to_camel_case("")[..], ""); } + +pub(crate) fn to_upper_snake_case(s: &str) -> String { + let mut last_lower = false; + let mut upper = String::new(); + for c in s.chars() { + if c == '_' { + last_lower = false; + } + else if c.is_lowercase() { + last_lower = true; + } else if c.is_uppercase() { + if last_lower { + upper.push('_'); + } + last_lower = false; + } + + for u in c.to_uppercase() { + upper.push(u); + } + } + upper +} + +#[test] +fn test_to_upper_snake_case() { + assert_eq!(to_upper_snake_case("abc"), "ABC"); + assert_eq!(to_upper_snake_case("a_bc"), "A_BC"); + assert_eq!(to_upper_snake_case("ABC"), "ABC"); + assert_eq!(to_upper_snake_case("A_BC"), "A_BC"); + assert_eq!(to_upper_snake_case("SomeInput"), "SOME_INPUT"); + assert_eq!(to_upper_snake_case("someInput"), "SOME_INPUT"); + assert_eq!(to_upper_snake_case("someINpuT"), "SOME_INPU_T"); + assert_eq!(to_upper_snake_case("some_INpuT"), "SOME_INPU_T"); +} From 45859bf4053d0238fd14d741de7761e8bf8257d7 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 13:16:02 +0100 Subject: [PATCH 11/26] (ci) Only test latest 3 rust versions on travis --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1902d2d0..c1c3c9ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,8 @@ rust: - nightly # Prevent accidentally breaking older Rust versions - - 1.17.0 - - 1.18.0 - - 1.19.0 - 1.20.0 + - 1.21.0 env: global: From 643875838dd5d0b589240f5f467c0f4c3792dc23 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:02:17 +0100 Subject: [PATCH 12/26] (codegen) improve derive for InputObject * Implement hack to allow usage in juniper crate * implement (default) attribute for Default::default() * Improve tests --- juniper_codegen/src/derive_input_object.rs | 105 ++++++++++++++---- .../src/codegen/derive_input_object.rs | 36 +++++- juniper_tests/src/lib.rs | 1 + 3 files changed, 115 insertions(+), 27 deletions(-) diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs index 469be09b..47f5018d 100644 --- a/juniper_codegen/src/derive_input_object.rs +++ b/juniper_codegen/src/derive_input_object.rs @@ -9,6 +9,7 @@ use util::*; struct ObjAttrs { name: Option, description: Option, + internal: bool, } impl ObjAttrs { @@ -26,6 +27,15 @@ impl ObjAttrs { res.description = Some(val); continue; } + match item { + &NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => { + if ident == "_internal" { + res.internal = true; + continue; + } + }, + _ => {}, + } panic!(format!( "Unknown attribute for #[derive(GraphQLInputObject)]: {:?}", item @@ -40,7 +50,8 @@ impl ObjAttrs { struct ObjFieldAttrs { name: Option, description: Option, - default: Option, + default: bool, + default_expr: Option, } impl ObjFieldAttrs { @@ -59,9 +70,18 @@ impl ObjFieldAttrs { continue; } if let Some(val) = keyed_item_value(item, "default", true) { - res.default = Some(val); + res.default_expr = Some(val); continue; } + match item { + &NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => { + if ident == "default" { + res.default = true; + continue; + } + } + _ => {}, + } panic!(format!( "Unknown attribute for #[derive(GraphQLInputObject)]: {:?}", item @@ -122,14 +142,20 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { None => quote!{ let field = field; }, }; - let default = match field_attrs.default { - Some(ref def) => match syn::parse_token_trees(def) { - Ok(t) => Some(quote!{ #(#t)* }), - Err(_) => { - panic!("#graphql(default = ?) must be a valid Rust expression inside a string"); + let default = { + if field_attrs.default { + Some(quote! { Default::default() } ) + } else { + match field_attrs.default_expr { + Some(ref def) => match syn::parse_token_trees(def) { + Ok(t) => Some(quote! { #(#t)* }), + Err(_) => { + panic!("#graphql(default = ?) must be a valid Rust expression inside a string"); + } + }, + None => None, } - }, - None => None, + } }; let create_meta_field = match default { @@ -158,7 +184,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { let from_input_default = match default { Some(ref def) => { quote!{ - Some(&&::juniper::InputValue::Null) | None if true => #def, + Some(&&_juniper::InputValue::Null) | None if true => #def, } } None => quote!{}, @@ -169,8 +195,11 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { // TODO: investigate the unwraps here, they seem dangerous! match obj.get(#name) { #from_input_default - Some(v) => ::juniper::FromInputValue::from_input_value(v).unwrap(), - _ => ::juniper::FromInputValue::from_input_value(&::juniper::InputValue::null()).unwrap() + Some(v) => _juniper::FromInputValue::from_input_value(v).unwrap(), + _ => { + _juniper::FromInputValue::from_input_value(&_juniper::InputValue::null()) + .unwrap() + }, } }, }; @@ -183,8 +212,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { to_inputs.push(to_input); } - quote! { - impl ::juniper::GraphQLType for #ident { + let body = quote! { + impl _juniper::GraphQLType for #ident { type Context = (); type TypeInfo = (); @@ -192,7 +221,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { Some(#name) } - fn meta<'r>(_: &(), registry: &mut ::juniper::Registry<'r>) -> ::juniper::meta::MetaType<'r> { + fn meta<'r>(_: &(), registry: &mut _juniper::Registry<'r>) -> _juniper::meta::MetaType<'r> { let fields = &[ #(#meta_fields)* ]; @@ -202,8 +231,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { } } - impl ::juniper::FromInputValue for #ident { - fn from_input_value(value: &::juniper::InputValue) -> Option<#ident> { + impl _juniper::FromInputValue for #ident { + fn from_input_value(value: &_juniper::InputValue) -> Option<#ident> { if let Some(obj) = value.to_object_value() { let item = #ident { #(#from_inputs)* @@ -216,12 +245,46 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens { } } - impl ::juniper::ToInputValue for #ident { - fn to_input_value(&self) -> ::juniper::InputValue { - ::juniper::InputValue::object(vec![ + impl _juniper::ToInputValue for #ident { + fn to_input_value(&self) -> _juniper::InputValue { + _juniper::InputValue::object(vec![ #(#to_inputs)* ].into_iter().collect()) } } - } + }; + + let dummy_const = Ident::new(format!("_IMPL_GRAPHQLINPUTOBJECT_FOR_{}", ident)); + + // This ugly hack makes it possible to use the derive inside juniper itself. + // FIXME: Figure out a better way to do this! + let crate_reference = if attrs.internal { + quote! { + #[doc(hidden)] + mod _juniper { + pub use ::{ + InputValue, + FromInputValue, + GraphQLType, + Registry, + meta, + ToInputValue + }; + } + } + } else { + quote! { + extern crate juniper as _juniper; + } + }; + let generated = quote! { + #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] + #[doc(hidden)] + const #dummy_const : () = { + #crate_reference + #body + }; + }; + + generated } diff --git a/juniper_tests/src/codegen/derive_input_object.rs b/juniper_tests/src/codegen/derive_input_object.rs index 57c24556..caed94dd 100644 --- a/juniper_tests/src/codegen/derive_input_object.rs +++ b/juniper_tests/src/codegen/derive_input_object.rs @@ -2,7 +2,7 @@ use fnv::FnvHashMap; #[cfg(test)] -use juniper::{self, FromInputValue, GraphQLType, ToInputValue}; +use juniper::{self, FromInputValue, GraphQLType, InputValue}; #[derive(GraphQLInputObject, Debug, PartialEq)] #[graphql(name = "MyInput", description = "input descr")] @@ -10,6 +10,9 @@ struct Input { regular_field: String, #[graphql(name = "haha", default = "33", description = "haha descr")] c: i32, + + #[graphql(default)] + other: Option, } #[test] @@ -22,10 +25,31 @@ fn test_derived_input_object() { assert_eq!(meta.name(), Some("MyInput")); assert_eq!(meta.description(), Some(&"input descr".to_string())); - let obj = Input { - regular_field: "a".to_string(), + // Test default value injection. + + let input_no_defaults: InputValue = ::serde_json::from_value(json!({ + "regularField": "a", + })).unwrap(); + + let output_no_defaults: Input = FromInputValue::from_input_value(&input_no_defaults).unwrap(); + assert_eq!(output_no_defaults, Input{ + regular_field: "a".into(), c: 33, - }; - let restored: Input = FromInputValue::from_input_value(&obj.to_input_value()).unwrap(); - assert_eq!(obj, restored); + other: None, + }); + + // Test with all values supplied. + + let input: InputValue = ::serde_json::from_value(json!({ + "regularField": "a", + "haha": 55, + "other": true, + })).unwrap(); + + let output: Input = FromInputValue::from_input_value(&input).unwrap(); + assert_eq!(output, Input{ + regular_field: "a".into(), + c: 55, + other: Some(true), + }); } diff --git a/juniper_tests/src/lib.rs b/juniper_tests/src/lib.rs index c4fc104c..61f980e7 100644 --- a/juniper_tests/src/lib.rs +++ b/juniper_tests/src/lib.rs @@ -1,5 +1,6 @@ #[macro_use] extern crate juniper; +#[macro_use] extern crate serde_json; #[cfg(test)] From 0e3c9940c6008514fddb1b4c1258e40c4c53e6a2 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:29:18 +0100 Subject: [PATCH 13/26] Exchange graphql_input_object! macro to custom derive * Refactor all internal use to derive * Remove macro * Move tests to executor_tests --- .../introspection}/input_object.rs | 131 ++++---- .../mod.rs} | 8 + juniper/src/executor_tests/variables.rs | 56 ++-- juniper/src/macros/input_object.rs | 283 ------------------ juniper/src/macros/mod.rs | 2 - juniper/src/macros/tests/args.rs | 11 +- juniper/src/macros/tests/mod.rs | 6 +- 7 files changed, 105 insertions(+), 392 deletions(-) rename juniper/src/{macros/tests => executor_tests/introspection}/input_object.rs (86%) rename juniper/src/executor_tests/{introspection.rs => introspection/mod.rs} (98%) delete mode 100644 juniper/src/macros/input_object.rs diff --git a/juniper/src/macros/tests/input_object.rs b/juniper/src/executor_tests/introspection/input_object.rs similarity index 86% rename from juniper/src/macros/tests/input_object.rs rename to juniper/src/executor_tests/introspection/input_object.rs index a4219be6..c4e15758 100644 --- a/juniper/src/macros/tests/input_object.rs +++ b/juniper/src/executor_tests/introspection/input_object.rs @@ -8,82 +8,81 @@ use types::scalars::EmptyMutation; struct Root; -graphql_input_object!( - struct DefaultName { - field_one: String, - field_two: String, - } -); +#[derive(GraphQLInputObject)] +#[graphql(_internal)] +struct DefaultName { + field_one: String, + field_two: String, +} -graphql_input_object!( - struct NoTrailingComma { - field_one: String, - field_two: String - } -); +#[derive(GraphQLInputObject)] +#[graphql(_internal)] +struct NoTrailingComma { + field_one: String, + field_two: String +} -graphql_input_object!( - #[derive(Debug)] - struct Derive { - field_one: String, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct Derive { + field_one: String, +} -graphql_input_object!( - struct Named as "ANamedInputObject" { - field_one: String, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(name = "ANamedInputObject", _internal)] +struct Named { + field_one: String, +} -graphql_input_object!( - description: "Description for the input object" +#[derive(GraphQLInputObject, Debug)] +#[graphql(description = "Description for the input object", _internal)] +struct Description { + field_one: String, +} - struct Description { - field_one: String, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +pub struct Public { + field_one: String, +} -graphql_input_object!( - pub struct Public { - field_one: String, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(description = "Description for the input object", _internal)] +pub struct PublicWithDescription { + field_one: String, +} -graphql_input_object!( - description: "Description for the input object" +#[derive(GraphQLInputObject, Debug)] +#[graphql(name = "APublicNamedInputObjectWithDescription", + description = "Description for the input object", + _internal)] +pub struct NamedPublicWithDescription { + field_one: String, +} - pub struct PublicWithDescription { - field_one: String, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(name = "APublicNamedInputObject", _internal)] +pub struct NamedPublic { + field_one: String, +} -graphql_input_object!( - description: "Description for the input object" +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct FieldDescription { + #[graphql(description = "The first field")] + field_one: String, + #[graphql(description = "The second field")] + field_two: String, +} - pub struct NamedPublicWithDescription as "APublicNamedInputObjectWithDescription" { - field_one: String, - } -); - -graphql_input_object!( - pub struct NamedPublic as "APublicNamedInputObject" { - field_one: String, - } -); - -graphql_input_object!( - struct FieldDescription { - field_one: String as "The first field", - field_two: String as "The second field", - } -); - -graphql_input_object!( - struct FieldWithDefaults { - field_one = 123: i32, - field_two = 456: i32 as "The second field", - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct FieldWithDefaults { + #[graphql(default = "123")] + field_one: i32, + #[graphql(default = "456", description = "The second field")] + field_two: i32, +} graphql_object!(Root: () |&self| { field test_field( diff --git a/juniper/src/executor_tests/introspection.rs b/juniper/src/executor_tests/introspection/mod.rs similarity index 98% rename from juniper/src/executor_tests/introspection.rs rename to juniper/src/executor_tests/introspection/mod.rs index 1480ca17..2471b0b7 100644 --- a/juniper/src/executor_tests/introspection.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -1,8 +1,16 @@ +mod input_object; + +// This asserts that the input objects defined public actually became public +#[allow(unused_imports)] +use self::input_object::{NamedPublic, NamedPublicWithDescription}; + use executor::Variables; use value::Value; use schema::model::RootNode; use types::scalars::EmptyMutation; + + #[derive(GraphQLEnum)] #[graphql(name = "SampleEnum", _internal)] enum Sample { diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index 900d9eda..bbbad525 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -30,39 +30,35 @@ graphql_scalar!(TestComplexScalar { } }); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct TestInputObject { + a: Option, + b: Option>>, + c: String, + d: Option, +} -graphql_input_object!( - #[derive(Debug)] - struct TestInputObject { - a: Option, - b: Option>>, - c: String, - d: Option, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct TestNestedInputObject { + na: TestInputObject, + nb: String, +} -graphql_input_object!( - #[derive(Debug)] - struct TestNestedInputObject { - na: TestInputObject, - nb: String, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct ExampleInputObject { + a: Option, + b: i32, +} -graphql_input_object!( - #[derive(Debug)] - struct ExampleInputObject { - a: Option, - b: i32, - } -); - -graphql_input_object!( - #[derive(Debug)] - struct InputWithDefaults { - a = 123: i32, - } -); +#[derive(GraphQLInputObject, Debug)] +#[graphql(_internal)] +struct InputWithDefaults { + #[graphql(default = "123")] + a: i32, +} graphql_object!(TestType: () |&self| { field field_with_object_input(input: Option) -> String { diff --git a/juniper/src/macros/input_object.rs b/juniper/src/macros/input_object.rs deleted file mode 100644 index adf43669..00000000 --- a/juniper/src/macros/input_object.rs +++ /dev/null @@ -1,283 +0,0 @@ -/** -Create an input object - -Input objects are used as data carriers for complex input values to -fields and mutations. Unlike the other helper macros, -`graphql_input_object!` actually *creates* the struct you define. It -does not add anything to the struct definition itself - what you type -is what will be generated: - -```rust -# #[macro_use] extern crate juniper; -# -graphql_input_object!( - description: "Coordinates for the user" - - struct Coordinates { - longitude: f64 as "The X coordinate, from -180 to +180", - latitude: f64 as "The Y coordinate, from -90 to +90", - } -); - -# fn main() { } -``` - -This macro creates the struct as specified and implements -`FromInputValue` to automatically parse values provided from variables -and arguments. - -If you want to expose the struct under a different name than the Rust -type, you can write `struct Coordinates as "MyCoordinates" { ...`. - -You can specify *default values* for input object fields; the syntax -is similar to argument default values: - -```rust -# #[macro_use] extern crate juniper; -# -graphql_input_object!( - struct SampleObject { - foo = 123: i32 as "A sample field, defaults to 123 if omitted" - } -); - -# fn main() { } -``` - -*/ -#[macro_export] -macro_rules! graphql_input_object { - // Calls $val.$func($arg) if $arg is not None - ( @maybe_apply, None, $func:ident, $val:expr ) => { $val }; - ( @maybe_apply, $arg:tt, $func:ident, $val:expr ) => { $val.$func($arg) }; - - // Calls $val.description($descr) when $descr is not empty - ( @apply_description, , $val:expr ) => { $val }; - ( @apply_description, $descr:tt , $val:expr ) => { $val.description($descr) }; - - // Generate the FromInputValue::from method body, provided a - // HashMap<&str, &InputValue> in $var - ( - @generate_from_input_value, - $name:tt, $var:tt, - ( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* ) - ) => { - Some($name { - $( $field_name: { - let n = $crate::to_camel_case(stringify!($field_name)); - let v: Option<&&$crate::InputValue> = $var.get(&n[..]); - - match v { - $( Some(&&$crate::InputValue::Null) | None if true => $default, )* - Some(v) => $crate::FromInputValue::from_input_value(v).unwrap(), - _ => $crate::FromInputValue::from_input_value(&$crate::InputValue::null()).unwrap() - } - } ),* - }) - }; - - // Generate the ToInputValue::To method body, provided self in $self - ( - @generate_to_input_value, - $name:tt, $selfvar:tt, - ( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* ) - ) => { - $crate::InputValue::object(vec![ - $( - ($crate::to_camel_case(stringify!($field_name)), $selfvar.$field_name.to_input_value()) - ),* - ].into_iter().collect()) - }; - - // Generate the struct declaration, including (Rust) meta attributes - ( - @generate_struct_fields, - ( $($meta:tt)* ), ( $($pubmod:tt)* ), $name:tt, - ( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* ) - ) => { - $($meta)* $($pubmod)* struct $name { - $( $field_name: $field_type, )* - } - }; - - // Generate single field meta for field with default value - ( - @generate_single_meta_field, - $reg:tt, - ( $field_name:ident = $default:tt : $field_type:ty $(as $descr:tt)* ) - ) => { - graphql_input_object!( - @apply_description, - $($descr)*, - $reg.arg_with_default::<$field_type>( - &$crate::to_camel_case(stringify!($field_name)), - &$default, &())) - }; - - // Generate single field meta for field without default value - ( - @generate_single_meta_field, - $reg:tt, - ( $field_name:ident : $field_type:ty $(as $descr:tt)* ) - ) => { - graphql_input_object!( - @apply_description, - $($descr)*, - $reg.arg::<$field_type>( - &$crate::to_camel_case(stringify!($field_name)), &())) - }; - - // Generate the input field meta list, i.e. &[Argument] for - ( - @generate_meta_fields, - $reg:tt, - ( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* ) - ) => { - &[ - $( - graphql_input_object!( - @generate_single_meta_field, - $reg, - ( $field_name $(= $default)* : $field_type $(as $descr)* ) - ) - ),* - ] - }; - - // #[...] struct $name { ... } - // struct $name { ... } - ( - @parse, - ( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ), - $(#[$meta:meta])* struct $name:ident { $($fields:tt)* } $($rest:tt)* - ) => { - graphql_input_object!( - @parse, - ( ( $(#[$meta])* ), ( ), $name, (stringify!($name)), ($($fields)*), $descr ), - $($rest)* - ); - }; - - // #[...] pub struct $name { ... } - // pub struct $name { ... } - ( - @parse, - ( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ), - $(#[$meta:meta])* pub struct $name:ident { $($fields:tt)* } $($rest:tt)* - ) => { - graphql_input_object!( - @parse, - ( ( $(#[$meta])* ), ( pub ), $name, (stringify!($name)), ($($fields)*), $descr ), - $($rest)* - ); - }; - - // #[...] struct $name as "GraphQLName" { ... } - // struct $name as "GraphQLName" { ... } - ( - @parse, - ( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ), - $(#[$meta:meta])* struct $name:ident as $outname:tt { $($fields:tt)* } $($rest:tt)* - ) => { - graphql_input_object!( - @parse, - ( ( $($meta)* ), ( ), $name, $outname, ($($fields)*), $descr ), - $($rest)* - ); - }; - - // #[...] pub struct $name as "GraphQLName" { ... } - // pub struct $name as "GraphQLName" { ... } - ( - @parse, - ( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ), - $(#[$meta:meta])* pub struct $name:ident as $outname:tt { $($fields:tt)* } $($rest:tt)* - ) => { - graphql_input_object!( - @parse, - ( ( $($meta)* ), ( pub ), $name, $outname, ($($fields)*), $descr ), - $($rest)* - ); - }; - - // description: - ( - @parse, - ( $meta:tt, $pubmod:tt, $name:tt, $outname:tt, $fields:tt, $_ignore:tt ), - description: $descr:tt $($rest:tt)* - ) => { - graphql_input_object!( - @parse, - ( $meta, $pubmod, $name, $outname, $fields, $descr ), - $($rest)* - ); - }; - - // No more data to parse, generate the struct and impls - ( - @parse, - ( $meta:tt, $pubmod:tt, $name:tt, $outname:tt, $fields:tt, $descr:tt ), - ) => { - graphql_input_object!(@generate_struct_fields, $meta, $pubmod, $name, $fields); - - impl $crate::FromInputValue for $name { - fn from_input_value(value: &$crate::InputValue) -> Option<$name> { - if let Some(obj) = value.to_object_value() { - graphql_input_object!(@generate_from_input_value, $name, obj, $fields) - } - else { - None - } - } - } - - impl $crate::ToInputValue for $name { - fn to_input_value(&self) -> $crate::InputValue { - graphql_input_object!(@generate_to_input_value, $name, self, $fields) - } - } - - impl $crate::GraphQLType for $name { - type Context = (); - type TypeInfo = (); - - fn name(_: &()) -> Option<&str> { - Some($outname) - } - - fn meta<'r>(_: &(), registry: &mut $crate::Registry<'r>) -> $crate::meta::MetaType<'r> { - let fields = graphql_input_object!(@generate_meta_fields, registry, $fields); - graphql_input_object!( - @maybe_apply, $descr, description, - registry.build_input_object_type::<$name>(&(), fields)).into_meta() - } - } - }; - - // Entry point: parse calls starting with a struct declaration - ( $(#[$meta:meta])* struct $($items:tt)* ) => { - graphql_input_object!( - @parse, - ( ( ), ( ), None, None, None, None ), - $(#[$meta])* struct $($items)* - ); - }; - - // Entry point: parse calls starting with a public struct declaration - ( $(#[$meta:meta])* pub struct $($items:tt)* ) => { - graphql_input_object!( - @parse, - ( ( ), ( ), None, None, None, None ), - $(#[$meta])* pub struct $($items)* - ); - }; - - // Entry point: parse calls starting with the description - ( description: $($items:tt)* ) => { - graphql_input_object!( - @parse, - ( ( ), ( ), None, None, None, None ), - description: $($items)* - ); - }; -} diff --git a/juniper/src/macros/mod.rs b/juniper/src/macros/mod.rs index c41f6be2..fa97f3ee 100644 --- a/juniper/src/macros/mod.rs +++ b/juniper/src/macros/mod.rs @@ -9,8 +9,6 @@ mod args; #[macro_use] mod field; #[macro_use] -mod input_object; -#[macro_use] mod union; #[cfg(test)] diff --git a/juniper/src/macros/tests/args.rs b/juniper/src/macros/tests/args.rs index 007233fc..6d51632a 100644 --- a/juniper/src/macros/tests/args.rs +++ b/juniper/src/macros/tests/args.rs @@ -19,12 +19,11 @@ Syntax to validate: */ -graphql_input_object!( - #[derive(Debug)] - struct Point { - x: i32, - } -); +#[derive(GraphQLInputObject)] +#[graphql(_internal)] +struct Point { + x: i32, +} graphql_object!(Root: () |&self| { field simple() -> i32 { 0 } diff --git a/juniper/src/macros/tests/mod.rs b/juniper/src/macros/tests/mod.rs index 5b458865..1738b128 100644 --- a/juniper/src/macros/tests/mod.rs +++ b/juniper/src/macros/tests/mod.rs @@ -1,6 +1,4 @@ mod scalar; -#[allow(dead_code)] -mod input_object; mod args; mod field; mod object; @@ -9,6 +7,4 @@ mod union; mod enums; -// This asserts that the input objects defined public actually became public -#[allow(unused_imports)] -use self::input_object::{NamedPublic, NamedPublicWithDescription}; + From 0024e67daeea3741bbe18ed2a460eb1f2010061f Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:46:11 +0100 Subject: [PATCH 14/26] Move enum introspection tests to executor_tests --- .../{macros/tests => executor_tests/introspection}/enums.rs | 0 juniper/src/executor_tests/introspection/mod.rs | 3 +-- juniper/src/macros/tests/mod.rs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) rename juniper/src/{macros/tests => executor_tests/introspection}/enums.rs (100%) diff --git a/juniper/src/macros/tests/enums.rs b/juniper/src/executor_tests/introspection/enums.rs similarity index 100% rename from juniper/src/macros/tests/enums.rs rename to juniper/src/executor_tests/introspection/enums.rs diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 2471b0b7..3e98f181 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -1,4 +1,5 @@ mod input_object; +mod enums; // This asserts that the input objects defined public actually became public #[allow(unused_imports)] @@ -9,8 +10,6 @@ use value::Value; use schema::model::RootNode; use types::scalars::EmptyMutation; - - #[derive(GraphQLEnum)] #[graphql(name = "SampleEnum", _internal)] enum Sample { diff --git a/juniper/src/macros/tests/mod.rs b/juniper/src/macros/tests/mod.rs index 1738b128..0cba2fb7 100644 --- a/juniper/src/macros/tests/mod.rs +++ b/juniper/src/macros/tests/mod.rs @@ -4,7 +4,6 @@ mod field; mod object; mod interface; mod union; -mod enums; From bdb1dd34afb87ef8e501db28d20a7930384a9fe1 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:54:39 +0100 Subject: [PATCH 15/26] (ci) Make appveyor and travis run the same test command --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 793779c4..b65f8567 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,4 +49,4 @@ install: build: false test_script: - - cargo make ci-flow + - cargo make workspace-ci-flow --no-workspace From bec52958278d0b7ccdfe164de0b8769aeeeac36b Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:55:29 +0100 Subject: [PATCH 16/26] Move juniper_iron crate back into the main repo. This is done for a lower maintainance burden and combined testing. --- Cargo.toml | 1 + juniper_iron/.gitignore | 2 + juniper_iron/CHANGELOG.md | 0 juniper_iron/Cargo.toml | 26 ++ juniper_iron/LICENSE | 25 ++ juniper_iron/README.md | 31 +++ juniper_iron/examples/iron_server.rs | 43 +++ juniper_iron/src/lib.rs | 397 +++++++++++++++++++++++++++ 8 files changed, 525 insertions(+) create mode 100644 juniper_iron/.gitignore create mode 100644 juniper_iron/CHANGELOG.md create mode 100644 juniper_iron/Cargo.toml create mode 100644 juniper_iron/LICENSE create mode 100644 juniper_iron/README.md create mode 100644 juniper_iron/examples/iron_server.rs create mode 100644 juniper_iron/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index cc833d78..cee85539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,5 @@ members = [ "juniper", "juniper_codegen", "juniper_tests", + "juniper_iron", ] diff --git a/juniper_iron/.gitignore b/juniper_iron/.gitignore new file mode 100644 index 00000000..a9d37c56 --- /dev/null +++ b/juniper_iron/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/juniper_iron/CHANGELOG.md b/juniper_iron/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/juniper_iron/Cargo.toml b/juniper_iron/Cargo.toml new file mode 100644 index 00000000..464e1f7a --- /dev/null +++ b/juniper_iron/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "juniper_iron" +version = "0.1.0" +authors = ["Magnus Hallin "] +description = "Iron integration for juniper" +license = "BSD-2-Clause" +documentation = "https://docs.rs/juniper_iron" +repository = "https://github.com/graphql-rust/juniper_iron" + +[dependencies] +serde = { version = "1.0.2" } +serde_json = { version = "1.0.2" } +urlencoded = { version = "0.5.0" } +iron = "0.5.1" +juniper = { version = "0.8.1", git = "https://github.com/graphql-rust/juniper" } + +[dev-dependencies] +iron-test = "^0.5.0" +router = "^0.5.0" +mount = "^0.3.0" +logger = "^0.3.0" +juniper = { version = "0.8.1", features = ["expose-test-schema", "serde_json"], git = "https://github.com/graphql-rust/juniper" } + +[badges] +travis-ci = { repository = "graphql-rust/juniper_iron" } +appveyor = { repository = "graphql-rust/juniper_iron" } diff --git a/juniper_iron/LICENSE b/juniper_iron/LICENSE new file mode 100644 index 00000000..0ccd1e17 --- /dev/null +++ b/juniper_iron/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2016, Magnus Hallin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/juniper_iron/README.md b/juniper_iron/README.md new file mode 100644 index 00000000..592268bd --- /dev/null +++ b/juniper_iron/README.md @@ -0,0 +1,31 @@ +# juniper_iron + +[![Build Status](https://travis-ci.org/graphql-rust/juniper_iron.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper_iron) +[![Build status](https://ci.appveyor.com/api/projects/status/rqguvfkl9m0g7hum?svg=true)](https://ci.appveyor.com/project/theduke/juniper-iron) +[![Crates.io](https://img.shields.io/crates/v/juniper_iron.svg?maxAge=2592000)](https://crates.io/crates/juniper_iron) +[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql) + +This repository contains the [Iron][Iron] web framework integration for [Juniper][Juniper], a [GraphQL][GraphQL] +implementation for Rust. + +## Documentation + +Once the crate is published, documentation will be on [docs.rs][documentation]. + +For now, please consult the documentation comments [here](https://github.com/graphql-rust/juniper_iron/blob/master/src/lib.rs). + +## Examples + +Check [examples/iron_server.rs][example] for example code of a working Iron server with GraphQL handlers. + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Iron]: https://github.com/iron/iron +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_iron +[example]: https://github.com/graphql-rust/juniper_iron/blob/master/examples/iron_server.rs diff --git a/juniper_iron/examples/iron_server.rs b/juniper_iron/examples/iron_server.rs new file mode 100644 index 00000000..9fa4f0ab --- /dev/null +++ b/juniper_iron/examples/iron_server.rs @@ -0,0 +1,43 @@ +extern crate iron; +extern crate mount; +extern crate logger; +extern crate serde; +extern crate juniper; +extern crate juniper_iron; + +use std::env; + +use mount::Mount; +use logger::Logger; +use iron::prelude::*; +use juniper::EmptyMutation; +use juniper_iron::{GraphQLHandler, GraphiQLHandler}; +use juniper::tests::model::Database; + +fn context_factory(_: &mut Request) -> Database { + Database::new() +} + +fn main() { + let mut mount = Mount::new(); + + let graphql_endpoint = GraphQLHandler::new( + context_factory, + Database::new(), + EmptyMutation::::new(), + ); + let graphiql_endpoint = GraphiQLHandler::new("/graphql"); + + mount.mount("/", graphiql_endpoint); + mount.mount("/graphql", graphql_endpoint); + + let (logger_before, logger_after) = Logger::new(None); + + let mut chain = Chain::new(mount); + chain.link_before(logger_before); + chain.link_after(logger_after); + + let host = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_owned()); + println!("GraphQL server started on {}", host); + Iron::new(chain).http(host.as_str()).unwrap(); +} diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs new file mode 100644 index 00000000..8fda09d5 --- /dev/null +++ b/juniper_iron/src/lib.rs @@ -0,0 +1,397 @@ +/*! + +[Juniper][1] handlers for the [Iron][2] framework. + +## Integrating with Iron + + + +For example, continuing from the schema created above and using Iron to expose +the schema on an HTTP endpoint supporting both GET and POST requests: + +```rust,no_run +extern crate iron; +# #[macro_use] extern crate juniper; +# extern crate juniper_iron; +# use std::collections::HashMap; + +use iron::prelude::*; +use juniper_iron::GraphQLHandler; +use juniper::{Context, EmptyMutation}; + +# use juniper::FieldResult; +# +# struct User { id: String, name: String, friend_ids: Vec } +# struct QueryRoot; +# struct Database { users: HashMap } +# +# graphql_object!(User: Database |&self| { +# field id() -> FieldResult<&String> { +# Ok(&self.id) +# } +# +# field name() -> FieldResult<&String> { +# Ok(&self.name) +# } +# +# field friends(&executor) -> FieldResult> { +# Ok(self.friend_ids.iter() +# .filter_map(|id| executor.context().users.get(id)) +# .collect()) +# } +# }); +# +# graphql_object!(QueryRoot: Database |&self| { +# field user(&executor, id: String) -> FieldResult> { +# Ok(executor.context().users.get(&id)) +# } +# }); + +// This function is executed for every request. Here, we would realistically +// provide a database connection or similar. For this example, we'll be +// creating the database from scratch. +fn context_factory(_: &mut Request) -> Database { + Database { + users: vec![ + ( "1000".to_owned(), User { + id: "1000".to_owned(), name: "Robin".to_owned(), + friend_ids: vec!["1001".to_owned()] } ), + ( "1001".to_owned(), User { + id: "1001".to_owned(), name: "Max".to_owned(), + friend_ids: vec!["1000".to_owned()] } ), + ].into_iter().collect() + } +} + +impl Context for Database {} + +fn main() { + // GraphQLHandler takes a context factory function, the root object, + // and the mutation object. If we don't have any mutations to expose, we + // can use the empty tuple () to indicate absence. + let graphql_endpoint = GraphQLHandler::new( + context_factory, QueryRoot, EmptyMutation::::new()); + + // Start serving the schema at the root on port 8080. + Iron::new(graphql_endpoint).http("localhost:8080").unwrap(); +} + +``` + +See the [iron_server.rs][5] +example for more information on how to use these handlers. + +See the the [`GraphQLHandler`][3] documentation for more information on what request methods are +supported. +There's also a built-in [GraphiQL][4] handler included. + +[1]: https://github.com/mhallin/Juniper +[2]: http://ironframework.io +[3]: ./struct.GraphQLHandler.html +[4]: https://github.com/graphql/graphiql +[5]: https://github.com/mhallin/juniper/blob/master/juniper_iron/examples/iron_server.rs + +*/ + +extern crate serde_json; +extern crate juniper; +extern crate urlencoded; +#[macro_use] +extern crate iron; +#[cfg(test)] +extern crate iron_test; + +use iron::prelude::*; +use iron::middleware::Handler; +use iron::mime::Mime; +use iron::status; +use iron::method; +use urlencoded::{UrlDecodingError, UrlEncodedQuery}; + +use std::io::Read; +use std::error::Error; +use std::fmt; + +use serde_json::error::Error as SerdeError; + +use juniper::{GraphQLType, InputValue, RootNode}; +use juniper::http; + +/// Handler that executes GraphQL queries in the given schema +/// +/// The handler responds to GET requests and POST requests only. In GET +/// requests, the query should be supplied in the `query` URL parameter, e.g. +/// `http://localhost:3000/graphql?query={hero{name}}`. +/// +/// POST requests support both queries and variables. POST a JSON document to +/// this endpoint containing the field `"query"` and optionally `"variables"`. +/// The variables should be a JSON object containing the variable to value +/// mapping. +pub struct GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT> +where + CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static, + CtxT: 'static, + Query: GraphQLType + Send + Sync + 'static, + Mutation: GraphQLType + Send + Sync + 'static, +{ + context_factory: CtxFactory, + root_node: RootNode<'a, Query, Mutation>, +} + +/// Handler that renders GraphiQL - a graphical query editor interface +pub struct GraphiQLHandler { + graphql_url: String, +} + + +fn get_single_value(mut values: Vec) -> IronResult { + if values.len() == 1 { + Ok(values.remove(0)) + } else { + Err( + GraphQLIronError::InvalidData("Duplicate URL query parameter").into(), + ) + } +} + +fn parse_url_param(params: Option>) -> IronResult> { + if let Some(values) = params { + get_single_value(values).map(Some) + } else { + Ok(None) + } +} + +fn parse_variable_param(params: Option>) -> IronResult> { + if let Some(values) = params { + Ok(serde_json::from_str::( + get_single_value(values)?.as_ref(), + ).map(Some) + .map_err(GraphQLIronError::Serde)?) + } else { + Ok(None) + } +} + + +impl<'a, CtxFactory, Query, Mutation, CtxT> GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT> +where + CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static, + CtxT: 'static, + Query: GraphQLType + Send + Sync + 'static, + Mutation: GraphQLType + Send + Sync + 'static, +{ + /// Build a new GraphQL handler + /// + /// The context factory will receive the Iron request object and is + /// expected to construct a context object for the given schema. This can + /// be used to construct e.g. database connections or similar data that + /// the schema needs to execute the query. + pub fn new(context_factory: CtxFactory, query: Query, mutation: Mutation) -> Self { + GraphQLHandler { + context_factory: context_factory, + root_node: RootNode::new(query, mutation), + } + } + + + fn handle_get(&self, req: &mut Request) -> IronResult { + let url_query_string = req.get_mut::() + .map_err(|e| GraphQLIronError::Url(e))?; + + let input_query = parse_url_param(url_query_string.remove("query"))? + .ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?; + let operation_name = parse_url_param(url_query_string.remove("operationName"))?; + let variables = parse_variable_param(url_query_string.remove("variables"))?; + + Ok(http::GraphQLRequest::new( + input_query, + operation_name, + variables, + )) + } + + fn handle_post(&self, req: &mut Request) -> IronResult { + let mut request_payload = String::new(); + itry!(req.body.read_to_string(&mut request_payload)); + + Ok(serde_json::from_str::( + request_payload.as_str(), + ).map_err(|err| GraphQLIronError::Serde(err))?) + } + + fn execute(&self, context: &CtxT, request: http::GraphQLRequest) -> IronResult { + let response = request.execute(&self.root_node, context); + let content_type = "application/json".parse::().unwrap(); + let json = serde_json::to_string_pretty(&response).unwrap(); + let status = if response.is_ok() { + status::Ok + } else { + status::BadRequest + }; + Ok(Response::with((content_type, status, json))) + } +} + +impl GraphiQLHandler { + /// Build a new GraphiQL handler targeting the specified URL. + /// + /// The provided URL should point to the URL of the attached `GraphQLHandler`. It can be + /// relative, so a common value could be `"/graphql"`. + pub fn new(graphql_url: &str) -> GraphiQLHandler { + GraphiQLHandler { + graphql_url: graphql_url.to_owned(), + } + } +} + +impl<'a, CtxFactory, Query, Mutation, CtxT> Handler + for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT> +where + CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static, + CtxT: 'static, + Query: GraphQLType + Send + Sync + 'static, + Mutation: GraphQLType + Send + Sync + 'static, + 'a: 'static, +{ + fn handle(&self, mut req: &mut Request) -> IronResult { + let context = (self.context_factory)(req); + + let graphql_request = match req.method { + method::Get => self.handle_get(&mut req)?, + method::Post => self.handle_post(&mut req)?, + _ => return Ok(Response::with((status::MethodNotAllowed))), + }; + + self.execute(&context, graphql_request) + } +} + +impl Handler for GraphiQLHandler { + fn handle(&self, _: &mut Request) -> IronResult { + let content_type = "text/html".parse::().unwrap(); + + Ok(Response::with(( + content_type, + status::Ok, + juniper::graphiql::graphiql_source(&self.graphql_url), + ))) + } +} + +#[derive(Debug)] +enum GraphQLIronError { + Serde(SerdeError), + Url(UrlDecodingError), + InvalidData(&'static str), +} + +impl fmt::Display for GraphQLIronError { + fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result { + match *self { + GraphQLIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f), + GraphQLIronError::Url(ref err) => fmt::Display::fmt(err, &mut f), + GraphQLIronError::InvalidData(ref err) => fmt::Display::fmt(err, &mut f), + } + } +} + +impl Error for GraphQLIronError { + fn description(&self) -> &str { + match *self { + GraphQLIronError::Serde(ref err) => err.description(), + GraphQLIronError::Url(ref err) => err.description(), + GraphQLIronError::InvalidData(ref err) => err, + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + GraphQLIronError::Serde(ref err) => Some(err), + GraphQLIronError::Url(ref err) => Some(err), + GraphQLIronError::InvalidData(_) => None, + } + } +} + +impl From for IronError { + fn from(err: GraphQLIronError) -> IronError { + let message = format!("{}", err); + IronError::new(err, (status::BadRequest, message)) + } +} + +#[cfg(test)] +mod tests { + use iron::prelude::*; + use iron_test::{request, response}; + use iron::{Handler, Headers}; + + use juniper::tests::model::Database; + use juniper::http::tests as http_tests; + use juniper::EmptyMutation; + + use super::GraphQLHandler; + + struct TestIronIntegration; + + impl http_tests::HTTPIntegration for TestIronIntegration { + fn get(&self, url: &str) -> http_tests::TestResponse { + make_test_response(request::get( + &("http://localhost:3000".to_owned() + url), + Headers::new(), + &make_handler(), + )) + } + + fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { + make_test_response(request::post( + &("http://localhost:3000".to_owned() + url), + Headers::new(), + body, + &make_handler(), + )) + } + } + + #[test] + fn test_iron_integration() { + let integration = TestIronIntegration; + + http_tests::run_http_test_suite(&integration); + } + + fn context_factory(_: &mut Request) -> Database { + Database::new() + } + + fn make_test_response(response: IronResult) -> http_tests::TestResponse { + let response = response.expect("Error response from GraphQL handler"); + let status_code = response + .status + .expect("No status code returned from handler") + .to_u16() as i32; + let content_type = String::from_utf8( + response + .headers + .get_raw("content-type") + .expect("No content type header from handler")[0] + .clone(), + ).expect("Content-type header invalid UTF-8"); + let body = response::extract_body_to_string(response); + + http_tests::TestResponse { + status_code: status_code, + body: Some(body), + content_type: content_type, + } + } + + fn make_handler() -> Box { + Box::new(GraphQLHandler::new( + context_factory, + Database::new(), + EmptyMutation::::new(), + )) + } +} From b89712a8878f46b26fdbbf815d8a650a01a29d25 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:59:23 +0100 Subject: [PATCH 17/26] Add juniper_rocket back to main repo This is done for a lower maintainance burden and combined testing. --- juniper_rocket/.gitignore | 2 + juniper_rocket/Cargo.toml | 23 +++ juniper_rocket/LICENSE | 25 +++ juniper_rocket/README.md | 31 +++ juniper_rocket/examples/rocket_server.rs | 51 +++++ juniper_rocket/src/lib.rs | 232 +++++++++++++++++++++++ 6 files changed, 364 insertions(+) create mode 100644 juniper_rocket/.gitignore create mode 100644 juniper_rocket/Cargo.toml create mode 100644 juniper_rocket/LICENSE create mode 100644 juniper_rocket/README.md create mode 100644 juniper_rocket/examples/rocket_server.rs create mode 100644 juniper_rocket/src/lib.rs diff --git a/juniper_rocket/.gitignore b/juniper_rocket/.gitignore new file mode 100644 index 00000000..a9d37c56 --- /dev/null +++ b/juniper_rocket/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml new file mode 100644 index 00000000..6f1c6fac --- /dev/null +++ b/juniper_rocket/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "juniper_rocket" +version = "0.1.0" +authors = ["Magnus Hallin "] +description = "Juniper GraphQL integration with Rocket" +license = "BSD-2-Clause" +documentation = "https://docs.rs/juniper_rocket" +repository = "https://github.com/graphql-rust/juniper_rocket" + +[dependencies] +serde = { version = "1.0.8" } +serde_derive = {version="1.0.8" } +serde_json = { version = "1.0.2" } +rocket = { version = "0.3.0" } +rocket_codegen = { version = "0.3.0" } +juniper = { version = "0.8.1" , git = "https://github.com/graphql-rust/juniper" } + +[badges] +travis-ci = { repository = "mhallin/juniper" } +appveyor = { repository = "mhallin/juniper" } + +[dev-dependencies] +juniper = { version = "0.8.1", path = "../juniper", features=["expose-test-schema", "serde_json"], git = "https://github.com/graphql-rust/juniper" } diff --git a/juniper_rocket/LICENSE b/juniper_rocket/LICENSE new file mode 100644 index 00000000..0ccd1e17 --- /dev/null +++ b/juniper_rocket/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2016, Magnus Hallin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/juniper_rocket/README.md b/juniper_rocket/README.md new file mode 100644 index 00000000..b099f940 --- /dev/null +++ b/juniper_rocket/README.md @@ -0,0 +1,31 @@ +# juniper_rocket + +[![Build Status](https://travis-ci.org/graphql-rust/juniper_rocket.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper_rocket) +[![Build status](https://ci.appveyor.com/api/projects/status/9j9bvj7q05jcxw2v?svg=true)](https://ci.appveyor.com/project/theduke/juniper-rocket) +[![Crates.io](https://img.shields.io/crates/v/juniper_rocket.svg?maxAge=2592000)](https://crates.io/crates/juniper_rocket) +[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql) + +This repository contains the [Rocket][Rocket] web server integration for [Juniper][Juniper], a [GraphQL][GraphQL] +implementation for Rust. + +## Documentation + +Once the crate is published, the documentation will be on [docs.rs][documentation]. + +For now, please consult the example below. + +## Examples + +Check [examples/rocket_server.rs][example] for example code of a working Rocket server with GraphQL handlers. + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Rocket]: https://rocket.rs +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_rocket +[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs diff --git a/juniper_rocket/examples/rocket_server.rs b/juniper_rocket/examples/rocket_server.rs new file mode 100644 index 00000000..82f42ea2 --- /dev/null +++ b/juniper_rocket/examples/rocket_server.rs @@ -0,0 +1,51 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; +extern crate juniper; +extern crate juniper_rocket; + +use rocket::response::content; +use rocket::State; + +use juniper::tests::model::Database; +use juniper::{EmptyMutation, RootNode}; + +type Schema = RootNode<'static, Database, EmptyMutation>; + +#[get("/")] +fn graphiql() -> content::Html { + juniper_rocket::graphiql_source("/graphql") +} + +#[get("/graphql?")] +fn get_graphql_handler( + context: State, + request: juniper_rocket::GraphQLRequest, + schema: State, +) -> juniper_rocket::GraphQLResponse { + request.execute(&schema, &context) +} + +#[post("/graphql", data = "")] +fn post_graphql_handler( + context: State, + request: juniper_rocket::GraphQLRequest, + schema: State, +) -> juniper_rocket::GraphQLResponse { + request.execute(&schema, &context) +} + +fn main() { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new( + Database::new(), + EmptyMutation::::new(), + )) + .mount( + "/", + routes![graphiql, get_graphql_handler, post_graphql_handler], + ) + .launch(); +} diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs new file mode 100644 index 00000000..262fcd12 --- /dev/null +++ b/juniper_rocket/src/lib.rs @@ -0,0 +1,232 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate juniper; +extern crate serde_json; +extern crate rocket; + +use std::io::{Cursor, Read}; +use std::error::Error; + +use rocket::Request; +use rocket::request::{FormItems, FromForm}; +use rocket::data::{FromData, Outcome as FromDataOutcome}; +use rocket::response::{content, Responder, Response}; +use rocket::http::{ContentType, Status}; +use rocket::Data; +use rocket::Outcome::{Failure, Forward, Success}; + +use juniper::InputValue; +use juniper::http; + +use juniper::GraphQLType; +use juniper::RootNode; + +/// Simple wrapper around an incoming GraphQL request +/// +/// See the `http` module for more information. This type can be constructed +/// automatically from both GET and POST routes by implementing the `FromForm` +/// and `FromData` traits. +pub struct GraphQLRequest(http::GraphQLRequest); + +/// Simple wrapper around the result of executing a GraphQL query +pub struct GraphQLResponse(Status, String); + +/// Generate an HTML page containing GraphiQL +pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html { + content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url)) +} + +impl GraphQLRequest { + /// Execute an incoming GraphQL query + pub fn execute( + &self, + root_node: &RootNode, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLType, + MutationT: GraphQLType, + { + let response = self.0.execute(root_node, context); + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string_pretty(&response).unwrap(); + + GraphQLResponse(status, json) + } +} + +impl<'f> FromForm<'f> for GraphQLRequest { + type Error = String; + + fn from_form(form_items: &mut FormItems<'f>, strict: bool) -> Result { + let mut query = None; + let mut operation_name = None; + let mut variables = None; + + for (key, value) in form_items { + match key.as_str() { + "query" => if query.is_some() { + return Err("Query parameter must not occur more than once".to_owned()); + } else { + query = Some(value.as_str().to_string()); + }, + "operation_name" => if operation_name.is_some() { + return Err( + "Operation name parameter must not occur more than once".to_owned(), + ); + } else { + operation_name = Some(value.as_str().to_string()); + }, + "variables" => if variables.is_some() { + return Err( + "Variables parameter must not occur more than once".to_owned(), + ); + } else { + variables = Some(serde_json::from_str::(value.as_str()) + .map_err(|err| err.description().to_owned())?); + }, + _ => if strict { + return Err(format!("Prohibited extra field '{}'", key).to_owned()); + }, + } + } + + if let Some(query) = query { + Ok(GraphQLRequest( + http::GraphQLRequest::new(query, operation_name, variables), + )) + } else { + Err("Query parameter missing".to_owned()) + } + } +} + +impl FromData for GraphQLRequest { + type Error = String; + + fn from_data(request: &Request, data: Data) -> FromDataOutcome { + if !request.content_type().map_or(false, |ct| ct.is_json()) { + return Forward(data); + } + + let mut body = String::new(); + if let Err(e) = data.open().read_to_string(&mut body) { + return Failure((Status::InternalServerError, format!("{:?}", e))); + } + + match serde_json::from_str(&body) { + Ok(value) => Success(GraphQLRequest(value)), + Err(failure) => return Failure((Status::BadRequest, format!("{}", failure))), + } + } +} + +impl<'r> Responder<'r> for GraphQLResponse { + fn respond_to(self, _: &Request) -> Result, Status> { + let GraphQLResponse(status, body) = self; + + Ok( + Response::build() + .header(ContentType::new("application", "json")) + .status(status) + .sized_body(Cursor::new(body)) + .finalize(), + ) + } +} + +#[cfg(test)] +mod tests { + + use rocket; + use rocket::Rocket; + use rocket::http::{ContentType, Method}; + use rocket::State; + + use juniper::RootNode; + use juniper::tests::model::Database; + use juniper::http::tests as http_tests; + use juniper::EmptyMutation; + + type Schema = RootNode<'static, Database, EmptyMutation>; + + + #[get("/?")] + fn get_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + #[post("/", data = "")] + fn post_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + struct TestRocketIntegration { + rocket: Rocket, + } + + /* + + impl http_tests::HTTPIntegration for TestRocketIntegration + { + fn get(&self, url: &str) -> http_tests::TestResponse { + make_test_response(&self.rocket, MockRequest::new( + Method::Get, + url)) + } + + fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { + make_test_response( + &self.rocket, + MockRequest::new( + Method::Post, + url, + ).header(ContentType::JSON).body(body)) + } + } + + #[test] + fn test_rocket_integration() { + let integration = TestRocketIntegration { + rocket: make_rocket(), + }; + + http_tests::run_http_test_suite(&integration); + } + + fn make_rocket() -> Rocket { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new(Database::new(), EmptyMutation::::new())) + .mount("/", routes![post_graphql_handler, get_graphql_handler]) + } + + fn make_test_response<'r>(rocket: &'r Rocket, mut request: MockRequest<'r>) -> http_tests::TestResponse { + let mut response = request.dispatch_with(&rocket); + let status_code = response.status().code as i32; + let content_type = response.header_values("content-type").collect::>().into_iter().next() + .expect("No content type header from handler").to_owned(); + let body = response.body().expect("No body returned from GraphQL handler").into_string(); + + http_tests::TestResponse { + status_code: status_code, + body: body, + content_type: content_type, + } + } + + */ +} From 4d6a99fe4e3553f54ff40961d12b15c3a740707f Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 16:09:08 +0100 Subject: [PATCH 18/26] Fix up Cargo.toml files and documentation. --- Cargo.toml | 1 + README.md | 178 ++++++++++++-------------------------- juniper/src/lib.rs | 145 +++++++++++++------------------ juniper_iron/Cargo.toml | 11 ++- juniper_iron/README.md | 24 ++--- juniper_iron/src/lib.rs | 28 +++--- juniper_rocket/Cargo.toml | 10 ++- juniper_rocket/README.md | 24 ++--- juniper_rocket/src/lib.rs | 38 ++++++++ 9 files changed, 211 insertions(+), 248 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cee85539..d6e64937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ members = [ "juniper_codegen", "juniper_tests", "juniper_iron", + "juniper_rocket", ] diff --git a/README.md b/README.md index a1e75fff..53aa122d 100644 --- a/README.md +++ b/README.md @@ -13,159 +13,87 @@ --- [GraphQL][graphql] is a data query language developed by Facebook intended to -serve mobile and web application frontends. Juniper makes it possible to write -GraphQL servers in Rust that are type-safe and blazingly fast. +serve mobile and web application frontends. + +*Juniper* makes it possible to write GraphQL servers in Rust that are +type-safe and blazingly fast. We also try to make declaring and resolving +GraphQL schemas as convenient as possible as Rust will allow. Juniper does not include a web server - instead it provides building blocks to make integration with existing servers straightforward. It optionally provides a -pre-built integration for the [Iron][iron] and [Rocket] frameworks. +pre-built integration for the [Iron][iron] and [Rocket] frameworks, including +embedded [Graphiql][graphiql] for easy debugging. * [Cargo crate](https://crates.io/crates/juniper) -* [API Documentation](https://docs.rs/juniper) +* [API Reference][docsrs] +* [Book][book]: Guides and Examples -## Installation -Add Juniper to your Cargo.toml: +## Getting Started -```toml -[dependencies] -juniper = { git = "https://github.com/graphql-rust/juniper" } -``` +The best place to get started is the [Juniper Book][book], which contains +guides with plenty of examples, covering all features of Juniper. -If you want Iron integration, you need to depend on the `juniper_iron` crate. -feature flag: +To get started quickly and get a feel for Juniper, check out the +[Quickstart][book_quickstart] section. -```toml -[dependencies] -juniper = { git = "https://github.com/graphql-rust/juniper" } -juniper_iron = { git = "https://github.com/graphql-rust/juniper_iron" } +For specific information about macros, types and the Juniper api, the +[API Reference][docsrs] is the best place to look. -``` +You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex +schema including polymorphism with traits and interfaces. +For an example of web framework integration, +see the [rocket][rocket_examples] and [iron][iron_examples] examples folders. -If you want Rocket integration, you need to depend on the `juniper_rocket` crate. - -**Note**: Until 0.9 is released, you will need to use a Git dependency to the current master branch. - -```toml -[dependencies] -juniper = { git = "https://github.com/graphql-rust/juniper" } -juniper_rocket = { git = "https://github.com/graphql-rust/juniper_rocket" } -``` - -## Building schemas - -GraphQL turns the REST paradigm as it's usually implemented on its head: instead -of providing a fixed structure of all types and relations in the system, GraphQL -defines a _schema_ which your users can query. The schema defines all types, -fields, and relations available, while the query defines which fields and -relations a user is interested in. - -Juniper expects you to already have the types you want to expose in GraphQL as -Rust data types. Other than that, it doesn't make any assumptions whether they -are stored in a database or just in memory. Exposing a type is a matter of -implementing the `GraphQLType` for your type. To make things a bit easier, -Juniper comes with a set of macros that help you do this, based on what kind of -type you want to expose. Let's look at how one could expose parts of the [Star -Wars Schema][swschema]: - -```rust -#[macro_use] extern crate juniper; - -use juniper::FieldResult; - -enum Episode { - NewHope, - Empire, - Jedi, -} - -struct Human { - id: String, - name: String, - appears_in: Vec, - home_planet: String, -} - -graphql_enum!(Episode { - Episode::NewHope => "NEW_HOPE", - Episode::Empire => "EMPIRE", - Episode::Jedi => "JEDI", -}); - -graphql_object!(Human: () |&self| { - description: "A humanoid creature in the Star Wars universe" - - // Field resolver methods look almost like ordinary methods. The macro picks - // up arguments and return types for the introspection schema, and verifies - // it during compilation. - field id() -> FieldResult<&String> { - Ok(&self.id) - } - - field name() -> FieldResult<&String> { - Ok(&self.name) - } - - field appears_in() -> FieldResult<&Vec> { - Ok(&self.appears_in) - } - - field home_planet() -> FieldResult<&String> { - Ok(&self.home_planet) - } -}); -``` - -You can find the full example in [src/tests/schema.rs][test_schema_rs], -including polymorphism with traits and interfaces. For an example of framework -integration, see the [rocket][rocket_examples] and [iron][iron_examples] examples folders. ## Features Juniper supports the full GraphQL query language according to the -[specification][graphql_spec], including the introspective schema and all -validations. It does not, however, support the schema language. +[specification][graphql_spec], including interfaces, unions, schema +introspection, and validations. +It does not, however, support the schema language. As an exception to other GraphQL libraries for other languages, Juniper builds non-null types by default. A field of type `Vec` will be converted into `[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be `Option>>`. +## Integrations + +### Data types + +Juniper has automatic integration with some very common Rust crates to make +building schemas a breeze. The types from these crates will be usable in +your Schemas automatically. + +* [uuid][uuid] +* [url][url] +* [chrono][chrono] + +### Web Frameworks + +* [rocket][rocket] +* [iron][iron] + + ## API Stability Juniper has not reached 1.0 yet, thus some API instability should be expected. -## 1.0 Roadmap - -> Version 0.8.1 probably be re-released as 1.0 to indicate API stability. - -The road to 1.0 _focuses_ on two aspects: making sure the API hasn't got any -obvious dead-ends with respect to probable future features, and improving test -coverage for general execution. There are some chores that need to be completed -as well. - -* [X] Extensive execution testing - * [X] Sending input objects and partial input objects in variables - * [X] Sending enums in variables - * [X] General input value type checking and validation -* [X] Improve helper macros - * [X] `graphql_union!` helper completely missing - * [X] `graphql_input_object!` helper completely missing - * [X] Add support for deprecating things - * [X] Custom enum values and descriptions - * [X] Improved syntax for fields that can't fail resolution - make - `FieldResult` optional maybe? -* [X] Investigate asynchronous execution - implementing it is not necessary, but - at least look at what API changes will be needed for us to hook into - [Tokio][tokio], for example. -* [X] Larger examples to illustrate things like database access - [graphql]: http://graphql.org +[graphiql]: https://github.com/graphql/graphiql [iron]: http://ironframework.io -[swschema]: http://graphql.org/docs/typesystem/ [graphql_spec]: http://facebook.github.io/graphql -[test_schema_rs]: juniper/src/tests/schema.rs +[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/src/tests/schema.rs [tokio]: https://github.com/tokio-rs/tokio -[rocket_examples]: https://github.com/graphql-rust/juniper_rocket/tree/master/examples -[iron_examples]: https://github.com/graphql-rust/juniper_iron/tree/master/examples +[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples +[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples [Rocket]: https://rocket.rs +[book]: https://graphql-rust.github.io/juniper-book +[book_quickstart]: https://graphql-rust.github.io/juniper-book/quickstart.html +[docsrs]: https://docs.rs/juniper + +[uuid]: https://crates.io/crates/uuid +[url]: https://crates.io/crates/url +[chrono]: https://crates.io/crates/chrono + diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index e034d3c7..cf5bdd53 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -2,115 +2,90 @@ # GraphQL -[GraphQL][1] is a data query language developed by Facebook intended to serve -mobile and web application frontends. A server provides a schema, containing -types and fields that applications can query. Queries are hierarchical, -composable, and statically typed. Schemas are introspective, which lets clients -statically verify their queries against a server without actually executing -them. +[GraphQL][graphql] is a data query language developed by Facebook intended to +serve mobile and web application frontends. -This library provides data types and traits to expose Rust types in a GraphQL -schema, as well as an optional integration into the [Iron framework][Iron] and -[Rocket]. It tries to keep the number of dynamic operations to a minimum, and -give you as the schema developer the control of the query execution path. +*Juniper* makes it possible to write GraphQL servers in Rust that are +type-safe and blazingly fast. We also try to make declaring and resolving +GraphQL schemas as convenient as possible as Rust will allow. -## Exposing data types +Juniper does not include a web server - instead it provides building blocks to +make integration with existing servers straightforward. It optionally provides a +pre-built integration for the [Iron][iron] and [Rocket] frameworks, including +embedded [Graphiql][graphiql] for easy debugging. -The `GraphQLType` trait is the primary interface towards application developers. -By implementing this trait, you can expose your types as either objects, enums, -interfaces, unions, or scalars. +* [Cargo crate](https://crates.io/crates/juniper) +* [API Reference][docsrs] +* [Book][book]: Guides and Examples -However, due to the dynamic nature of GraphQL's type system, doing this -manually is a bit tedious, especially in order to do it in a fully type safe -manner. -The library provides two methods of mapping your Rust data types to GraphQL schemas: custom derive -implementations and macros. +## Getting Started -```rust -# use std::collections::HashMap; -# #[macro_use] extern crate juniper; -use juniper::{Context, FieldResult}; +The best place to get started is the [Juniper Book][book], which contains +guides with plenty of examples, covering all features of Juniper. -struct User { id: String, name: String, friend_ids: Vec } -struct QueryRoot; -struct Database { users: HashMap } +To get started quickly and get a feel for Juniper, check out the +[Quickstart][book_quickstart] section. -impl Context for Database {} +For specific information about macros, types and the Juniper api, the +[API Reference][docsrs] is the best place to look. -// GraphQL objects can access a "context object" during execution. Use this -// object to provide e.g. database access to the field accessors. This object -// must implement the `Context` trait. If you don't need a context, use the -// empty tuple `()` to indicate this. -// -// In this example, we use the Database struct as our context. -graphql_object!(User: Database |&self| { +You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex +schema including polymorphism with traits and interfaces. +For an example of web framework integration, +see the [rocket][rocket_examples] and [iron][iron_examples] examples folders. - // Expose a simple field as a GraphQL string. - field id() -> &String { - &self.id - } - field name() -> &String { - &self.name - } +## Features - // FieldResult is an alias for Result, which can be - // converted to from anything that implements std::fmt::Display - simply - // return an error with a string using the ? operator from this method and - // it will be correctly inserted into the execution response. - field secret() -> FieldResult<&String> { - Err("Can't touch this".to_owned())? - } +Juniper supports the full GraphQL query language according to the +[specification][graphql_spec], including interfaces, unions, schema +introspection, and validations. +It does not, however, support the schema language. - // Field accessors can optionally take an "executor" as their first - // argument. This object can help guide query execution and provides - // access to the context instance. - // - // In this example, the context is used to convert the friend_ids array - // into actual User objects. - field friends(&executor) -> Vec<&User> { - self.friend_ids.iter() - .filter_map(|id| executor.context().users.get(id)) - .collect() - } -}); +As an exception to other GraphQL libraries for other languages, Juniper builds +non-null types by default. A field of type `Vec` will be converted into +`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be +`Option>>`. -// The context object is passed down to all referenced types - all your exposed -// types need to have the same context type. -graphql_object!(QueryRoot: Database |&self| { +## Integrations - // Arguments work just like they do on functions. - field user(&executor, id: String) -> Option<&User> { - executor.context().users.get(&id) - } -}); +### Data types -# fn main() { } -``` +Juniper has automatic integration with some very common Rust crates to make +building schemas a breeze. The types from these crates will be usable in +your Schemas automatically. -Adding per type, field, and argument documentation is possible directly from -this macro. For more in-depth information on how to expose fields and types, see -the [`graphql_object!`][3] macro. +* [uuid][uuid] +* [url][url] +* [chrono][chrono] -### Built-in object type integrations +### Web Frameworks -Juniper has [built-in integrations][object_integrations] for converting existing object types to -GraphQL objects for popular crates. +* [rocket][rocket] +* [iron][iron] -## Integrating with web servers -The most obvious usecase is to expose the GraphQL schema over an HTTP endpoint. -To support this, Juniper offers additional crates that integrate with popular web frameworks. +## API Stability -* [juniper_iron][juniper_iron]: Handlers for [Iron][Iron] -* [juniper_rocket][juniper_rocket]: Handlers for [Rocket][Rocket] +Juniper has not reached 1.0 yet, thus some API instability should be expected. -[1]: http://graphql.org -[3]: macro.graphql_object!.html -[Iron]: http://ironframework.io +[graphql]: http://graphql.org +[graphiql]: https://github.com/graphql/graphiql +[iron]: http://ironframework.io +[graphql_spec]: http://facebook.github.io/graphql +[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs +[tokio]: https://github.com/tokio-rs/tokio +[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples +[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples [Rocket]: https://rocket.rs -[object_integrations]: integrations/index.html +[book]: https://graphql-rust.github.io/juniper-book +[book_quickstart]: https://graphql-rust.github.io/juniper-book/quickstart.html +[docsrs]: https://docs.rs/juniper + +[uuid]: https://crates.io/crates/uuid +[url]: https://crates.io/crates/url +[chrono]: https://crates.io/crates/chrono */ #![warn(missing_docs)] diff --git a/juniper_iron/Cargo.toml b/juniper_iron/Cargo.toml index 464e1f7a..14595ad8 100644 --- a/juniper_iron/Cargo.toml +++ b/juniper_iron/Cargo.toml @@ -8,18 +8,23 @@ documentation = "https://docs.rs/juniper_iron" repository = "https://github.com/graphql-rust/juniper_iron" [dependencies] -serde = { version = "1.0.2" } +serde = { version = "1.0.8" } serde_json = { version = "1.0.2" } +juniper = { version = "0.9.0", path = "../juniper" } + urlencoded = { version = "0.5.0" } iron = "0.5.1" -juniper = { version = "0.8.1", git = "https://github.com/graphql-rust/juniper" } [dev-dependencies] iron-test = "^0.5.0" router = "^0.5.0" mount = "^0.3.0" logger = "^0.3.0" -juniper = { version = "0.8.1", features = ["expose-test-schema", "serde_json"], git = "https://github.com/graphql-rust/juniper" } + +[dev-dependencies.juniper] +version = "0.9.0" +features = ["expose-test-schema", "serde_json"] +path = "../juniper" [badges] travis-ci = { repository = "graphql-rust/juniper_iron" } diff --git a/juniper_iron/README.md b/juniper_iron/README.md index 592268bd..18b42a78 100644 --- a/juniper_iron/README.md +++ b/juniper_iron/README.md @@ -1,23 +1,22 @@ # juniper_iron -[![Build Status](https://travis-ci.org/graphql-rust/juniper_iron.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper_iron) -[![Build status](https://ci.appveyor.com/api/projects/status/rqguvfkl9m0g7hum?svg=true)](https://ci.appveyor.com/project/theduke/juniper-iron) -[![Crates.io](https://img.shields.io/crates/v/juniper_iron.svg?maxAge=2592000)](https://crates.io/crates/juniper_iron) -[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql) +This repository contains the [Iron][Iron] web framework integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. -This repository contains the [Iron][Iron] web framework integration for [Juniper][Juniper], a [GraphQL][GraphQL] -implementation for Rust. +For documentation, including guides and examples, check out [Juniper][Juniper]. -## Documentation - -Once the crate is published, documentation will be on [docs.rs][documentation]. - -For now, please consult the documentation comments [here](https://github.com/graphql-rust/juniper_iron/blob/master/src/lib.rs). +A basic usage example can also be found in the [Api documentation][documentation]. ## Examples Check [examples/iron_server.rs][example] for example code of a working Iron server with GraphQL handlers. +## Links + +* [Juniper][Juniper] +* [Api Reference][documetation] +* [Iron framework][Iron] + ## License This project is under the BSD-2 license. @@ -28,4 +27,5 @@ Check the LICENSE file for details. [Juniper]: https://github.com/graphql-rust/juniper [GraphQL]: http://graphql.org [documentation]: https://docs.rs/juniper_iron -[example]: https://github.com/graphql-rust/juniper_iron/blob/master/examples/iron_server.rs +[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_iron/examples/iron_server.rs + diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index 8fda09d5..9cf4ed7c 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -1,11 +1,23 @@ /*! -[Juniper][1] handlers for the [Iron][2] framework. +# juniper_iron + +This repository contains the [Iron][Iron] web framework integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [Api documentation][documentation]. + +## Links + +* [Juniper][Juniper] +* [Api Reference][documentation] +* [Iron framework][Iron] ## Integrating with Iron - For example, continuing from the schema created above and using Iron to expose the schema on an HTTP endpoint supporting both GET and POST requests: @@ -78,18 +90,14 @@ fn main() { ``` -See the [iron_server.rs][5] -example for more information on how to use these handlers. - See the the [`GraphQLHandler`][3] documentation for more information on what request methods are supported. -There's also a built-in [GraphiQL][4] handler included. -[1]: https://github.com/mhallin/Juniper -[2]: http://ironframework.io [3]: ./struct.GraphQLHandler.html -[4]: https://github.com/graphql/graphiql -[5]: https://github.com/mhallin/juniper/blob/master/juniper_iron/examples/iron_server.rs +[Iron]: https://github.com/iron/iron +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_iron */ diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml index 6f1c6fac..4f1ef3f4 100644 --- a/juniper_rocket/Cargo.toml +++ b/juniper_rocket/Cargo.toml @@ -11,13 +11,17 @@ repository = "https://github.com/graphql-rust/juniper_rocket" serde = { version = "1.0.8" } serde_derive = {version="1.0.8" } serde_json = { version = "1.0.2" } +juniper = { version = "0.9.0" , path = "../juniper"} + rocket = { version = "0.3.0" } rocket_codegen = { version = "0.3.0" } -juniper = { version = "0.8.1" , git = "https://github.com/graphql-rust/juniper" } [badges] travis-ci = { repository = "mhallin/juniper" } appveyor = { repository = "mhallin/juniper" } -[dev-dependencies] -juniper = { version = "0.8.1", path = "../juniper", features=["expose-test-schema", "serde_json"], git = "https://github.com/graphql-rust/juniper" } +[dev-dependencies.juniper] +version = "0.9.0" +features = ["expose-test-schema", "serde_json"] +path = "../juniper" + diff --git a/juniper_rocket/README.md b/juniper_rocket/README.md index b099f940..632db95a 100644 --- a/juniper_rocket/README.md +++ b/juniper_rocket/README.md @@ -1,22 +1,24 @@ # juniper_rocket -[![Build Status](https://travis-ci.org/graphql-rust/juniper_rocket.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper_rocket) -[![Build status](https://ci.appveyor.com/api/projects/status/9j9bvj7q05jcxw2v?svg=true)](https://ci.appveyor.com/project/theduke/juniper-rocket) -[![Crates.io](https://img.shields.io/crates/v/juniper_rocket.svg?maxAge=2592000)](https://crates.io/crates/juniper_rocket) -[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql) - -This repository contains the [Rocket][Rocket] web server integration for [Juniper][Juniper], a [GraphQL][GraphQL] -implementation for Rust. +This repository contains the [Rocket][Rocket] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. ## Documentation -Once the crate is published, the documentation will be on [docs.rs][documentation]. +For documentation, including guides and examples, check out [Juniper][Juniper]. -For now, please consult the example below. +A basic usage example can also be found in the [Api documentation][documentation]. ## Examples -Check [examples/rocket_server.rs][example] for example code of a working Rocket server with GraphQL handlers. +Check [examples/rocket_server.rs][example] for example code of a working Rocket +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [Api Reference][documetation] +* [Rocket][Iron] ## License @@ -29,3 +31,5 @@ Check the LICENSE file for details. [GraphQL]: http://graphql.org [documentation]: https://docs.rs/juniper_rocket [example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs + + diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index 262fcd12..8018d184 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -1,3 +1,41 @@ +/*! + +# juniper_rocket + +This repository contains the [Rocket][Rocket] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. + +## Documentation + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [Api documentation][documentation]. + +## Examples + +Check [examples/rocket_server.rs][example] for example code of a working Rocket +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [Api Reference][documentation] +* [Rocket][Rocket] + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Rocket]: https://rocket.rs +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_rocket +[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs + +*/ + #![feature(plugin)] #![plugin(rocket_codegen)] From 00e80bbe888bf3a2912f1743cd2f7b0688b7ece1 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 23:37:40 +0100 Subject: [PATCH 19/26] (tests) Only build juniper_rocket on nightly --- juniper_rocket/Makefile.toml | 18 ++++++++++++++++++ juniper_tests/Cargo.toml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 juniper_rocket/Makefile.toml diff --git a/juniper_rocket/Makefile.toml b/juniper_rocket/Makefile.toml new file mode 100644 index 00000000..5c624642 --- /dev/null +++ b/juniper_rocket/Makefile.toml @@ -0,0 +1,18 @@ +[tasks.scoped-ci-flow] +description = "CI task will run cargo build and cargo test with verbose output" +dependencies = [ + "pre-ci-flow", + "pre-build", + "build-verbose", + "post-build", + "pre-test", + "test-verbose", + "post-test", + "ci-coverage-flow", + "post-ci-flow" +] + +[tasks.ci-flow] +condition = { channels = ["nightly"] } +run_task = "scoped-ci-flow" +dependencies = [] diff --git a/juniper_tests/Cargo.toml b/juniper_tests/Cargo.toml index 7b87fa78..2e3a586d 100644 --- a/juniper_tests/Cargo.toml +++ b/juniper_tests/Cargo.toml @@ -3,7 +3,7 @@ name = "juniper_tests" version = "0.1.0" [dependencies] -juniper = { path = "../juniper" } +juniper = { version = "0.9.0", path = "../juniper" } serde_json = { version = "1" } [dev-dependencies] From 9e424a8630d28135e743a999f1082497f1030dad Mon Sep 17 00:00:00 2001 From: Sagie Gur-Ari Date: Sun, 3 Dec 2017 11:31:56 +0200 Subject: [PATCH 20/26] v09 build fix for windows (#115) Fix cargo make makefiles for only running the juniper_rocket build on nightly. --- Makefile.toml | 4 ++++ juniper_rocket/Makefile.toml | 18 ------------------ 2 files changed, 4 insertions(+), 18 deletions(-) create mode 100644 Makefile.toml delete mode 100644 juniper_rocket/Makefile.toml diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 00000000..559be3b7 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,4 @@ + +[tasks.init] +condition = { channels = ["beta", "stable"] } +env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "juniper_rocket;juniper_tests" } diff --git a/juniper_rocket/Makefile.toml b/juniper_rocket/Makefile.toml deleted file mode 100644 index 5c624642..00000000 --- a/juniper_rocket/Makefile.toml +++ /dev/null @@ -1,18 +0,0 @@ -[tasks.scoped-ci-flow] -description = "CI task will run cargo build and cargo test with verbose output" -dependencies = [ - "pre-ci-flow", - "pre-build", - "build-verbose", - "post-build", - "pre-test", - "test-verbose", - "post-test", - "ci-coverage-flow", - "post-ci-flow" -] - -[tasks.ci-flow] -condition = { channels = ["nightly"] } -run_task = "scoped-ci-flow" -dependencies = [] From e4cf21086c4691fb17e1bacb8542204b423fa411 Mon Sep 17 00:00:00 2001 From: theduke Date: Sun, 3 Dec 2017 11:23:43 +0100 Subject: [PATCH 21/26] (ci) Stop using cargo-make on appveyor. Neccessary due to failing builds... --- appveyor.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b65f8567..6d6a0df8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,4 +49,9 @@ install: build: false test_script: - - cargo make workspace-ci-flow --no-workspace + - cd juniper && cargo build --verbose && cargo test --verbose && cd .. + - cd juniper_codegen && cargo build --verbose && cargo test --verbose && cd .. + - cd juniper_tests && cargo build --verbose && cargo test --verbose && cd .. + - cd juniper_iron && cargo build --verbose && cargo test --verbose && cd .. + - IF NOT %TARGET% == %TARGET:msvc=% ( IF %CHANNEL% == "nightly" ( cd juniper_rocket && cargo test --verbose && cargo build --verbose && cd .. ) ) + From b6d9c337d02a4876f79846c9b32d115ca955cace Mon Sep 17 00:00:00 2001 From: theduke Date: Sun, 3 Dec 2017 13:12:41 +0100 Subject: [PATCH 22/26] Fix appveyor badge in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53aa122d..3802015b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > GraphQL server library for Rust [![Build Status](https://travis-ci.org/graphql-rust/juniper.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper) -[![Build status](https://ci.appveyor.com/api/projects/status/vsrwmsh9wobxugbs?svg=true)](https://ci.appveyor.com/project/theduke/juniper/branch/master) +[![Build status](https://ci.appveyor.com/api/projects/status/xav6tor6biu617uu?svg=true)](https://ci.appveyor.com/project/theduke/juniper) [![codecov](https://codecov.io/gh/graphql-rust/juniper/branch/master/graph/badge.svg)](https://codecov.io/gh/graphql-rust/juniper) [![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper) [![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql) @@ -32,7 +32,7 @@ embedded [Graphiql][graphiql] for easy debugging. ## Getting Started The best place to get started is the [Juniper Book][book], which contains -guides with plenty of examples, covering all features of Juniper. +guides with plenty of examples, covering all features of Juniper. (very much WIP) To get started quickly and get a feel for Juniper, check out the [Quickstart][book_quickstart] section. From e37b7cc77d2f491cd2f5f0ae24063c57b66d471b Mon Sep 17 00:00:00 2001 From: theduke Date: Sun, 3 Dec 2017 13:12:45 +0100 Subject: [PATCH 23/26] Fix repository links in iron/rocket Cargo.toml --- juniper_iron/Cargo.toml | 2 +- juniper_rocket/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/juniper_iron/Cargo.toml b/juniper_iron/Cargo.toml index 14595ad8..bcfbe659 100644 --- a/juniper_iron/Cargo.toml +++ b/juniper_iron/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Magnus Hallin "] description = "Iron integration for juniper" license = "BSD-2-Clause" documentation = "https://docs.rs/juniper_iron" -repository = "https://github.com/graphql-rust/juniper_iron" +repository = "https://github.com/graphql-rust/juniper" [dependencies] serde = { version = "1.0.8" } diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml index 4f1ef3f4..ece4d690 100644 --- a/juniper_rocket/Cargo.toml +++ b/juniper_rocket/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Magnus Hallin "] description = "Juniper GraphQL integration with Rocket" license = "BSD-2-Clause" documentation = "https://docs.rs/juniper_rocket" -repository = "https://github.com/graphql-rust/juniper_rocket" +repository = "https://github.com/graphql-rust/juniper" [dependencies] serde = { version = "1.0.8" } From 1d6645cc1c918d273a53d3a1de0d2f2981e8f97a Mon Sep 17 00:00:00 2001 From: theduke Date: Sun, 3 Dec 2017 13:14:29 +0100 Subject: [PATCH 24/26] (ci) Drop 32bit builds on appveyor Who uses 32bit anymore anyway? This cuts appveyor build times in half. --- appveyor.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6d6a0df8..8a47fe8c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,28 +5,16 @@ environment: matrix: # Stable channel - - TARGET: i686-pc-windows-gnu - CHANNEL: stable - - TARGET: i686-pc-windows-msvc - CHANNEL: stable - TARGET: x86_64-pc-windows-gnu CHANNEL: stable - TARGET: x86_64-pc-windows-msvc CHANNEL: stable # Beta channel - - TARGET: i686-pc-windows-gnu - CHANNEL: beta - - TARGET: i686-pc-windows-msvc - CHANNEL: beta - TARGET: x86_64-pc-windows-gnu CHANNEL: beta - TARGET: x86_64-pc-windows-msvc CHANNEL: beta # Nightly channel - - TARGET: i686-pc-windows-gnu - CHANNEL: nightly - - TARGET: i686-pc-windows-msvc - CHANNEL: nightly - TARGET: x86_64-pc-windows-gnu CHANNEL: nightly - TARGET: x86_64-pc-windows-msvc From 7344b53f57f072029ca2c5a5130b5e12b39b655a Mon Sep 17 00:00:00 2001 From: theduke Date: Sun, 3 Dec 2017 15:05:20 +0100 Subject: [PATCH 25/26] Add changelog for 0.9.0 --- changelog/0.9.0.md | 89 ++++++++++++++++++++++++++++++ CHANGELOG.md => changelog/older.md | 16 ------ 2 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 changelog/0.9.0.md rename CHANGELOG.md => changelog/older.md (95%) diff --git a/changelog/0.9.0.md b/changelog/0.9.0.md new file mode 100644 index 00000000..bc9e2f7a --- /dev/null +++ b/changelog/0.9.0.md @@ -0,0 +1,89 @@ +# [0.9.0] 2017-12-03 + +## Changes + +This is the first release in a long time. +Quite a few changes have accumulated since `0.8.1`, including multiple breaking +changes. + +### Custom derive & macros + +Juniper has gained custom derive implementations for input objects, objects and +enums. + +* `#[derive(GraphQLInputObject)]` +* `#[derive(GraphQLEnum)]` +* `#[derive(GraphQLObject)]` + +The `graphql_enum!` and `graphql_input_object!` macros did not provide any more +benefits, so they have been removed! +All functionality is now covered by custom derive. +Check the [docs](https://graphql-rust.github.io) to find out more. + +### Web framework integrations - Iron & Rocket + +The iron and rocket integrations were removed from the main crate, and are now +available via the [juniper_iron](https://crates.io/crates/juniper_iron) and +[juniper_rocket](https://crates.io/crates/juniper_rocket) crates. + +### FieldError - Custom data + +The `FieldError` type now supports custom data with the `Value` type from +serde_json. Use this to populate the `data` field in returned errors. + +### Dynamic Schemas + +Juniper has gained support for dynamic schemas, thanks to @srijs. + +That also means the type of `RootNode` has changed to include a lifetime. + +The repository was restructured to a multi crate workspace to enable several new +features like custom_derive and an extracted parser. + +#[#66](https://github.com/graphql-rust/juniper/pull/66) + +### Data Type Integrations + +Integrations with multiple popular crates was added to make working with them +easier. + +* uuid +* url +* chrono + +### Field Order + +To better comply with the specification, order of requested fields is +now preserved. + +[#82](https://github.com/graphql-rust/juniper/issues/82 + +### From/ToInputValue + +The `::from` and `::to` methods in `From/ToInputValue` were renamed to +`from/to_input_value()` to not conflict with other methods. + +[#90](https://github.com/graphql-rust/juniper/pull/90) + +### Other changes + +* Several small performance improvements +* Use [fnv](https://github.com/servo/rust-fnv) hash map for better performance + + +## Contributors + +A big shoutout to the many contributors for this version, sorted alphabetically. + +* Cameron Eldridge +* Christian Legnitto +* Jacob Haslehurst +* Jane Keibler +* Magnus Hallin +* rushmorem +* Rushmore Mushambi +* Sagie Gur-Ari +* Sam Rijs +* Stanko Krtalić +* theduke +* thomas-jeepe diff --git a/CHANGELOG.md b/changelog/older.md similarity index 95% rename from CHANGELOG.md rename to changelog/older.md index b8b294cd..182f201b 100644 --- a/CHANGELOG.md +++ b/changelog/older.md @@ -1,22 +1,6 @@ Change log ========== -## [Unreleased] - -The repository was restructured to a multi crate workspace to enable several new features like custom_derive and an extracted parser. - -### New features - -* New juniper_codegen crate which provides custom derives: - * `#[derive(GraphQLInputObject)]` - * `#[derive(GraphQLEnum)]` - * `#[derive(GraphQLObject)]` - -## Breaking changes - -* To better comply with the specification, order of requested fields is - now preserved. - ([#82](https://github.com/graphql-rust/juniper/issues/82) ## [0.8.1] – 2017-06-15 From ed58f2671b19d8e4b57cacfcb3f93826a74eb917 Mon Sep 17 00:00:00 2001 From: theduke Date: Sun, 3 Dec 2017 15:14:23 +0100 Subject: [PATCH 26/26] Final readme and cargo.toml fixups --- README.md | 4 ++-- juniper/Cargo.toml | 2 +- juniper/src/lib.rs | 4 ++-- juniper_iron/Cargo.toml | 2 +- juniper_rocket/Cargo.toml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3802015b..6d525c61 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,8 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. [rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples [iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples [Rocket]: https://rocket.rs -[book]: https://graphql-rust.github.io/juniper-book -[book_quickstart]: https://graphql-rust.github.io/juniper-book/quickstart.html +[book]: https://graphql-rust.github.io +[book_quickstart]: https://graphql-rust.github.io/quickstart.html [docsrs]: https://docs.rs/juniper [uuid]: https://crates.io/crates/uuid diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 6c28d2b0..93d74355 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -34,7 +34,7 @@ juniper_codegen = { version = "0.9.0", path = "../juniper_codegen" } fnv = "1.0.3" ordermap = { version = "0.2.11", features = ["serde-1"] } serde = { version = "1.0.8" } -serde_derive = {version="1.0.8" } +serde_derive = {version="1.0.2" } chrono = { version = "0.4.0", optional = true } serde_json = { version="1.0.2", optional = true } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index cf5bdd53..ede56a78 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -79,8 +79,8 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. [rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples [iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples [Rocket]: https://rocket.rs -[book]: https://graphql-rust.github.io/juniper-book -[book_quickstart]: https://graphql-rust.github.io/juniper-book/quickstart.html +[book]: https://graphql-rust.github.io/ +[book_quickstart]: https://graphql-rust.github.io/quickstart.html [docsrs]: https://docs.rs/juniper [uuid]: https://crates.io/crates/uuid diff --git a/juniper_iron/Cargo.toml b/juniper_iron/Cargo.toml index bcfbe659..edb373c3 100644 --- a/juniper_iron/Cargo.toml +++ b/juniper_iron/Cargo.toml @@ -8,7 +8,7 @@ documentation = "https://docs.rs/juniper_iron" repository = "https://github.com/graphql-rust/juniper" [dependencies] -serde = { version = "1.0.8" } +serde = { version = "1.0.2" } serde_json = { version = "1.0.2" } juniper = { version = "0.9.0", path = "../juniper" } diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml index ece4d690..e10e4402 100644 --- a/juniper_rocket/Cargo.toml +++ b/juniper_rocket/Cargo.toml @@ -8,8 +8,8 @@ documentation = "https://docs.rs/juniper_rocket" repository = "https://github.com/graphql-rust/juniper" [dependencies] -serde = { version = "1.0.8" } -serde_derive = {version="1.0.8" } +serde = { version = "1.0.2" } +serde_derive = {version="1.0.2" } serde_json = { version = "1.0.2" } juniper = { version = "0.9.0" , path = "../juniper"}