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 9 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use juniper::GraphQLScalarValue;

#[derive(GraphQLScalarValue)]
#[graphql(specified_by_url = "not an url")]
struct ScalarSpecifiedByUrl(i64);

fn main() {}
Original file line number Diff line number Diff line change
@@ -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")]
| ^^^^^^^^^^^^
22 changes: 22 additions & 0 deletions integration_tests/codegen_fail/fail/scalar/impl_invalid_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use juniper::graphql_scalar;

struct ScalarSpecifiedByUrl(i32);

#[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<ScalarSpecifiedByUrl, String> {
v.as_int_value()
.map(ScalarSpecifiedByUrl)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}

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

fn main() {}
Original file line number Diff line number Diff line change
@@ -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")]
| ^^^^^^^^^^^^^^^^
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,23 @@ 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) -> Result<ScalarSpecifiedByUrl, String> {
v.as_int_value()
.map(ScalarSpecifiedByUrl)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}

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 +152,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 +318,7 @@ async fn scalar_description_introspection() {
__type(name: "ScalarDescription") {
name
description
specifiedByUrl
}
}
"#;
Expand All @@ -312,6 +334,32 @@ 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"),
);
}
3 changes: 3 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +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 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

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
28 changes: 28 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,9 +252,24 @@ impl<'a, S> MetaType<'a, S> {
}
}

/// Accesses the [specification URL][0], if applicable.
///
/// 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 {
specified_by_url, ..
}) => specified_by_url.as_deref(),
_ => None,
}
}

/// 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 {
Expand Down Expand Up @@ -421,6 +438,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 +452,16 @@ impl<'a, S> ScalarMeta<'a, S> {
self
}

/// Sets the [specification URL][0] for this [`ScalarMeta`] type.
///
/// Overwrites any previously set [specification URL][0].
///
/// [0]: https://spec.graphql.org/October2021#sec--specifiedBy
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
Loading