Skip to content

Commit 847d09f

Browse files
authored
Support @specifiedBy(url: "...") directive via specified_by_url attribute argument in #[graphql_scalar] and #[derive(GraphQLScalarValue)] macros (#1003, #1000)
- support `isRepeatable` field on directives - support `__Schema.description`, `__Type.specifiedByURL` and `__Directive.isRepeatable` fields in introspection
1 parent 3e4d4ea commit 847d09f

File tree

24 files changed

+453
-15
lines changed

24 files changed

+453
-15
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use juniper::GraphQLScalarValue;
2+
3+
#[derive(GraphQLScalarValue)]
4+
#[graphql(specified_by_url = "not an url")]
5+
struct ScalarSpecifiedByUrl(i64);
6+
7+
fn main() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: Invalid URL: relative URL without a base
2+
--> fail/scalar/derive_invalid_url.rs:4:30
3+
|
4+
4 | #[graphql(specified_by_url = "not an url")]
5+
| ^^^^^^^^^^^^
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use juniper::graphql_scalar;
2+
3+
struct ScalarSpecifiedByUrl(i32);
4+
5+
#[graphql_scalar(specified_by_url = "not an url")]
6+
impl GraphQLScalar for ScalarSpecifiedByUrl {
7+
fn resolve(&self) -> Value {
8+
Value::scalar(self.0)
9+
}
10+
11+
fn from_input_value(v: &InputValue) -> Result<ScalarSpecifiedByUrl, String> {
12+
v.as_int_value()
13+
.map(ScalarSpecifiedByUrl)
14+
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
15+
}
16+
17+
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
18+
<i32 as ParseScalarValue>::from_str(value)
19+
}
20+
}
21+
22+
fn main() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: Invalid URL: relative URL without a base
2+
--> fail/scalar/impl_invalid_url.rs:5:22
3+
|
4+
5 | #[graphql_scalar(specified_by_url = "not an url")]
5+
| ^^^^^^^^^^^^^^^^

integration_tests/juniper_tests/src/codegen/derive_scalar.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ use juniper::{
66
use crate::custom_scalar::MyScalarValue;
77

88
#[derive(Debug, PartialEq, Eq, Hash, juniper::GraphQLScalarValue)]
9-
#[graphql(transparent, scalar = MyScalarValue)]
9+
#[graphql(
10+
transparent,
11+
scalar = MyScalarValue,
12+
specified_by_url = "https://tools.ietf.org/html/rfc4122",
13+
)]
1014
pub struct LargeId(i64);
1115

1216
#[derive(juniper::GraphQLObject)]
@@ -49,6 +53,29 @@ fn test_scalar_value_large_id() {
4953
assert_eq!(output, InputValue::scalar(num));
5054
}
5155

