From 44c8adca38ab2ba28bfbea261c78618e4d4e5b3a Mon Sep 17 00:00:00 2001 From: Brielle Harrison Date: Thu, 22 Mar 2018 17:38:43 -0700 Subject: [PATCH] Add support for Symbol.toStringTag **Changes** * Add static and instance property getters for Symbol.toStringTag for each exported class. **Purpose** Being able to compare class and class instances via internal class type as per the definition and usage of Symbol.toStringTag allows other libraries to validate types during runtime in this manner. It also prevents them from having to patch the live values in their own codebases. **Contrived Example** With no modules and vanilla JavaScript we should be able to do something like the following. ```javascript let type = new GraphQLObjectType({name: 'Sample'}); if (({}).toString.call(type) === '[object GraphQLObjectType]') { // we have the right type of class } ``` However, with libraries such as `type-detect` or `ne-types` the code can look far cleaner. ```javascript // type-detect let type = require('type-detect') let obj = new GraphQLObjectType({name:'Example'}) assert(type(obj) === GraphQLObjectType.name) // ne-types let { typeOf } = require('ne-types') let obj = new GraphQLObjectType({name:'Example'}) assert(typeOf(obj) === GraphQLObjectType.name) ``` There are a lot of libraries out there, despite doing nearly the same thing in all cases, that support the usage of `Symbol.toStringTag` and by adding support for that in the base GraphQL classes, all of these libraries can be used with GraphQL. --- src/jsutils/applyToStringTag.js | 37 +++++++++ src/language/source.js | 4 + src/type/__tests__/toStringTag-test.js | 109 +++++++++++++++++++++++++ src/type/definition.js | 19 +++++ src/type/directives.js | 4 + src/type/schema.js | 4 + 6 files changed, 177 insertions(+) create mode 100644 src/jsutils/applyToStringTag.js create mode 100644 src/type/__tests__/toStringTag-test.js diff --git a/src/jsutils/applyToStringTag.js b/src/jsutils/applyToStringTag.js new file mode 100644 index 0000000000..9da9b3eaa5 --- /dev/null +++ b/src/jsutils/applyToStringTag.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +/** + * The `applyToStringTag()` function checks first to see if the runtime + * supports the `Symbol` class and then if the `Symbol.toStringTag` constant + * is defined as a `Symbol` instance. If both conditions are met, the + * Symbol.toStringTag property is defined as a getter that returns the + * supplied class constructor's name. + * + * @method applyToStringTag + * + * @param {Class<*>} classObject a class such as Object, String, Number but + * typically one of your own creation through the class keyword; `class A {}`, + * for example. + */ +export function applyToStringTag(classObject: Class<*>): void { + const symbolType: string = typeof Symbol; + const toStringTagType: string = typeof Symbol.toStringTag; + + if (symbolType === 'function' && toStringTagType === 'symbol') { + Object.defineProperty(classObject.prototype, Symbol.toStringTag, { + get() { + return this.constructor.name; + }, + }); + } +} + +/** Support both default export and named `applyToStringTag` export */ +export default applyToStringTag; diff --git a/src/language/source.js b/src/language/source.js index aae70b1df6..14817b69b3 100644 --- a/src/language/source.js +++ b/src/language/source.js @@ -8,6 +8,7 @@ */ import invariant from '../jsutils/invariant'; +import applyToStringTag from '../jsutils/applyToStringTag'; type Location = { line: number, @@ -41,3 +42,6 @@ export class Source { ); } } + +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(Source); diff --git a/src/type/__tests__/toStringTag-test.js b/src/type/__tests__/toStringTag-test.js new file mode 100644 index 0000000000..e1d977ec6f --- /dev/null +++ b/src/type/__tests__/toStringTag-test.js @@ -0,0 +1,109 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { + GraphQLDirective, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLSchema, + GraphQLUnionType, + Source, +} from '../../'; + +function typeOf(object) { + return /(\b\w+\b)\]/.exec(Object.prototype.toString.call(object))[1]; +} + +describe('Check to see if Symbol.toStringTag is defined on types', () => { + const s = Symbol.toStringTag; + const hasSymbol = o => Object.getOwnPropertySymbols(o).includes(s); + + it('GraphQLDirective should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLDirective.prototype)).to.equal(true); + }); + + it('GraphQLEnumType should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLEnumType.prototype)).to.equal(true); + }); + + it('GraphQLInputObjectType should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLInputObjectType.prototype)).to.equal(true); + }); + + it('GraphQLInterfaceType should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLInterfaceType.prototype)).to.equal(true); + }); + + it('GraphQLObjectType should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLObjectType.prototype)).to.equal(true); + }); + + it('GraphQLScalarType should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLScalarType.prototype)).to.equal(true); + }); + + it('GraphQLSchema should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLSchema.prototype)).to.equal(true); + }); + + it('GraphQLUnionType should have Symbol.toStringTag', () => { + expect(hasSymbol(GraphQLUnionType.prototype)).to.equal(true); + }); + + it('Source should have Symbol.toStringTag', () => { + expect(hasSymbol(Source.prototype)).to.equal(true); + }); +}); + +describe('Check to see if Symbol.toStringTag tests on instances', () => { + // variables _interface and _enum have preceding underscores due to being + // reserved keywords in JavaScript + + const schema = Object.create(GraphQLSchema.prototype); + const scalar = Object.create(GraphQLScalarType.prototype); + const object = Object.create(GraphQLObjectType.prototype); + const _interface = Object.create(GraphQLInterfaceType.prototype); + const union = Object.create(GraphQLUnionType.prototype); + const _enum = Object.create(GraphQLEnumType.prototype); + const inputType = Object.create(GraphQLInputObjectType.prototype); + const directive = Object.create(GraphQLDirective.prototype); + const source = Object.create(Source.prototype); + + it('should return the class name for GraphQLSchema instance', () => { + expect(typeOf(schema)).to.equal(GraphQLSchema.name); + }); + + it('should return the class name for GraphQLScalarType instance', () => { + expect(typeOf(scalar)).to.equal(GraphQLScalarType.name); + }); + + it('should return the class name for GraphQLObjectType instance', () => { + expect(typeOf(object)).to.equal(GraphQLObjectType.name); + }); + + it('should return the class name for GraphQLInterfaceType instance', () => { + expect(typeOf(_interface)).to.equal(GraphQLInterfaceType.name); + }); + + it('should return the class name for GraphQLUnionType instance', () => { + expect(typeOf(union)).to.equal(GraphQLUnionType.name); + }); + + it('should return the class name for GraphQLEnumType instance', () => { + expect(typeOf(_enum)).to.equal(GraphQLEnumType.name); + }); + + it('should return the class name for GraphQLInputObjectType instance', () => { + expect(typeOf(inputType)).to.equal(GraphQLInputObjectType.name); + }); + + it('should return the class name for GraphQLDirective instance', () => { + expect(typeOf(directive)).to.equal(GraphQLDirective.name); + }); + + it('should return the class name for Source instance', () => { + expect(typeOf(source)).to.equal(Source.name); + }); +}); diff --git a/src/type/definition.js b/src/type/definition.js index 2d7025a470..586b248ba5 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -7,6 +7,7 @@ * @flow strict */ +import applyToStringTag from '../jsutils/applyToStringTag'; import instanceOf from '../jsutils/instanceOf'; import invariant from '../jsutils/invariant'; import isInvalid from '../jsutils/isInvalid'; @@ -586,6 +587,9 @@ export class GraphQLScalarType { inspect: () => string; } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLScalarType); + // Also provide toJSON and inspect aliases for toString. GraphQLScalarType.prototype.toJSON = GraphQLScalarType.prototype.inspect = GraphQLScalarType.prototype.toString; @@ -688,6 +692,9 @@ export class GraphQLObjectType { inspect: () => string; } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLObjectType); + // Also provide toJSON and inspect aliases for toString. GraphQLObjectType.prototype.toJSON = GraphQLObjectType.prototype.inspect = GraphQLObjectType.prototype.toString; @@ -937,6 +944,9 @@ export class GraphQLInterfaceType { inspect: () => string; } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLInterfaceType); + // Also provide toJSON and inspect aliases for toString. GraphQLInterfaceType.prototype.toJSON = GraphQLInterfaceType.prototype.inspect = GraphQLInterfaceType.prototype.toString; @@ -1016,6 +1026,9 @@ export class GraphQLUnionType { inspect: () => string; } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLUnionType); + // Also provide toJSON and inspect aliases for toString. GraphQLUnionType.prototype.toJSON = GraphQLUnionType.prototype.inspect = GraphQLUnionType.prototype.toString; @@ -1131,6 +1144,9 @@ export class GraphQLEnumType /* */ { inspect: () => string; } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLEnumType); + // Also provide toJSON and inspect aliases for toString. GraphQLEnumType.prototype.toJSON = GraphQLEnumType.prototype.inspect = GraphQLEnumType.prototype.toString; @@ -1264,6 +1280,9 @@ export class GraphQLInputObjectType { inspect: () => string; } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLInputObjectType); + // Also provide toJSON and inspect aliases for toString. GraphQLInputObjectType.prototype.toJSON = GraphQLInputObjectType.prototype.toString; diff --git a/src/type/directives.js b/src/type/directives.js index 9d4d7764f8..1d5ecea851 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -13,6 +13,7 @@ import type { } from './definition'; import { GraphQLNonNull } from './definition'; import { GraphQLString, GraphQLBoolean } from './scalars'; +import applyToStringTag from '../jsutils/applyToStringTag'; import instanceOf from '../jsutils/instanceOf'; import invariant from '../jsutils/invariant'; import type { DirectiveDefinitionNode } from '../language/ast'; @@ -76,6 +77,9 @@ export class GraphQLDirective { } } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLDirective); + export type GraphQLDirectiveConfig = { name: string, description?: ?string, diff --git a/src/type/schema.js b/src/type/schema.js index 96ac0167b3..0762251f2e 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -32,6 +32,7 @@ import { } from './directives'; import type { GraphQLError } from '../error/GraphQLError'; import { __Schema } from './introspection'; +import applyToStringTag from '../jsutils/applyToStringTag'; import find from '../jsutils/find'; import instanceOf from '../jsutils/instanceOf'; import invariant from '../jsutils/invariant'; @@ -230,6 +231,9 @@ export class GraphQLSchema { } } +// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported +applyToStringTag(GraphQLSchema); + type TypeMap = ObjMap; export type GraphQLSchemaValidationOptions = {|