From a9df7f004a89d74bc5de6ab32b0f1f21abc8056a Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 9 Dec 2021 15:02:24 +0300 Subject: [PATCH 1/8] Support `@specifiedBy(url: "...")` directive --- .../src/codegen/derive_scalar.rs | 29 ++++- .../juniper_tests/src/codegen/impl_scalar.rs | 48 ++++++++ .../src/codegen/scalar_value_transparent.rs | 7 +- .../src/executor_tests/introspection/mod.rs | 2 + juniper/src/introspection/mod.rs | 4 +- juniper/src/introspection/query.graphql | 3 + .../query_without_descriptions.graphql | 2 + juniper/src/schema/meta.rs | 23 ++++ juniper/src/schema/model.rs | 14 ++- juniper/src/schema/schema.rs | 15 +++ juniper/src/tests/schema_introspection.rs | 112 ++++++++++++++++++ juniper/src/validation/test_harness.rs | 6 + juniper_codegen/src/derive_scalar_value.rs | 16 ++- juniper_codegen/src/impl_scalar.rs | 9 +- juniper_codegen/src/lib.rs | 7 +- juniper_codegen/src/util/mod.rs | 17 ++- 16 files changed, 299 insertions(+), 15 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/derive_scalar.rs b/integration_tests/juniper_tests/src/codegen/derive_scalar.rs index 8010cbea9..49e272383 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_scalar.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_scalar.rs @@ -6,7 +6,11 @@ use juniper::{ use crate::custom_scalar::MyScalarValue; #[derive(Debug, PartialEq, Eq, Hash, juniper::GraphQLScalarValue)] -#[graphql(transparent, scalar = MyScalarValue)] +#[graphql( + transparent, + scalar = MyScalarValue, + specified_by_url = "https://tools.ietf.org/html/rfc4122", +)] pub struct LargeId(i64); #[derive(juniper::GraphQLObject)] @@ -49,6 +53,29 @@ fn test_scalar_value_large_id() { assert_eq!(output, InputValue::scalar(num)); } +#[tokio::test] +async fn test_scalar_value_large_specified_url() { + let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value( + Query, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + + let doc = r#"{ + __type(name: "LargeId") { + specifiedByUrl + } + }"#; + + assert_eq!( + execute(doc, None, &schema, &Variables::::new(), &()).await, + Ok(( + graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc4122"}}), + vec![] + )), + ); +} + #[tokio::test] async fn test_scalar_value_large_query() { let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value( diff --git a/integration_tests/juniper_tests/src/codegen/impl_scalar.rs b/integration_tests/juniper_tests/src/codegen/impl_scalar.rs index 3432c2f6a..fe428ec60 100644 --- a/integration_tests/juniper_tests/src/codegen/impl_scalar.rs +++ b/integration_tests/juniper_tests/src/codegen/impl_scalar.rs @@ -9,6 +9,7 @@ struct DefaultName(i32); struct OtherOrder(i32); struct Named(i32); struct ScalarDescription(i32); +struct ScalarSpecifiedByUrl(i32); struct Generated(String); struct Root; @@ -87,6 +88,21 @@ impl GraphQLScalar for ScalarDescription { } } +#[graphql_scalar(specified_by_url = "https://tools.ietf.org/html/rfc4122")] +impl GraphQLScalar for ScalarSpecifiedByUrl { + fn resolve(&self) -> Value { + Value::scalar(self.0) + } + + fn from_input_value(v: &InputValue) -> Option { + v.as_scalar_value::().map(|i| ScalarSpecifiedByUrl(*i)) + } + + fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> { + ::from_str(value) + } +} + macro_rules! impl_scalar { ($name: ident) => { #[graphql_scalar] @@ -127,6 +143,9 @@ impl Root { fn scalar_description() -> ScalarDescription { ScalarDescription(0) } + fn scalar_specified_by_url() -> ScalarSpecifiedByUrl { + ScalarSpecifiedByUrl(0) + } fn generated() -> Generated { Generated("foo".to_owned()) } @@ -287,6 +306,7 @@ async fn scalar_description_introspection() { __type(name: "ScalarDescription") { name description + specifiedByUrl } } "#; @@ -302,6 +322,34 @@ async fn scalar_description_introspection() { "A sample scalar, represented as an integer", )), ); + assert_eq!( + type_info.get_field_value("specifiedByUrl"), + Some(&graphql_value!(null)), + ); + }) + .await; +} + +#[tokio::test] +async fn scalar_specified_by_url_introspection() { + let doc = r#" + { + __type(name: "ScalarSpecifiedByUrl") { + name + specifiedByUrl + } + } + "#; + + run_type_info_query(doc, |type_info| { + assert_eq!( + type_info.get_field_value("name"), + Some(&graphql_value!("ScalarSpecifiedByUrl")), + ); + assert_eq!( + type_info.get_field_value("specifiedByUrl"), + Some(&graphql_value!("https://tools.ietf.org/html/rfc4122")), + ); }) .await; } diff --git a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs index f2a45af1f..220fe8a33 100644 --- a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs +++ b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs @@ -14,7 +14,7 @@ struct CustomUserId(String); /// The doc comment... #[derive(GraphQLScalarValue, Debug, Eq, PartialEq)] -#[graphql(transparent)] +#[graphql(transparent, specified_by_url = "https://tools.ietf.org/html/rfc4122")] struct IdWithDocComment(i32); #[derive(GraphQLObject)] @@ -64,6 +64,7 @@ fn test_scalar_value_custom() { let meta = CustomUserId::meta(&(), &mut registry); assert_eq!(meta.name(), Some("MyUserId")); assert_eq!(meta.description(), Some("custom description...")); + assert_eq!(meta.specified_by_url(), None); let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap(); let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap(); @@ -79,4 +80,8 @@ fn test_scalar_value_doc_comment() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = IdWithDocComment::meta(&(), &mut registry); assert_eq!(meta.description(), Some("The doc comment...")); + assert_eq!( + meta.specified_by_url(), + Some("https://tools.ietf.org/html/rfc4122") + ); } diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 5778d8cb6..27f91d7d5 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -490,6 +490,7 @@ async fn scalar_introspection() { name kind description + specifiedByUrl fields { name } interfaces { name } possibleTypes { name } @@ -525,6 +526,7 @@ async fn scalar_introspection() { "name": "SampleScalar", "kind": "SCALAR", "description": null, + "specifiedByUrl": null, "fields": null, "interfaces": null, "possibleTypes": null, diff --git a/juniper/src/introspection/mod.rs b/juniper/src/introspection/mod.rs index 39011324d..aedd4ef9a 100644 --- a/juniper/src/introspection/mod.rs +++ b/juniper/src/introspection/mod.rs @@ -1,10 +1,10 @@ -/// From +/// From pub(crate) const INTROSPECTION_QUERY: &str = include_str!("./query.graphql"); pub(crate) const INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS: &str = include_str!("./query_without_descriptions.graphql"); /// The desired GraphQL introspection format for the canonical query -/// () +/// () pub enum IntrospectionFormat { /// The canonical GraphQL introspection query. All, diff --git a/juniper/src/introspection/query.graphql b/juniper/src/introspection/query.graphql index 38ff1797d..fe522f4ef 100644 --- a/juniper/src/introspection/query.graphql +++ b/juniper/src/introspection/query.graphql @@ -1,5 +1,6 @@ query IntrospectionQuery { __schema { + description queryType { name } @@ -15,6 +16,7 @@ query IntrospectionQuery { directives { name description + isRepeatable locations args { ...InputValue @@ -26,6 +28,7 @@ fragment FullType on __Type { kind name description + specifiedByUrl fields(includeDeprecated: true) { name description diff --git a/juniper/src/introspection/query_without_descriptions.graphql b/juniper/src/introspection/query_without_descriptions.graphql index 57aa6368d..0699bc5b3 100644 --- a/juniper/src/introspection/query_without_descriptions.graphql +++ b/juniper/src/introspection/query_without_descriptions.graphql @@ -14,6 +14,7 @@ query IntrospectionQuery { } directives { name + isRepeatable locations args { ...InputValue @@ -24,6 +25,7 @@ query IntrospectionQuery { fragment FullType on __Type { kind name + specifiedByUrl fields(includeDeprecated: true) { name args { diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 6566df910..8655028f7 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -46,6 +46,8 @@ pub struct ScalarMeta<'a, S> { pub name: Cow<'a, str>, #[doc(hidden)] pub description: Option, + #[doc(hidden)] + pub specified_by_url: Option>, pub(crate) try_parse_fn: for<'b> fn(&'b InputValue) -> bool, pub(crate) parse_fn: for<'b> fn(ScalarToken<'b>) -> Result>, } @@ -248,6 +250,18 @@ impl<'a, S> MetaType<'a, S> { } } + /// Access the specification url, if applicable + /// + /// Only custom scalars can have specification url. + pub fn specified_by_url(&self) -> Option<&str> { + match self { + Self::Scalar(ScalarMeta { + specified_by_url, .. + }) => specified_by_url.as_deref(), + _ => None, + } + } + /// Construct a `TypeKind` for a given type /// /// # Panics @@ -419,6 +433,7 @@ where ScalarMeta { name, description: None, + specified_by_url: None, try_parse_fn: try_parse_fn::, parse_fn: >::from_str, } @@ -432,6 +447,14 @@ where self } + /// Set the specification url for the given scalar type + /// + /// If a description already was set prior to calling this method, it will be overwritten. + pub fn specified_by_url(mut self, url: impl Into>) -> ScalarMeta<'a, S> { + self.specified_by_url = Some(url.into()); + self + } + /// Wrap the scalar in a generic meta type pub fn into_meta(self) -> MetaType<'a, S> { MetaType::Scalar(self) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 953904cb0..d256d30be 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{borrow::Cow, fmt}; use fnv::FnvHashMap; #[cfg(feature = "graphql-parser-integration")] @@ -49,6 +49,7 @@ pub struct RootNode< /// Metadata for a schema #[derive(Debug)] pub struct SchemaType<'a, S> { + pub(crate) description: Option>, pub(crate) types: FnvHashMap>, pub(crate) query_type_name: String, pub(crate) mutation_type_name: Option, @@ -71,6 +72,7 @@ pub struct DirectiveType<'a, S> { pub description: Option, pub locations: Vec, pub arguments: Vec>, + pub is_repeatable: bool, } #[derive(Clone, PartialEq, Eq, Debug, GraphQLEnum)] @@ -235,6 +237,7 @@ impl<'a, S> SchemaType<'a, S> { } } SchemaType { + description: None, types: registry.types, query_type_name, mutation_type_name: if &mutation_type_name != "_EmptyMutation" { @@ -251,6 +254,11 @@ impl<'a, S> SchemaType<'a, S> { } } + /// Add a description. + pub fn set_description(&mut self, description: impl Into>) { + self.description = Some(description.into()); + } + /// Add a directive like `skip` or `include`. pub fn add_directive(&mut self, directive: DirectiveType<'a, S>) { self.directives.insert(directive.name.clone(), directive); @@ -489,12 +497,14 @@ where name: &str, locations: &[DirectiveLocation], arguments: &[Argument<'a, S>], + is_repeatable: bool, ) -> DirectiveType<'a, S> { DirectiveType { name: name.to_owned(), description: None, locations: locations.to_vec(), arguments: arguments.to_vec(), + is_repeatable, } } @@ -510,6 +520,7 @@ where DirectiveLocation::InlineFragment, ], &[registry.arg::("if", &())], + false, ) } @@ -525,6 +536,7 @@ where DirectiveLocation::InlineFragment, ], &[registry.arg::("if", &())], + false, ) } diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index 5651ccff5..d20287560 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -137,6 +137,10 @@ where internal, )] impl<'a, S: ScalarValue + 'a> SchemaType<'a, S> { + fn description(&self) -> Option<&str> { + self.description.as_deref() + } + fn types(&self) -> Vec> { self.type_list() .into_iter() @@ -192,6 +196,13 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> { } } + fn specified_by_url(&self) -> Option<&str> { + match self { + Self::Concrete(t) => t.specified_by_url(), + _ => None, + } + } + fn kind(&self) -> TypeKind { match self { TypeType::Concrete(t) => t.type_kind(), @@ -401,6 +412,10 @@ impl<'a, S: ScalarValue + 'a> DirectiveType<'a, S> { &self.locations } + fn is_repeatable(&self) -> bool { + self.is_repeatable + } + fn args(&self) -> &[Argument] { &self.arguments } diff --git a/juniper/src/tests/schema_introspection.rs b/juniper/src/tests/schema_introspection.rs index 5926235b0..367e81a6c 100644 --- a/juniper/src/tests/schema_introspection.rs +++ b/juniper/src/tests/schema_introspection.rs @@ -36,6 +36,7 @@ pub(super) fn sort_schema_value(value: &mut Value) { pub(crate) fn schema_introspection_result() -> Value { let mut v = graphql_value!({ "__schema": { + "description": null, "queryType": { "name": "Query" }, @@ -46,6 +47,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "Human", "description": "A humanoid creature in the Star Wars universe.", + "specifiedByUrl": null, "fields": [ { "name": "id", @@ -151,6 +153,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "SCALAR", "name": "Boolean", "description": null, + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -161,6 +164,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "__InputValue", "description": null, + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -228,6 +232,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "SCALAR", "name": "String", "description": null, + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -238,6 +243,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "__Field", "description": null, + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -345,6 +351,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "ENUM", "name": "__TypeKind", "description": "GraphQL type kind\n\nThe GraphQL specification defines a number of type kinds - the meta type of a type.", + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -404,6 +411,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "__Type", "description": null, + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -548,6 +556,18 @@ pub(crate) fn schema_introspection_result() -> Value { "isDeprecated": false, "deprecationReason": null }, + { + "name": "specifiedByUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "enumValues", "description": null, @@ -589,6 +609,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "__Schema", "description": null, + "specifiedByUrl": null, "fields": [ { "name": "types", @@ -614,6 +635,18 @@ pub(crate) fn schema_introspection_result() -> Value { "isDeprecated": false, "deprecationReason": null }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "queryType", "description": null, @@ -688,6 +721,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "Droid", "description": "A mechanical creature in the Star Wars universe.", + "specifiedByUrl": null, "fields": [ { "name": "id", @@ -793,6 +827,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "Query", "description": "The root query object of the schema", + "specifiedByUrl": null, "fields": [ { "name": "human", @@ -882,6 +917,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "__EnumValue", "description": null, + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -949,6 +985,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "ENUM", "name": "Episode", "description": null, + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -978,6 +1015,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "ENUM", "name": "__DirectiveLocation", "description": null, + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -1031,6 +1069,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "INTERFACE", "name": "Character", "description": "A character in the Star Wars Trilogy", + "specifiedByUrl": null, "fields": [ { "name": "id", @@ -1129,6 +1168,7 @@ pub(crate) fn schema_introspection_result() -> Value { "kind": "OBJECT", "name": "__Directive", "description": null, + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -1158,6 +1198,22 @@ pub(crate) fn schema_introspection_result() -> Value { "isDeprecated": false, "deprecationReason": null }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "locations", "description": null, @@ -1265,6 +1321,7 @@ pub(crate) fn schema_introspection_result() -> Value { { "name": "skip", "description": null, + "isRepeatable": false, "locations": [ "FIELD", "FRAGMENT_SPREAD", @@ -1290,6 +1347,7 @@ pub(crate) fn schema_introspection_result() -> Value { { "name": "include", "description": null, + "isRepeatable": false, "locations": [ "FIELD", "FRAGMENT_SPREAD", @@ -1331,6 +1389,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "Human", + "specifiedByUrl": null, "fields": [ { "name": "id", @@ -1430,6 +1489,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "SCALAR", "name": "Boolean", + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -1439,6 +1499,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "__InputValue", + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -1501,6 +1562,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "SCALAR", "name": "String", + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -1510,6 +1572,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "__Field", + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -1610,6 +1673,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "ENUM", "name": "__TypeKind", + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -1660,6 +1724,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "__Type", + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -1795,6 +1860,17 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { "isDeprecated": false, "deprecationReason": null }, + { + "name": "specifiedByUrl", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "enumValues", "args": [ @@ -1833,7 +1909,19 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "__Schema", + "specifiedByUrl": null, "fields": [ + { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "types", "args": [], @@ -1926,6 +2014,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "Droid", + "specifiedByUrl": null, "fields": [ { "name": "id", @@ -2025,6 +2114,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "Query", + "specifiedByUrl": null, "fields": [ { "name": "human", @@ -2106,6 +2196,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "__EnumValue", + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -2168,6 +2259,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "ENUM", "name": "Episode", + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -2193,6 +2285,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "ENUM", "name": "__DirectiveLocation", + "specifiedByUrl": null, "fields": null, "inputFields": null, "interfaces": null, @@ -2238,6 +2331,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "INTERFACE", "name": "Character", + "specifiedByUrl": null, "fields": [ { "name": "id", @@ -2331,6 +2425,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { { "kind": "OBJECT", "name": "__Directive", + "specifiedByUrl": null, "fields": [ { "name": "name", @@ -2358,6 +2453,21 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { "isDeprecated": false, "deprecationReason": null }, + { + "name": "isRepeatable", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "locations", "args": [], @@ -2459,6 +2569,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { "directives": [ { "name": "skip", + "isRepeatable": false, "locations": [ "FIELD", "FRAGMENT_SPREAD", @@ -2482,6 +2593,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { }, { "name": "include", + "isRepeatable": false, "locations": [ "FIELD", "FRAGMENT_SPREAD", diff --git a/juniper/src/validation/test_harness.rs b/juniper/src/validation/test_harness.rs index 7bd75f7b0..45892b6a6 100644 --- a/juniper/src/validation/test_harness.rs +++ b/juniper/src/validation/test_harness.rs @@ -835,31 +835,37 @@ where "onQuery", &[DirectiveLocation::Query], &[], + false, )); root.schema.add_directive(DirectiveType::new( "onMutation", &[DirectiveLocation::Mutation], &[], + false, )); root.schema.add_directive(DirectiveType::new( "onField", &[DirectiveLocation::Field], &[], + false, )); root.schema.add_directive(DirectiveType::new( "onFragmentDefinition", &[DirectiveLocation::FragmentDefinition], &[], + false, )); root.schema.add_directive(DirectiveType::new( "onFragmentSpread", &[DirectiveLocation::FragmentSpread], &[], + false, )); root.schema.add_directive(DirectiveType::new( "onInlineFragment", &[DirectiveLocation::InlineFragment], &[], + false, )); let doc = diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 12a5136b6..992894ece 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -12,6 +12,7 @@ struct TransparentAttributes { transparent: Option, name: Option, description: Option, + specified_by_url: Option, scalar: Option, } @@ -21,6 +22,7 @@ impl syn::parse::Parse for TransparentAttributes { transparent: None, name: None, description: None, + specified_by_url: None, scalar: None, }; @@ -37,6 +39,11 @@ impl syn::parse::Parse for TransparentAttributes { let val = input.parse::()?; output.description = Some(val.value()); } + "specified_by_url" => { + input.parse::()?; + let val = input.parse::()?; + output.specified_by_url = Some(val.value()); + } "transparent" => { output.transparent = Some(true); } @@ -101,10 +108,10 @@ fn impl_scalar_struct( let inner_ty = &field.ty; let name = attrs.name.unwrap_or_else(|| ident.to_string()); - let description = match attrs.description { - Some(val) => quote!( .description( #val ) ), - None => quote!(), - }; + let description = attrs.description.map(|val| quote!( .description( #val ) )); + let specified_by_url = attrs + .specified_by_url + .map(|url| quote!( .specified_by_url( #url ) )); let scalar = attrs .scalar @@ -159,6 +166,7 @@ fn impl_scalar_struct( { registry.build_scalar_type::(info) #description + #specified_by_url .into_meta() } } diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index 8109f147f..449548ac7 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -202,10 +202,10 @@ pub fn build_scalar( .name .map(SpanContainer::into_inner) .unwrap_or_else(|| impl_for_type.ident.to_string()); - let description = match attrs.description { - Some(val) => quote!(.description(#val)), - None => quote!(), - }; + let description = attrs.description.map(|val| quote!( .description( #val ) )); + let specified_by_url = attrs + .specified_by_url + .map(|url| quote!( .specified_by_url( #url ) )); let async_generic_type = match input.custom_data_type_is_struct { true => quote!(__S), _ => quote!(#custom_data_type), @@ -273,6 +273,7 @@ pub fn build_scalar( { registry.build_scalar_type::(info) #description + #specified_by_url .into_meta() } } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index e0b0729c9..5b5803c1e 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -177,6 +177,8 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// // A description can also specified in the attribute. /// // This will the doc comment, if one exists. /// description = "...", +/// // A specification URL. +/// specified_by_url = "https://tools.ietf.org/html/rfc4122", /// )] /// struct UserId(String); /// ``` @@ -221,7 +223,10 @@ pub fn derive_scalar_value(input: TokenStream) -> TokenStream { /// name = "MyName", /// // You can also specify a description here. /// // If present, doc comments will be ignored. -/// description = "An opaque identifier, represented as a string")] +/// description = "An opaque identifier, represented as a string", +/// // A specification URL. +/// specified_by_url = "https://tools.ietf.org/html/rfc4122", +/// )] /// impl GraphQLScalar for UserID /// where /// S: juniper::ScalarValue diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index ae84fa65d..966e0d68b 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -454,6 +454,7 @@ pub enum FieldAttributeParseMode { enum FieldAttribute { Name(SpanContainer), Description(SpanContainer), + SpecifiedByUrl(SpanContainer), Deprecation(SpanContainer), Skip(SpanContainer), Arguments(HashMap), @@ -488,6 +489,15 @@ impl Parse for FieldAttribute { lit, ))) } + "specified_by_url" => { + input.parse::()?; + let lit = input.parse::()?; + Ok(FieldAttribute::SpecifiedByUrl(SpanContainer::new( + ident.span(), + Some(lit.span()), + lit, + ))) + } "deprecated" | "deprecation" => { let reason = if input.peek(token::Eq) { input.parse::()?; @@ -542,7 +552,9 @@ pub struct FieldAttributes { pub name: Option>, pub description: Option>, pub deprecation: Option>, - // Only relevant for GraphQLObject derive. + /// Only relevant for scalar impl macro. + pub specified_by_url: Option>, + /// Only relevant for GraphQLObject derive. pub skip: Option>, /// Only relevant for object macro. pub arguments: HashMap, @@ -564,6 +576,9 @@ impl Parse for FieldAttributes { FieldAttribute::Description(name) => { output.description = Some(name.map(|val| val.value())); } + FieldAttribute::SpecifiedByUrl(url) => { + output.specified_by_url = Some(url.map(|val| val.value())); + } FieldAttribute::Deprecation(attr) => { output.deprecation = Some(attr); } From 055d82f08e7d2201ff7188538b816a6e5fe68d2f Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 9 Dec 2021 15:18:28 +0300 Subject: [PATCH 2/8] CHANGELOG --- juniper/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 5c968de4b..0bab9a28b 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -16,6 +16,7 @@ - Use `null` in addition to `None` to create `Value::Null` in `graphql_value!` macro to mirror `serde_json::json!`. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Add `From` impls to `InputValue` mirroring the ones for `Value` and provide better support for `Option` handling. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996)) +- Add `specified_by_url` attribute to `GraphQLScalarValue` derive and `graphql_scalar` attribute macros. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) ## Fixes From 95e5e27cdc247fb284d5ea952b7f7a4e785fc461 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 9 Dec 2021 15:32:28 +0300 Subject: [PATCH 3/8] CHANGELOG --- juniper/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 0bab9a28b..78e0119b3 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -17,6 +17,7 @@ - Add `From` impls to `InputValue` mirroring the ones for `Value` and provide better support for `Option` handling. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Add `specified_by_url` attribute to `GraphQLScalarValue` derive and `graphql_scalar` attribute macros. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) +- Support `isRepeatable` field on directives. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) ## Fixes From 709d166a8214032081adfe9abadac94c0bfb5bb6 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 16 Dec 2021 13:45:19 +0300 Subject: [PATCH 4/8] Correction --- juniper/src/schema/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 81ffb317c..c14045af5 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -452,7 +452,7 @@ impl<'a, S> ScalarMeta<'a, S> { /// Set the `specification url` for the given [`ScalarMeta`] type /// /// Overwrites any previously set specification url. - pub fn specified_by_url(mut self, url: impl Into>) -> ScalarMeta<'a, S> { + pub fn specified_by_url(mut self, url: impl Into>) -> Self { self.specified_by_url = Some(url.into()); self } From c744688696d0cf69d66dd7825af589c5518d4d0f Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Dec 2021 15:44:55 +0100 Subject: [PATCH 5/8] Some corrections [skip ci] --- juniper/CHANGELOG.md | 3 ++- juniper/src/schema/meta.rs | 13 +++++++++---- juniper/src/schema/schema.rs | 2 +- juniper_codegen/src/derive_scalar_value.rs | 4 ++-- juniper_codegen/src/impl_scalar.rs | 4 ++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index b1bd97b2b..bc68a29b9 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -20,8 +20,9 @@ - Add `From` impls to `InputValue` mirroring the ones for `Value` and provide better support for `Option` handling. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Support [`time` crate](https://docs.rs/time) types as GraphQL scalars behind `time` feature. ([#1006](https://github.com/graphql-rust/juniper/pull/1006)) -- Add `specified_by_url` attribute to `GraphQLScalarValue` derive and `graphql_scalar` attribute macros. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) +- Add `specified_by_url` attribute argument to `#[derive(GraphQLScalarValue)]` and `#[graphql_scalar!]` macros. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) - Support `isRepeatable` field on directives. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) +- Support `__Schema.description`, `__Type.specifiedByURL` and `__Directive.isRepeatable` fields in introspection. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) ## Fixes diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index c14045af5..d4f350842 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -252,9 +252,11 @@ impl<'a, S> MetaType<'a, S> { } } - /// Access the specification url, if applicable + /// Accesses the [specification URL][0], if applicable. /// - /// Only custom scalars can have specification url. + /// Only custom GraphQL scalars can have a [specification URL][0]. + /// + /// [0]: https://spec.graphql.org/October2021#sec--specifiedBy pub fn specified_by_url(&self) -> Option<&str> { match self { Self::Scalar(ScalarMeta { @@ -267,6 +269,7 @@ impl<'a, S> MetaType<'a, S> { /// Construct a `TypeKind` for a given type /// /// # Panics + /// /// Panics if the type represents a placeholder or nullable type. pub fn type_kind(&self) -> TypeKind { match *self { @@ -449,9 +452,11 @@ impl<'a, S> ScalarMeta<'a, S> { self } - /// Set the `specification url` for the given [`ScalarMeta`] type + /// Sets the [specification URL][0] for this [`ScalarMeta`] type. + /// + /// Overwrites any previously set [specification URL][0]. /// - /// Overwrites any previously set specification url. + /// [0]: https://spec.graphql.org/October2021#sec--specifiedBy pub fn specified_by_url(mut self, url: impl Into>) -> Self { self.specified_by_url = Some(url.into()); self diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index 2416c03d2..30906478f 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -199,7 +199,7 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> { fn specified_by_url(&self) -> Option<&str> { match self { Self::Concrete(t) => t.specified_by_url(), - _ => None, + Self::NonNull(_) | Self::List(..) => None, } } diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 714e8a440..fb270697a 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -108,10 +108,10 @@ fn impl_scalar_struct( let inner_ty = &field.ty; let name = attrs.name.unwrap_or_else(|| ident.to_string()); - let description = attrs.description.map(|val| quote!( .description( #val ) )); + let description = attrs.description.map(|val| quote!(.description(#val))); let specified_by_url = attrs .specified_by_url - .map(|url| quote!( .specified_by_url( #url ) )); + .map(|url| quote!(.specified_by_url(#url))); let scalar = attrs .scalar diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index f23e4f222..9374b5c3b 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -202,10 +202,10 @@ pub fn build_scalar( .name .map(SpanContainer::into_inner) .unwrap_or_else(|| impl_for_type.ident.to_string()); - let description = attrs.description.map(|val| quote!( .description( #val ) )); + let description = attrs.description.map(|val| quote!(.description(#val))); let specified_by_url = attrs .specified_by_url - .map(|url| quote!( .specified_by_url( #url ) )); + .map(|url| quote!(.specified_by_url(#url))); let async_generic_type = match input.custom_data_type_is_struct { true => quote!(__S), _ => quote!(#custom_data_type), From 594f050dde1fae9c5c14521d53f4b32fdab99eb5 Mon Sep 17 00:00:00 2001 From: ilslv Date: Mon, 20 Dec 2021 09:07:24 +0300 Subject: [PATCH 6/8] Assert for correct URL in codegen --- .../fail/scalar/derive_wrong_url.rs | 5 ++ .../fail/scalar/derive_wrong_url.stderr | 5 ++ .../fail/scalar/impl_wrong_url.rs | 20 +++++++ .../fail/scalar/impl_wrong_url.stderr | 5 ++ .../juniper_tests/src/codegen/impl_scalar.rs | 6 +- juniper/src/schema/model.rs | 18 ++++++ juniper/src/tests/introspection_tests.rs | 6 ++ juniper/src/tests/schema_introspection.rs | 57 +++++++++++++++++++ juniper_codegen/Cargo.toml | 1 + juniper_codegen/src/derive_scalar_value.rs | 16 ++++-- juniper_codegen/src/impl_scalar.rs | 7 ++- juniper_codegen/src/util/mod.rs | 14 ++++- juniper_codegen/src/util/span_container.rs | 9 +++ 13 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr diff --git a/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs b/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs new file mode 100644 index 000000000..0dd7e84cf --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs @@ -0,0 +1,5 @@ +#[derive(juniper::GraphQLScalarValue)] +#[graphql(specified_by_url = "not an url")] +struct ScalarSpecifiedByUrl(i64); + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr b/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr new file mode 100644 index 000000000..9d3df2c17 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr @@ -0,0 +1,5 @@ +error: Failed to parse URL: relative URL without a base + --> fail/scalar/derive_wrong_url.rs:2:30 + | +2 | #[graphql(specified_by_url = "not an url")] + | ^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs b/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs new file mode 100644 index 000000000..8c27eaae1 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs @@ -0,0 +1,20 @@ +struct ScalarSpecifiedByUrl(i32); + +#[juniper::graphql_scalar(specified_by_url = "not an url")] +impl GraphQLScalar for ScalarSpecifiedByUrl { + fn resolve(&self) -> Value { + Value::scalar(self.0) + } + + fn from_input_value(v: &InputValue) -> Result { + v.as_int_value() + .map(ScalarSpecifiedByUrl) + .ok_or_else(|| format!("Expected `Int`, found: {}", v)) + } + + fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> { + ::from_str(value) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr b/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr new file mode 100644 index 000000000..bf125aa76 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr @@ -0,0 +1,5 @@ +error: Failed to parse URL: relative URL without a base + --> fail/scalar/impl_wrong_url.rs:3:27 + | +3 | #[juniper::graphql_scalar(specified_by_url = "not an url")] + | ^^^^^^^^^^^^^^^^ diff --git a/integration_tests/juniper_tests/src/codegen/impl_scalar.rs b/integration_tests/juniper_tests/src/codegen/impl_scalar.rs index 2b8050a61..4fea7ef66 100644 --- a/integration_tests/juniper_tests/src/codegen/impl_scalar.rs +++ b/integration_tests/juniper_tests/src/codegen/impl_scalar.rs @@ -100,8 +100,10 @@ impl GraphQLScalar for ScalarSpecifiedByUrl { Value::scalar(self.0) } - fn from_input_value(v: &InputValue) -> Option { - v.as_scalar_value::().map(|i| ScalarSpecifiedByUrl(*i)) + fn from_input_value(v: &InputValue) -> Result { + v.as_int_value() + .map(ScalarSpecifiedByUrl) + .ok_or_else(|| format!("Expected `Int`, found: {}", v)) } fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> { diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index d256d30be..cc48f592e 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -82,6 +82,7 @@ pub enum DirectiveLocation { Mutation, Subscription, Field, + Scalar, #[graphql(name = "FRAGMENT_DEFINITION")] FragmentDefinition, #[graphql(name = "FRAGMENT_SPREAD")] @@ -213,6 +214,10 @@ impl<'a, S> SchemaType<'a, S> { "include".to_owned(), DirectiveType::new_include(&mut registry), ); + directives.insert( + "specifiedBy".to_owned(), + DirectiveType::new_specified_by(&mut registry), + ); let mut meta_fields = vec![ registry.field::>("__schema", &()), @@ -540,6 +545,18 @@ where ) } + fn new_specified_by(registry: &mut Registry<'a, S>) -> DirectiveType<'a, S> + where + S: ScalarValue, + { + Self::new( + "specifiedBy", + &[DirectiveLocation::Scalar], + &[registry.arg::("url", &())], + false, + ) + } + pub fn description(mut self, description: &str) -> DirectiveType<'a, S> { self.description = Some(description.to_owned()); self @@ -556,6 +573,7 @@ impl fmt::Display for DirectiveLocation { DirectiveLocation::FragmentDefinition => "fragment definition", DirectiveLocation::FragmentSpread => "fragment spread", DirectiveLocation::InlineFragment => "inline fragment", + DirectiveLocation::Scalar => "scalar", }) } } diff --git a/juniper/src/tests/introspection_tests.rs b/juniper/src/tests/introspection_tests.rs index 0ab33da50..16416b2f5 100644 --- a/juniper/src/tests/introspection_tests.rs +++ b/juniper/src/tests/introspection_tests.rs @@ -208,6 +208,12 @@ async fn test_introspection_directives() { "INLINE_FRAGMENT", ], }, + { + "name": "specifiedBy", + "locations": [ + "SCALAR", + ], + }, ], }, }); diff --git a/juniper/src/tests/schema_introspection.rs b/juniper/src/tests/schema_introspection.rs index 367e81a6c..fbea3b987 100644 --- a/juniper/src/tests/schema_introspection.rs +++ b/juniper/src/tests/schema_introspection.rs @@ -1061,6 +1061,12 @@ pub(crate) fn schema_introspection_result() -> Value { "description": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "SCALAR", + "description": null, + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -1369,6 +1375,30 @@ pub(crate) fn schema_introspection_result() -> Value { "defaultValue": null } ] + }, + { + "name": "specifiedBy", + "description": null, + "isRepeatable": false, + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ] } ] } @@ -2324,6 +2354,11 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { "name": "INLINE_FRAGMENT", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "SCALAR", + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -2614,6 +2649,28 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { "defaultValue": null } ] + }, + { + "name": "specifiedBy", + "isRepeatable": false, + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ] } ] } diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index 28d44c131..b34ba2ad9 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -22,6 +22,7 @@ proc-macro-error = "1.0.2" proc-macro2 = "1.0.1" quote = "1.0.3" syn = { version = "1.0.60", features = ["extra-traits", "full", "parsing"], default-features = false } +url = "2.2.2" [dev-dependencies] derive_more = "0.99.7" diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index fb270697a..feab928f1 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -6,13 +6,14 @@ use crate::{ use proc_macro2::TokenStream; use quote::quote; use syn::{spanned::Spanned, token, Data, Fields, Ident, Variant}; +use url::Url; #[derive(Debug, Default)] struct TransparentAttributes { transparent: Option, name: Option, description: Option, - specified_by_url: Option, + specified_by_url: Option, scalar: Option, } @@ -41,8 +42,10 @@ impl syn::parse::Parse for TransparentAttributes { } "specified_by_url" => { input.parse::()?; - let val = input.parse::()?; - output.specified_by_url = Some(val.value()); + let val: syn::LitStr = input.parse::()?; + output.specified_by_url = Some(val.value().parse().map_err(|e| { + syn::Error::new(val.span(), format!("Failed to parse URL: {}", e)) + })?); } "transparent" => { output.transparent = Some(true); @@ -109,9 +112,10 @@ fn impl_scalar_struct( let name = attrs.name.unwrap_or_else(|| ident.to_string()); let description = attrs.description.map(|val| quote!(.description(#val))); - let specified_by_url = attrs - .specified_by_url - .map(|url| quote!(.specified_by_url(#url))); + let specified_by_url = attrs.specified_by_url.map(|url| { + let url = url.as_str(); + quote!(.specified_by_url(#url)) + }); let scalar = attrs .scalar diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index 9374b5c3b..c33866155 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -203,9 +203,10 @@ pub fn build_scalar( .map(SpanContainer::into_inner) .unwrap_or_else(|| impl_for_type.ident.to_string()); let description = attrs.description.map(|val| quote!(.description(#val))); - let specified_by_url = attrs - .specified_by_url - .map(|url| quote!(.specified_by_url(#url))); + let specified_by_url = attrs.specified_by_url.map(|url| { + let url = url.as_str(); + quote!(.specified_by_url(#url)) + }); let async_generic_type = match input.custom_data_type_is_struct { true => quote!(__S), _ => quote!(#custom_data_type), diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 4a043827f..823a2ba1c 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -17,6 +17,7 @@ use syn::{ spanned::Spanned, token, Attribute, Ident, Lit, Meta, MetaList, MetaNameValue, NestedMeta, }; +use url::Url; use crate::common::parse::ParseBufferExt as _; @@ -553,7 +554,7 @@ pub struct FieldAttributes { pub description: Option>, pub deprecation: Option>, /// Only relevant for scalar impl macro. - pub specified_by_url: Option>, + pub specified_by_url: Option>, /// Only relevant for GraphQLObject derive. pub skip: Option>, /// Only relevant for object macro. @@ -577,7 +578,16 @@ impl Parse for FieldAttributes { output.description = Some(name.map(|val| val.value())); } FieldAttribute::SpecifiedByUrl(url) => { - output.specified_by_url = Some(url.map(|val| val.value())); + output.specified_by_url = Some( + url.map(|val| Url::parse(&val.value())) + .transpose() + .map_err(|e| { + syn::Error::new( + e.span_ident(), + format!("Failed to parse URL: {}", e.inner()), + ) + })?, + ); } FieldAttribute::Deprecation(attr) => { output.deprecation = Some(attr); diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/util/span_container.rs index 370f17a74..2040a48ff 100644 --- a/juniper_codegen/src/util/span_container.rs +++ b/juniper_codegen/src/util/span_container.rs @@ -58,6 +58,15 @@ impl SpanContainer { } } +impl SpanContainer> { + pub fn transpose(self) -> Result, SpanContainer> { + match self.val { + Ok(v) => Ok(SpanContainer::new(self.ident, self.expr, v)), + Err(e) => Err(SpanContainer::new(self.ident, self.expr, e)), + } + } +} + impl AsRef for SpanContainer { fn as_ref(&self) -> &T { &self.val From be90d460f26cd1ffe81010c28a4367b2fe8680bb Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 20 Dec 2021 12:30:23 +0100 Subject: [PATCH 7/8] Corrections --- .../{derive_wrong_url.rs => derive_invalid_url.rs} | 4 +++- .../fail/scalar/derive_invalid_url.stderr | 5 +++++ .../codegen_fail/fail/scalar/derive_wrong_url.stderr | 5 ----- .../scalar/{impl_wrong_url.rs => impl_invalid_url.rs} | 4 +++- .../codegen_fail/fail/scalar/impl_invalid_url.stderr | 5 +++++ .../codegen_fail/fail/scalar/impl_wrong_url.stderr | 5 ----- .../juniper_tests/src/codegen/derive_scalar.rs | 8 ++++---- .../juniper_tests/src/codegen/impl_scalar.rs | 6 ++---- .../src/codegen/scalar_value_transparent.rs | 2 +- juniper_codegen/Cargo.toml | 2 +- juniper_codegen/src/derive_scalar_value.rs | 11 ++++++----- juniper_codegen/src/impl_scalar.rs | 4 ++-- juniper_codegen/src/util/mod.rs | 2 +- 13 files changed, 33 insertions(+), 30 deletions(-) rename integration_tests/codegen_fail/fail/scalar/{derive_wrong_url.rs => derive_invalid_url.rs} (58%) create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_invalid_url.stderr delete mode 100644 integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr rename integration_tests/codegen_fail/fail/scalar/{impl_wrong_url.rs => impl_invalid_url.rs} (86%) create mode 100644 integration_tests/codegen_fail/fail/scalar/impl_invalid_url.stderr delete mode 100644 integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr diff --git a/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs b/integration_tests/codegen_fail/fail/scalar/derive_invalid_url.rs similarity index 58% rename from integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs rename to integration_tests/codegen_fail/fail/scalar/derive_invalid_url.rs index 0dd7e84cf..50549f11b 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.rs +++ b/integration_tests/codegen_fail/fail/scalar/derive_invalid_url.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLScalarValue)] +use juniper::GraphQLScalarValue; + +#[derive(GraphQLScalarValue)] #[graphql(specified_by_url = "not an url")] struct ScalarSpecifiedByUrl(i64); diff --git a/integration_tests/codegen_fail/fail/scalar/derive_invalid_url.stderr b/integration_tests/codegen_fail/fail/scalar/derive_invalid_url.stderr new file mode 100644 index 000000000..9a0d5afde --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_invalid_url.stderr @@ -0,0 +1,5 @@ +error: Invalid URL: relative URL without a base + --> fail/scalar/derive_invalid_url.rs:4:30 + | +4 | #[graphql(specified_by_url = "not an url")] + | ^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr b/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr deleted file mode 100644 index 9d3df2c17..000000000 --- a/integration_tests/codegen_fail/fail/scalar/derive_wrong_url.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Failed to parse URL: relative URL without a base - --> fail/scalar/derive_wrong_url.rs:2:30 - | -2 | #[graphql(specified_by_url = "not an url")] - | ^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs b/integration_tests/codegen_fail/fail/scalar/impl_invalid_url.rs similarity index 86% rename from integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs rename to integration_tests/codegen_fail/fail/scalar/impl_invalid_url.rs index 8c27eaae1..a71da71b1 100644 --- a/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.rs +++ b/integration_tests/codegen_fail/fail/scalar/impl_invalid_url.rs @@ -1,6 +1,8 @@ +use juniper::graphql_scalar; + struct ScalarSpecifiedByUrl(i32); -#[juniper::graphql_scalar(specified_by_url = "not an url")] +#[graphql_scalar(specified_by_url = "not an url")] impl GraphQLScalar for ScalarSpecifiedByUrl { fn resolve(&self) -> Value { Value::scalar(self.0) diff --git a/integration_tests/codegen_fail/fail/scalar/impl_invalid_url.stderr b/integration_tests/codegen_fail/fail/scalar/impl_invalid_url.stderr new file mode 100644 index 000000000..bb2aeaeeb --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/impl_invalid_url.stderr @@ -0,0 +1,5 @@ +error: Invalid URL: relative URL without a base + --> fail/scalar/impl_invalid_url.rs:5:22 + | +5 | #[graphql_scalar(specified_by_url = "not an url")] + | ^^^^^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr b/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr deleted file mode 100644 index bf125aa76..000000000 --- a/integration_tests/codegen_fail/fail/scalar/impl_wrong_url.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Failed to parse URL: relative URL without a base - --> fail/scalar/impl_wrong_url.rs:3:27 - | -3 | #[juniper::graphql_scalar(specified_by_url = "not an url")] - | ^^^^^^^^^^^^^^^^ diff --git a/integration_tests/juniper_tests/src/codegen/derive_scalar.rs b/integration_tests/juniper_tests/src/codegen/derive_scalar.rs index 49e272383..24e329bcf 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_scalar.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_scalar.rs @@ -62,16 +62,16 @@ async fn test_scalar_value_large_specified_url() { ); let doc = r#"{ - __type(name: "LargeId") { - specifiedByUrl - } + __type(name: "LargeId") { + specifiedByUrl + } }"#; assert_eq!( execute(doc, None, &schema, &Variables::::new(), &()).await, Ok(( graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc4122"}}), - vec![] + vec![], )), ); } diff --git a/integration_tests/juniper_tests/src/codegen/impl_scalar.rs b/integration_tests/juniper_tests/src/codegen/impl_scalar.rs index 4fea7ef66..b852416ec 100644 --- a/integration_tests/juniper_tests/src/codegen/impl_scalar.rs +++ b/integration_tests/juniper_tests/src/codegen/impl_scalar.rs @@ -344,14 +344,12 @@ async fn scalar_description_introspection() { #[tokio::test] async fn scalar_specified_by_url_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "ScalarSpecifiedByUrl") { name specifiedByUrl } - } - "#; + }"#; run_type_info_query(doc, |type_info| { assert_eq!( diff --git a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs index 220fe8a33..828f12d32 100644 --- a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs +++ b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs @@ -82,6 +82,6 @@ fn test_scalar_value_doc_comment() { assert_eq!(meta.description(), Some("The doc comment...")); assert_eq!( meta.specified_by_url(), - Some("https://tools.ietf.org/html/rfc4122") + Some("https://tools.ietf.org/html/rfc4122"), ); } diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index b34ba2ad9..acd1a33ee 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -22,7 +22,7 @@ proc-macro-error = "1.0.2" proc-macro2 = "1.0.1" quote = "1.0.3" syn = { version = "1.0.60", features = ["extra-traits", "full", "parsing"], default-features = false } -url = "2.2.2" +url = "2.0" [dev-dependencies] derive_more = "0.99.7" diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index feab928f1..3e7582247 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -43,9 +43,10 @@ impl syn::parse::Parse for TransparentAttributes { "specified_by_url" => { input.parse::()?; let val: syn::LitStr = input.parse::()?; - output.specified_by_url = Some(val.value().parse().map_err(|e| { - syn::Error::new(val.span(), format!("Failed to parse URL: {}", e)) - })?); + output.specified_by_url = + Some(val.value().parse().map_err(|e| { + syn::Error::new(val.span(), format!("Invalid URL: {}", e)) + })?); } "transparent" => { output.transparent = Some(true); @@ -113,8 +114,8 @@ fn impl_scalar_struct( let description = attrs.description.map(|val| quote!(.description(#val))); let specified_by_url = attrs.specified_by_url.map(|url| { - let url = url.as_str(); - quote!(.specified_by_url(#url)) + let url_lit = url.as_str(); + quote!(.specified_by_url(#url_lit)) }); let scalar = attrs diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index c33866155..a790abac5 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -204,8 +204,8 @@ pub fn build_scalar( .unwrap_or_else(|| impl_for_type.ident.to_string()); let description = attrs.description.map(|val| quote!(.description(#val))); let specified_by_url = attrs.specified_by_url.map(|url| { - let url = url.as_str(); - quote!(.specified_by_url(#url)) + let url_lit = url.as_str(); + quote!(.specified_by_url(#url_lit)) }); let async_generic_type = match input.custom_data_type_is_struct { true => quote!(__S), diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 823a2ba1c..4109ce7e3 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -584,7 +584,7 @@ impl Parse for FieldAttributes { .map_err(|e| { syn::Error::new( e.span_ident(), - format!("Failed to parse URL: {}", e.inner()), + format!("Invalid URL: {}", e.inner()), ) })?, ); From b57c06e1ddbcb1189a478f3fe0d989f75103903a Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 20 Dec 2021 12:33:51 +0100 Subject: [PATCH 8/8] Fix typo --- juniper/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index bc68a29b9..3ab147d65 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -20,7 +20,7 @@ - Add `From` impls to `InputValue` mirroring the ones for `Value` and provide better support for `Option` handling. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Support [`time` crate](https://docs.rs/time) types as GraphQL scalars behind `time` feature. ([#1006](https://github.com/graphql-rust/juniper/pull/1006)) -- Add `specified_by_url` attribute argument to `#[derive(GraphQLScalarValue)]` and `#[graphql_scalar!]` macros. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) +- Add `specified_by_url` attribute argument to `#[derive(GraphQLScalarValue)]` and `#[graphql_scalar]` macros. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) - Support `isRepeatable` field on directives. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) - Support `__Schema.description`, `__Type.specifiedByURL` and `__Directive.isRepeatable` fields in introspection. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000))