56+
#[tokio::test]
57+
async fn test_scalar_value_large_specified_url() {
58+
let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value(
59+
Query,
60+
EmptyMutation::<()>::new(),
61+
EmptySubscription::<()>::new(),
62+
);
63+
64+
let doc = r#"{
65+
__type(name: "LargeId") {
66+
specifiedByUrl
67+
}
68+
}"#;
69+
70+
assert_eq!(
71+
execute(doc, None, &schema, &Variables::<MyScalarValue>::new(), &()).await,
72+
Ok((
73+
graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc4122"}}),
74+
vec![],
75+
)),
76+
);
77+
}
78+
5279
#[tokio::test]
5380
async fn test_scalar_value_large_query() {
5481
let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value(

integration_tests/juniper_tests/src/codegen/impl_scalar.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct DefaultName(i32);
99
struct OtherOrder(i32);
1010
struct Named(i32);
1111
struct ScalarDescription(i32);
12+
struct ScalarSpecifiedByUrl(i32);
1213
struct Generated(String);
1314

1415
struct Root;
@@ -93,6 +94,23 @@ impl GraphQLScalar for ScalarDescription {
9394
}
9495
}
9596

97+
#[graphql_scalar(specified_by_url = "https://tools.ietf.org/html/rfc4122")]
98+
impl GraphQLScalar for ScalarSpecifiedByUrl {
99+
fn resolve(&self) -> Value {
100+
Value::scalar(self.0)
101+
}
102+
103+
fn from_input_value(v: &InputValue) -> Result<ScalarSpecifiedByUrl, String> {
104+
v.as_int_value()
105+
.map(ScalarSpecifiedByUrl)
106+
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
107+
}
108+
109+
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
110+
<i32 as ParseScalarValue>::from_str(value)
111+
}
112+
}
113+
96114
macro_rules! impl_scalar {
97115
($name: ident) => {
98116
#[graphql_scalar]
@@ -134,6 +152,9 @@ impl Root {
134152
fn scalar_description() -> ScalarDescription {
135153
ScalarDescription(0)
136154
}
155+
fn scalar_specified_by_url() -> ScalarSpecifiedByUrl {
156+
ScalarSpecifiedByUrl(0)
157+
}
137158
fn generated() -> Generated {
138159
Generated("foo".to_owned())
139160
}
@@ -297,6 +318,7 @@ async fn scalar_description_introspection() {
297318
__type(name: "ScalarDescription") {
298319
name
299320
description
321+
specifiedByUrl
300322
}
301323
}
302324
"#;
@@ -312,6 +334,32 @@ async fn scalar_description_introspection() {
312334
"A sample scalar, represented as an integer",
313335
)),
314336
);
337+
assert_eq!(
338+
type_info.get_field_value("specifiedByUrl"),
339+
Some(&graphql_value!(null)),
340+
);
341+
})
342+
.await;
343+
}
344+
345+
#[tokio::test]
346+
async fn scalar_specified_by_url_introspection() {
347+
let doc = r#"{
348+
__type(name: "ScalarSpecifiedByUrl") {
349+
name
350+
specifiedByUrl
351+
}
352+
}"#;
353+
354+
run_type_info_query(doc, |type_info| {
355+
assert_eq!(
356+
type_info.get_field_value("name"),
357+
Some(&graphql_value!("ScalarSpecifiedByUrl")),
358+
);
359+
assert_eq!(
360+
type_info.get_field_value("specifiedByUrl"),
361+
Some(&graphql_value!("https://tools.ietf.org/html/rfc4122")),
362+
);
315363
})
316364
.await;
317365
}

integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct CustomUserId(String);
1414

1515
/// The doc comment...
1616
#[derive(GraphQLScalarValue, Debug, Eq, PartialEq)]
17-
#[graphql(transparent)]
17+
#[graphql(transparent, specified_by_url = "https://tools.ietf.org/html/rfc4122")]
1818
struct IdWithDocComment(i32);
1919

2020
#[derive(GraphQLObject)]
@@ -64,6 +64,7 @@ fn test_scalar_value_custom() {
6464
let meta = CustomUserId::meta(&(), &mut registry);
6565
assert_eq!(meta.name(), Some("MyUserId"));
6666
assert_eq!(meta.description(), Some("custom description..."));
67+
assert_eq!(meta.specified_by_url(), None);
6768

6869
let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
6970
let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap();
@@ -79,4 +80,8 @@ fn test_scalar_value_doc_comment() {
7980
let mut registry: Registry = Registry::new(FnvHashMap::default());
8081
let meta = IdWithDocComment::meta(&(), &mut registry);
8182
assert_eq!(meta.description(), Some("The doc comment..."));
83+
assert_eq!(
84+
meta.specified_by_url(),
85+
Some("https://tools.ietf.org/html/rfc4122"),
86+
);
8287
}

juniper/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
- 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))
2121
- Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996))
2222
- Support [`time` crate](https://docs.rs/time) types as GraphQL scalars behind `time` feature. ([#1006](https://github.com/graphql-rust/juniper/pull/1006))
23+
- 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))
24+
- Support `isRepeatable` field on directives. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000))
25+
- 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))
2326

2427
## Fixes
2528

