Merge branch 'master' into fix-codegen-tests

This commit is contained in:
ilslv 2021-12-20 15:05:26 +03:00
commit c10ac894b8
24 changed files with 453 additions and 15 deletions

View file

@ -0,0 +1,7 @@
use juniper::GraphQLScalarValue;
#[derive(GraphQLScalarValue)]
#[graphql(specified_by_url = "not an url")]
struct ScalarSpecifiedByUrl(i64);
fn main() {}

View file

@ -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")]
| ^^^^^^^^^^^^

View file

@ -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() {}

View file

@ -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")]
| ^^^^^^^^^^^^^^^^

View file

@ -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::<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(

View file

@ -9,6 +9,7 @@ struct DefaultName(i32);
struct OtherOrder(i32);
struct Named(i32);
struct ScalarDescription(i32);
struct ScalarSpecifiedByUrl(i32);
struct Generated(String);
struct Root;
@ -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]
@ -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())
}
@ -297,6 +318,7 @@ async fn scalar_description_introspection() {
__type(name: "ScalarDescription") {
name
description
specifiedByUrl
}
}
"#;
@ -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;
}

View file

@ -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"),
);
}

View file

@ -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

View file

@ -492,6 +492,7 @@ async fn scalar_introspection() {
name
kind
description
specifiedByUrl
fields { name }
interfaces { name }
possibleTypes { name }
@ -527,6 +528,7 @@ async fn scalar_introspection() {
"name": "SampleScalar",
"kind": "SCALAR",
"description": null,
"specifiedByUrl": null,
"fields": null,
"interfaces": null,
"possibleTypes": null,

View file

@ -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,

View file

@ -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

View file

@ -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 {

View file

@ -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>>,
}
@ -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 {
@ -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,
}
@ -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)

View file

@ -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<Cow<'a, str>>,
pub(crate) types: FnvHashMap<Name, MetaType<'a, S>>,
pub(crate) query_type_name: String,
pub(crate) mutation_type_name: Option<String>,
@ -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)]
@ -80,6 +82,7 @@ pub enum DirectiveLocation {
Mutation,
Subscription,
Field,
Scalar,
#[graphql(name = "FRAGMENT_DEFINITION")]
FragmentDefinition,
#[graphql(name = "FRAGMENT_SPREAD")]
@ -211,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::<SchemaType<S>>("__schema", &()),
@ -235,6 +242,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 +259,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);
@ -489,12 +502,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 +525,7 @@ where
DirectiveLocation::InlineFragment,
],
&[registry.arg::<bool>("if", &())],
false,
)
}
@ -525,6 +541,19 @@ where
DirectiveLocation::InlineFragment,
],
&[registry.arg::<bool>("if", &())],
false,
)
}
fn new_specified_by(registry: &mut Registry<'a, S>) -> DirectiveType<'a, S>
where
S: ScalarValue,
{
Self::new(
"specifiedBy",
&[DirectiveLocation::Scalar],
&[registry.arg::<String>("url", &())],
false,
)
}
@ -544,6 +573,7 @@ impl fmt::Display for DirectiveLocation {
DirectiveLocation::FragmentDefinition => "fragment definition",
DirectiveLocation::FragmentSpread => "fragment spread",
DirectiveLocation::InlineFragment => "inline fragment",
DirectiveLocation::Scalar => "scalar",
})
}
}

View file

@ -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()
@ -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(),
Self::NonNull(_) | Self::List(..) => 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<S>] {
&self.arguments
}

View file

@ -208,6 +208,12 @@ async fn test_introspection_directives() {
"INLINE_FRAGMENT",
],
},
{
"name": "specifiedBy",
"locations": [
"SCALAR",
],
},
],
},
});

View file

