Skip to content

Commit

Permalink
Add @specified directive
Browse files Browse the repository at this point in the history
This in an implementation for a spec proposal:

* Spec proposal: [[RFC] Custom Scalar Specification URIs](graphql/graphql-spec#649)
* Original issue: [[RFC] Custom Scalar Specification URIs](graphql/graphql-spec#635)
  • Loading branch information
m14t committed Dec 31, 2019
1 parent 576682f commit 18940cf
Show file tree
Hide file tree
Showing 19 changed files with 246 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/APIReference-TypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ type GraphQLScalarTypeConfig<InternalType> = {
serialize: (value: mixed) => ?InternalType;
parseValue?: (value: mixed) => ?InternalType;
parseLiteral?: (valueAST: Value) => ?InternalType;
specifiedBy?: string;
}
```
Expand Down
23 changes: 23 additions & 0 deletions src/type/__tests__/definition-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ describe('Type System: Scalars', () => {
expect(() => new GraphQLScalarType({ name: 'SomeScalar' })).not.to.throw();
});

it('accepts a Scalar type defining specifiedBy', () => {
expect(
() =>
new GraphQLScalarType({
name: 'SomeScalar',
specifiedBy: 'https://tools.ietf.org/html/rfc4122',
}),
).not.to.throw();
});

it('accepts a Scalar type defining parseValue and parseLiteral', () => {
expect(
() =>
Expand Down Expand Up @@ -118,6 +128,19 @@ describe('Type System: Scalars', () => {
'SomeScalar must provide both "parseValue" and "parseLiteral" functions.',
);
});

it('rejects a Scalar type defining specifiedBy with an incorrect type', () => {
expect(
() =>
new GraphQLScalarType({
name: 'SomeScalar',
// $DisableFlowOnNegativeTest
specifiedBy: {},
}),
).to.throw(
'SomeScalar must provide "specifiedBy" as a string, but got: {}.',
);
});
});

describe('Type System: Objects', () => {
Expand Down
41 changes: 41 additions & 0 deletions src/type/__tests__/introspection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'SCALAR',
Expand All @@ -68,6 +69,7 @@ describe('Introspection', () => {
interfaces: null,
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -161,6 +163,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -203,6 +206,17 @@ describe('Introspection', () => {
isDeprecated: false,
deprecationReason: null,
},
{
args: [],
deprecationReason: null,
isDeprecated: false,
name: 'specifiedBy',
type: {
kind: 'SCALAR',
name: 'String',
ofType: null,
},
},
{
name: 'fields',
args: [
Expand Down Expand Up @@ -334,6 +348,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'ENUM',
Expand Down Expand Up @@ -384,6 +399,7 @@ describe('Introspection', () => {
},
],
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'SCALAR',
Expand All @@ -393,6 +409,7 @@ describe('Introspection', () => {
interfaces: null,
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -493,6 +510,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -555,6 +573,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -617,6 +636,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -699,6 +719,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'ENUM',
Expand Down Expand Up @@ -804,6 +825,7 @@ describe('Introspection', () => {
},
],
possibleTypes: null,
specifiedBy: null,
},
],
directives: [
Expand Down Expand Up @@ -845,6 +867,25 @@ describe('Introspection', () => {
},
],
},
{
name: 'specified',
locations: ['SCALAR'],
args: [
{
defaultValue: null,
name: 'by',
type: {
kind: 'NON_NULL',
name: null,
ofType: {
kind: 'SCALAR',
name: 'String',
ofType: null,
},
},
},
],
},
{
name: 'deprecated',
locations: ['FIELD_DEFINITION', 'ENUM_VALUE'],
Expand Down
3 changes: 3 additions & 0 deletions src/type/definition.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export class GraphQLScalarType {
extensions: Maybe<Readonly<Record<string, any>>>;
astNode: Maybe<ScalarTypeDefinitionNode>;
extensionASTNodes: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
specifiedBy?: Maybe<string>;

constructor(config: Readonly<GraphQLScalarTypeConfig<any, any>>);

Expand All @@ -300,6 +301,7 @@ export class GraphQLScalarType {
parseLiteral: GraphQLScalarLiteralParser<any>;
extensions: Maybe<Readonly<Record<string, any>>>;
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
specifiedBy: Maybe<string>;
};

toString(): string;
Expand Down Expand Up @@ -330,6 +332,7 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
extensions?: Maybe<Readonly<Record<string, any>>>;
astNode?: Maybe<ScalarTypeDefinitionNode>;
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
specifiedBy?: Maybe<string>;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ export class GraphQLScalarType {
extensions: ?ReadOnlyObjMap<mixed>;
astNode: ?ScalarTypeDefinitionNode;
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>;
specifiedBy: ?string;

constructor(config: $ReadOnly<GraphQLScalarTypeConfig<mixed, mixed>>): void {
const parseValue = config.parseValue || identityFunc;
Expand All @@ -564,6 +565,7 @@ export class GraphQLScalarType {
this.extensions = config.extensions && toObjMap(config.extensions);
this.astNode = config.astNode;
this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes);
this.specifiedBy = config.specifiedBy;

devAssert(typeof config.name === 'string', 'Must provide name.');
devAssert(
Expand All @@ -578,6 +580,14 @@ export class GraphQLScalarType {
`${this.name} must provide both "parseValue" and "parseLiteral" functions.`,
);
}
if (config.specifiedBy != null) {
devAssert(
typeof config.specifiedBy === 'string',
`${this.name} must provide "specifiedBy" as a string, ` +
`but got: ${inspect(config.specifiedBy)}.`,
);
}
}
toConfig(): {|
Expand All @@ -587,6 +597,7 @@ export class GraphQLScalarType {
parseLiteral: GraphQLScalarLiteralParser<mixed>,
extensions: ?ReadOnlyObjMap<mixed>,
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
specifiedBy: ?string,
|} {
return {
name: this.name,
Expand All @@ -597,6 +608,7 @@ export class GraphQLScalarType {
extensions: this.extensions,
astNode: this.astNode,
extensionASTNodes: this.extensionASTNodes,
specifiedBy: this.specifiedBy,
};
}
Expand Down Expand Up @@ -628,6 +640,7 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
extensions?: ?ReadOnlyObjMapLike<mixed>,
astNode?: ?ScalarTypeDefinitionNode,
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
specifiedBy?: ?string,
|};
/**
Expand Down
5 changes: 5 additions & 0 deletions src/type/directives.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export const GraphQLIncludeDirective: GraphQLDirective;
*/
export const GraphQLSkipDirective: GraphQLDirective;

/**
* Used to provide a URL for specifying the behavior of custom scalar definitions.
*/
export const GraphQLSpecifiedDirective: GraphQLDirective;

/**
* Constant string used for default reason for a deprecation.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/type/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ export const GraphQLSkipDirective = new GraphQLDirective({
},
});

/**
* Used to provide a URL for specifying the behaviour of custom scalar definitions.
*/
export const GraphQLSpecifiedDirective = new GraphQLDirective({
name: 'specified',
description: 'Exposes a URL that specifies the behaviour of this scalar.',
locations: [DirectiveLocation.SCALAR],
args: {
by: {
type: GraphQLNonNull(GraphQLString),
description: 'The URL that specifies the behaviour of this scalar.',
},
},
});

/**
* Constant string used for default reason for a deprecation.
*/
Expand Down Expand Up @@ -195,6 +210,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({
export const specifiedDirectives = Object.freeze([
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLSpecifiedDirective,
GraphQLDeprecatedDirective,
]);

Expand Down
7 changes: 6 additions & 1 deletion src/type/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const __DirectiveLocation = new GraphQLEnumType({
export const __Type = new GraphQLObjectType({
name: '__Type',
description:
'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.',
'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.',
fields: () =>
({
kind: {
Expand Down Expand Up @@ -221,6 +221,11 @@ export const __Type = new GraphQLObjectType({
resolve: obj =>
obj.description !== undefined ? obj.description : undefined,
},
specifiedBy: {
type: GraphQLString,
resolve: obj =>
obj.specifiedBy !== undefined ? obj.specifiedBy : undefined,
},
fields: {
type: GraphQLList(GraphQLNonNull(__Field)),
args: {
Expand Down
Loading

0 comments on commit 18940cf

Please sign in to comment.