juniper/src/executor_tests/introspection/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ async fn scalar_introspection() {
492492
name
493493
kind
494494
description
495+
specifiedByUrl
495496
fields { name }
496497
interfaces { name }
497498
possibleTypes { name }
@@ -527,6 +528,7 @@ async fn scalar_introspection() {
527528
"name": "SampleScalar",
528529
"kind": "SCALAR",
529530
"description": null,
531+
"specifiedByUrl": null,
530532
"fields": null,
531533
"interfaces": null,
532534
"possibleTypes": null,

juniper/src/introspection/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
/// From <https://github.com/graphql/graphql-js/blob/8c96dc8276f2de27b8af9ffbd71a4597d483523f/src/utilities/introspectionQuery.js#L21>
1+
/// From <https://github.com/graphql/graphql-js/blob/90bd6ff72625173dd39a1f82cfad9336cfad8f65/src/utilities/getIntrospectionQuery.ts#L62>
22
pub(crate) const INTROSPECTION_QUERY: &str = include_str!("./query.graphql");
33
pub(crate) const INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS: &str =
44
include_str!("./query_without_descriptions.graphql");
55

66
/// The desired GraphQL introspection format for the canonical query
7-
/// (<https://github.com/graphql/graphql-js/blob/8c96dc8276f2de27b8af9ffbd71a4597d483523f/src/utilities/introspectionQuery.js#L21>)
7+
/// (<https://github.com/graphql/graphql-js/blob/90bd6ff72625173dd39a1f82cfad9336cfad8f65/src/utilities/getIntrospectionQuery.ts#L62>)
88
pub enum IntrospectionFormat {
99
/// The canonical GraphQL introspection query.
1010
All,

juniper/src/introspection/query.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
query IntrospectionQuery {
22
__schema {
3+
description
34
queryType {
45
name
56
}
@@ -15,6 +16,7 @@ query IntrospectionQuery {
1516
directives {
1617
name
1718
description
19+
isRepeatable
1820
locations
1921
args {
2022
...InputValue
@@ -26,6 +28,7 @@ fragment FullType on __Type {
2628
kind
2729
name
2830
description
31+
specifiedByUrl
2932
fields(includeDeprecated: true) {
3033
name
3134
description

juniper/src/introspection/query_without_descriptions.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ query IntrospectionQuery {
1414
}
1515
directives {
1616
name
17+
isRepeatable
1718
locations
1819
args {
1920
...InputValue
@@ -24,6 +25,7 @@ query IntrospectionQuery {
2425
fragment FullType on __Type {
2526
kind
2627
name
28+
specifiedByUrl
2729
fields(includeDeprecated: true) {
2830
name
2931
args {

juniper/src/schema/meta.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub struct ScalarMeta<'a, S> {
4848
pub name: Cow<'a, str>,
4949
#[doc(hidden)]
5050
pub description: Option<String>,
51+
#[doc(hidden)]
52+
pub specified_by_url: Option<Cow<'a, str>>,
5153
pub(crate) try_parse_fn: for<'b> fn(&'b InputValue<S>) -> Result<(), FieldError<S>>,
5254
pub(crate) parse_fn: for<'b> fn(ScalarToken<'b>) -> Result<S, ParseError<'b>>,
5355
}
@@ -250,9 +252,24 @@ impl<'a, S> MetaType<'a, S> {
250252
}
251253
}
252254

255+
/// Accesses the [specification URL][0], if applicable.
256+
///
257+
/// Only custom GraphQL scalars can have a [specification URL][0].
258+
///
259+
/// [0]: https://spec.graphql.org/October2021#sec--specifiedBy
260+
pub fn specified_by_url(&self) -> Option<&str> {
261+
match self {
262+
Self::Scalar(ScalarMeta {
263+
specified_by_url, ..
264+
}) => specified_by_url.as_deref(),
265+
_ => None,
266+
}
267+
}
268+
253269
/// Construct a `TypeKind` for a given type
254270
///
255271
/// # Panics
272+
///
256273
/// Panics if the type represents a placeholder or nullable type.
257274
pub fn type_kind(&self) -> TypeKind {
258275
match *self {
@@ -421,6 +438,7 @@ impl<'a, S> ScalarMeta<'a, S> {
421438
Self {
422439
name,
423440
description: None,
441+
specified_by_url: None,
424442
try_parse_fn: try_parse_fn::<S, T>,
425443
parse_fn: <T as ParseScalarValue<S>>::from_str,
426444
}
@@ -434,6 +452,16 @@ impl<'a, S> ScalarMeta<'a, S> {
434452
self
435453
}
436454

455+
/// Sets the [specification URL][0] for this [`ScalarMeta`] type.
456+
///
457+
/// Overwrites any previously set [specification URL][0].
458+
///
459+
/// [0]: https://spec.graphql.org/October2021#sec--specifiedBy
460+
pub fn specified_by_url(mut self, url: impl Into<Cow<'a, str>>) -> Self {
461+
self.specified_by_url = Some(url.into());
462+
self
463+
}
464+
437465
/// Wraps this [`ScalarMeta`] type into a generic [`MetaType`].
438466
pub fn into_meta(self) -> MetaType<'a, S> {
439467
MetaType::Scalar(self)

0 commit comments

Comments
 (0)