Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GraphQL October 2021] Support @specifiedBy(url: "...") directive (#1000) #1003

Merged
merged 10 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion integration_tests/juniper_tests/src/codegen/derive_scalar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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::<MyScalarValue>::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(
Expand Down
48 changes: 48 additions & 0 deletions integration_tests/juniper_tests/src/codegen/impl_scalar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct DefaultName(i32);
struct OtherOrder(i32);
struct Named(i32);
struct ScalarDescription(i32);
struct ScalarSpecifiedByUrl(i32);
struct Generated(String);

struct Root;
Expand Down Expand Up @@ -93,6 +94,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<ScalarSpecifiedByUrl> {
v.as_scalar_value::<i32>().map(|i| ScalarSpecifiedByUrl(*i))
}

fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}

macro_rules! impl_scalar {
($name: ident) => {
#[graphql_scalar]
Expand Down Expand Up @@ -134,6 +150,9 @@ impl Root {
fn scalar_description() -> ScalarDescription {
ScalarDescription(0)
}
fn scalar_specified_by_url() -> ScalarSpecifiedByUrl {
ScalarSpecifiedByUrl(0)
}
fn generated() -> Generated {
Generated("foo".to_owned())
}
Expand Down Expand Up @@ -297,6 +316,7 @@ async fn scalar_description_introspection() {
__type(name: "ScalarDescription") {
name
description
specifiedByUrl
}
}
"#;
Expand All @@ -312,6 +332,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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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();
Expand All @@ -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")
);
}
2 changes: 2 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
- 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))
- Support `isRepeatable` field on directives. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000))

## Fixes

Expand Down
2 changes: 2 additions & 0 deletions juniper/src/executor_tests/introspection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ async fn scalar_introspection() {
name
kind
description
specifiedByUrl
fields { name }
interfaces { name }
possibleTypes { name }
Expand Down Expand Up @@ -527,6 +528,7 @@ async fn scalar_introspection() {
"name": "SampleScalar",
"kind": "SCALAR",
"description": null,
"specifiedByUrl": null,
"fields": null,
"interfaces": null,
"possibleTypes": null,
Expand Down
4 changes: 2 additions & 2 deletions juniper/src/introspection/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/// From <https://github.com/graphql/graphql-js/blob/8c96dc8276f2de27b8af9ffbd71a4597d483523f/src/utilities/introspectionQuery.js#L21>
/// From <https://github.com/graphql/graphql-js/blob/90bd6ff72625173dd39a1f82cfad9336cfad8f65/src/utilities/getIntrospectionQuery.ts#L62>
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
/// (<https://github.com/graphql/graphql-js/blob/8c96dc8276f2de27b8af9ffbd71a4597d483523f/src/utilities/introspectionQuery.js#L21>)
/// (<https://github.com/graphql/graphql-js/blob/90bd6ff72625173dd39a1f82cfad9336cfad8f65/src/utilities/getIntrospectionQuery.ts#L62>)
pub enum IntrospectionFormat {
/// The canonical GraphQL introspection query.
All,
Expand Down
3 changes: 3 additions & 0 deletions juniper/src/introspection/query.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
query IntrospectionQuery {
__schema {
description
queryType {
name
}
Expand All @@ -15,6 +16,7 @@ query IntrospectionQuery {
directives {
name
description
isRepeatable
locations
args {
...InputValue
Expand All @@ -26,6 +28,7 @@ fragment FullType on __Type {
kind
name
description
specifiedByUrl
fields(includeDeprecated: true) {
name
description
Expand Down
2 changes: 2 additions & 0 deletions juniper/src/introspection/query_without_descriptions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ query IntrospectionQuery {
}
directives {
name
isRepeatable
locations
args {
...InputValue
Expand All @@ -24,6 +25,7 @@ query IntrospectionQuery {
fragment FullType on __Type {
kind
name
specifiedByUrl
fields(includeDeprecated: true) {
name
args {
Expand Down
23 changes: 23 additions & 0 deletions juniper/src/schema/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub struct ScalarMeta<'a, S> {
pub name: Cow<'a, str>,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub specified_by_url: Option<Cow<'a, str>>,
pub(crate) try_parse_fn: for<'b> fn(&'b InputValue<S>) -> Result<(), FieldError<S>>,
pub(crate) parse_fn: for<'b> fn(ScalarToken<'b>) -> Result<S, ParseError<'b>>,
}
Expand Down Expand Up @@ -250,6 +252,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
Expand Down Expand Up @@ -421,6 +435,7 @@ impl<'a, S> ScalarMeta<'a, S> {
Self {
name,
description: None,
specified_by_url: None,
try_parse_fn: try_parse_fn::<S, T>,
parse_fn: <T as ParseScalarValue<S>>::from_str,
}
Expand All @@ -434,6 +449,14 @@ impl<'a, S> ScalarMeta<'a, S> {
self
}

/// 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<Cow<'a, str>>) -> Self {
self.specified_by_url = Some(url.into());
self
}

/// Wraps this [`ScalarMeta`] type into a generic [`MetaType`].
pub fn into_meta(self) -> MetaType<'a, S> {
MetaType::Scalar(self)
Expand Down
14 changes: 13 additions & 1 deletion juniper/src/schema/model.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fmt;
use std::{borrow::Cow, fmt};

use fnv::FnvHashMap;
#[cfg(feature = "graphql-parser-integration")]
Expand Down Expand Up @@ -49,6 +49,7 @@ pub struct RootNode<
/// Metadata for a schema
#[derive(Debug)]
pub struct SchemaType<'a, S> {
pub(crate) description: Option<Cow<'a, str>>,
pub(crate) types: FnvHashMap<Name, MetaType<'a, S>>,
pub(crate) query_type_name: String,
pub(crate) mutation_type_name: Option<String>,
Expand All @@ -71,6 +72,7 @@ pub struct DirectiveType<'a, S> {
pub description: Option<String>,
pub locations: Vec<DirectiveLocation>,
pub arguments: Vec<Argument<'a, S>>,
pub is_repeatable: bool,
}

#[derive(Clone, PartialEq, Eq, Debug, GraphQLEnum)]
Expand Down Expand Up @@ -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" {
Expand All @@ -251,6 +254,11 @@ impl<'a, S> SchemaType<'a, S> {
}
}

/// Add a description.
pub fn set_description(&mut self, description: impl Into<Cow<'a, str>>) {
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);
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -510,6 +520,7 @@ where
DirectiveLocation::InlineFragment,
],
&[registry.arg::<bool>("if", &())],
false,
)
}

Expand All @@ -525,6 +536,7 @@ where
DirectiveLocation::InlineFragment,
],
&[registry.arg::<bool>("if", &())],
false,
)
}

Expand Down
15 changes: 15 additions & 0 deletions juniper/src/schema/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeType<S>> {
self.type_list()
.into_iter()
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<S>] {
&self.arguments
}
Expand Down
Loading