@ -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,
@ -1023,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
@ -1031,6 +1075,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 +1174,7 @@ pub(crate) fn schema_introspection_result() -> Value {
"kind": "OBJECT",
"name": "__Directive",
"description": null,
"specifiedByUrl": null,
"fields": [
{
"name": "name",
@ -1158,6 +1204,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 +1327,7 @@ pub(crate) fn schema_introspection_result() -> Value {
{
"name": "skip",
"description": null,
"isRepeatable": false,
"locations": [
"FIELD",
"FRAGMENT_SPREAD",
@ -1290,6 +1353,7 @@ pub(crate) fn schema_introspection_result() -> Value {
{
"name": "include",
"description": null,
"isRepeatable": false,
"locations": [
"FIELD",
"FRAGMENT_SPREAD",
@ -1311,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
}
]
}
]
}
@ -1331,6 +1419,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "Human",
"specifiedByUrl": null,
"fields": [
{
"name": "id",
@ -1430,6 +1519,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "SCALAR",
"name": "Boolean",
"specifiedByUrl": null,
"fields": null,
"inputFields": null,
"interfaces": null,
@ -1439,6 +1529,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "__InputValue",
"specifiedByUrl": null,
"fields": [
{
"name": "name",
@ -1501,6 +1592,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "SCALAR",
"name": "String",
"specifiedByUrl": null,
"fields": null,
"inputFields": null,
"interfaces": null,
@ -1510,6 +1602,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "__Field",
"specifiedByUrl": null,
"fields": [
{
"name": "name",
@ -1610,6 +1703,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "ENUM",
"name": "__TypeKind",
"specifiedByUrl": null,
"fields": null,
"inputFields": null,
"interfaces": null,
@ -1660,6 +1754,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "__Type",
"specifiedByUrl": null,
"fields": [
{
"name": "name",
@ -1795,6 +1890,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 +1939,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 +2044,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "Droid",
"specifiedByUrl": null,
"fields": [
{
"name": "id",
@ -2025,6 +2144,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "Query",
"specifiedByUrl": null,
"fields": [
{
"name": "human",
@ -2106,6 +2226,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "__EnumValue",
"specifiedByUrl": null,
"fields": [
{
"name": "name",
@ -2168,6 +2289,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "ENUM",
"name": "Episode",
"specifiedByUrl": null,
"fields": null,
"inputFields": null,
"interfaces": null,
@ -2193,6 +2315,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "ENUM",
"name": "__DirectiveLocation",
"specifiedByUrl": null,
"fields": null,
"inputFields": null,
"interfaces": null,
@ -2231,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
@ -2238,6 +2366,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "INTERFACE",
"name": "Character",
"specifiedByUrl": null,
"fields": [
{
"name": "id",
@ -2331,6 +2460,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
{
"kind": "OBJECT",
"name": "__Directive",
"specifiedByUrl": null,
"fields": [
{
"name": "name",
@ -2358,6 +2488,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 +2604,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
"directives": [
{
"name": "skip",
"isRepeatable": false,
"locations": [
"FIELD",
"FRAGMENT_SPREAD",
@ -2482,6 +2628,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
},
{
"name": "include",
"isRepeatable": false,
"locations": [
"FIELD",
"FRAGMENT_SPREAD",
@ -2502,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
}
]
}
]
}

View file

@ -855,31 +855,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 =

View file

@ -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.0"
[dev-dependencies]
derive_more = "0.99.7"

View file

@ -6,12 +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<bool>,
name: Option<String>,
description: Option<String>,
specified_by_url: Option<Url>,
scalar: Option<syn::Type>,
}
@ -21,6 +23,7 @@ impl syn::parse::Parse for TransparentAttributes {
transparent: None,
name: None,
description: None,
specified_by_url: None,
scalar: None,
};
@ -37,6 +40,14 @@ impl syn::parse::Parse for TransparentAttributes {
let val = input.parse::<syn::LitStr>()?;
output.description = Some(val.value());
}
"specified_by_url" => {
input.parse::<token::Eq>()?;
let val: syn::LitStr = input.parse::<syn::LitStr>()?;
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);
}
@ -101,10 +112,11 @@ 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| {
let url_lit = url.as_str();
quote!(.specified_by_url(#url_lit))
});
let scalar = attrs
.scalar
@ -159,6 +171,7 @@ fn impl_scalar_struct(
{
registry.build_scalar_type::<Self>(info)
#description
#specified_by_url
.into_meta()
}
}

View file

@ -202,10 +202,11 @@ 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| {
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),
_ => quote!(#custom_data_type),
@ -273,6 +274,7 @@ pub fn build_scalar(
{
registry.build_scalar_type::<Self>(info)
#description
#specified_by_url
.into_meta()
}
}

View file

@ -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<S> GraphQLScalar for UserID
/// where
/// S: juniper::ScalarValue

View file

@ -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 _;
@ -454,6 +455,7 @@ pub enum FieldAttributeParseMode {
enum FieldAttribute {
Name(SpanContainer<syn::LitStr>),
Description(SpanContainer<syn::LitStr>),
SpecifiedByUrl(SpanContainer<syn::LitStr>),
Deprecation(SpanContainer<DeprecationAttr>),
Skip(SpanContainer<syn::Ident>),
Arguments(HashMap<String, FieldAttributeArgument>),
@ -488,6 +490,15 @@ impl Parse for FieldAttribute {
lit,
)))
}
"specified_by_url" => {
input.parse::<token::Eq>()?;
let lit = input.parse::<syn::LitStr>()?;
Ok(FieldAttribute::SpecifiedByUrl(SpanContainer::new(
ident.span(),
Some(lit.span()),
lit,
)))
}
"deprecated" | "deprecation" => {
let reason = if input.peek(token::Eq) {
input.parse::<token::Eq>()?;
@ -542,7 +553,9 @@ pub struct FieldAttributes {
pub name: Option<SpanContainer<String>>,
pub description: Option<SpanContainer<String>>,
pub deprecation: Option<SpanContainer<DeprecationAttr>>,
// Only relevant for GraphQLObject derive.
/// Only relevant for scalar impl macro.
pub specified_by_url: Option<SpanContainer<Url>>,
/// Only relevant for GraphQLObject derive.
pub skip: Option<SpanContainer<syn::Ident>>,
/// Only relevant for object macro.
pub arguments: HashMap<String, FieldAttributeArgument>,
@ -564,6 +577,18 @@ 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| Url::parse(&val.value()))
.transpose()
.map_err(|e| {
syn::Error::new(
e.span_ident(),
format!("Invalid URL: {}", e.inner()),
)
})?,
);
}
FieldAttribute::Deprecation(attr) => {
output.deprecation = Some(attr);
}

View file

@ -58,6 +58,15 @@ impl<T> SpanContainer<T> {
}
}
impl<T, E> SpanContainer<Result<T, E>> {
pub fn transpose(self) -> Result<SpanContainer<T>, SpanContainer<E>> {
match self.val {
Ok(v) => Ok(SpanContainer::new(self.ident, self.expr, v)),
Err(e) => Err(SpanContainer::new(self.ident, self.expr, e)),
}
}
}
impl<T> AsRef<T> for SpanContainer<T> {
fn as_ref(&self) -> &T {
&self.val