Skip to content

Commit

Permalink
Add support for Symbol.toStringTag
Browse files Browse the repository at this point in the history
**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.
  • Loading branch information
nyteshade authored and Brielle Harrison committed May 25, 2018
1 parent 0a30b62 commit 1c5bd38
Show file tree
Hide file tree
Showing 8 changed files with 8,648 additions and 0 deletions.
8,370 changes: 8,370 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions src/jsutils/__tests__/symbolSupport-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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.
*/

import { expect } from 'chai';
import { describe, it } from 'mocha';
import { hasSymbolSupport, applyToStringTag } from '../symbolSupport';

describe('symbolSupportTests', () => {
// NOTE Symbol appeared in nodejs in 0.12.18 but was largely unusable in
// that format. Symbol.toStringTag showed up in node 6.4.0, but was available
// behind a flag as early as 4.9.1.
const [, major, minor, patch] = /^v(\d+)\.?(\d+)?\.?(\d+)?/.exec(
process.version,
);

it('should have Symbol in scope if the version >= 4.9.1', () => {
expect(major >= 4).to.equal(true);

if (major === 4) {
expect(minor >= 9).to.equal(true);
}

if (minor === 9) {
expect(patch >= 1).to.equal(true);
}

expect(hasSymbolSupport()).to.equal(true);
});

it('should have Symbol in scope if the version >= 6.4.0', () => {
expect(major >= 6).to.equal(true);

if (major === 6) {
expect(minor >= 4).to.equal(true);
}

if (minor === 4) {
expect(patch >= 0).to.equal(true);
}

expect(typeof Symbol !== 'undefined').to.equal(true);
expect(hasSymbolSupport('toStringTag')).to.equal(true);
});

it('should be able to apply toStringTag to a class', () => {
class A {}
applyToStringTag(A);

const a = new A();

expect(Object.prototype.toString.call(a)).to.equal('[object A]');
expect(Object.prototype.toString.call(A)).not.to.equal('[object A]');
});
});
80 changes: 80 additions & 0 deletions src/jsutils/symbolSupport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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
*/

/**
* A function that can either simply determine if `Symbol`s are allowed as
* well as, optionally, determine whether or not a property of symbol
* exists.
*
* If the name of a specific `Symbol` property is supplied, the resulting
* value will only be true if property is mapped to an instance of `Symbol`
* such as `.toStringTag`, `.iterator`, `.species`, `.isConcatSpreadable`,
* etc...
*
* @method hasSymbolSupport
*
* @param {string} specificSymbol an optional name of a property on the
* `Symbol` class itself that statically refers to a predefined `Symbol`. If
* properly specified and the value is set, then true will be returned.
* @return {bool} true if `Symbol` is both defined and a function. Optionally
* true if the `Symbol` class is true, a predefined symbol name such as
* `toStringTag` is set on the `Symbol` class and it maps to an instance of
* `Symbol`. False in all other cases
*/
export function hasSymbolSupport(specificSymbol?: string): boolean {
const hasSymbols: boolean = typeof Symbol === 'function';

if (!hasSymbols) {
return false;
}

if (specificSymbol) {
// NOTE: The capture of type and string comparison over a few lines is
// necessary to appease the lint and flowtype gods.
//
// ((typeof Symbol[specificSymbol]): string) !== 'symbol' makes lint angry
// and typeof Symbol[specificSymbol] !== 'symbol' makes flow angry
//
// The former thinks everything is too verbose the later thinks I am
// comparing Symbol instance rather than the string resulting from a call
// to typeof.
//
// Le sigh....
const type: string = typeof Symbol[specificSymbol];

if (type !== 'symbol') {
return false;
}
}

return true;
}

/**
* 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 {
if (hasSymbolSupport('toStringTag')) {
Object.defineProperty(classObject.prototype, Symbol.toStringTag, {
get() {
return this.constructor.name;
},
});
}
}
4 changes: 4 additions & 0 deletions src/language/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import invariant from '../jsutils/invariant';
import { applyToStringTag } from '../jsutils/symbolSupport';

type Location = {
line: number,
Expand Down Expand Up @@ -41,3 +42,6 @@ export class Source {
);
}
}

// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
applyToStringTag(Source);
109 changes: 109 additions & 0 deletions src/type/__tests__/toStringTag-test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
19 changes: 19 additions & 0 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @flow strict
*/

import { applyToStringTag } from '../jsutils/symbolSupport';
import instanceOf from '../jsutils/instanceOf';
import invariant from '../jsutils/invariant';
import isInvalid from '../jsutils/isInvalid';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1131,6 +1144,9 @@ export class GraphQLEnumType /* <T> */ {
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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/type/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from './definition';
import { GraphQLNonNull } from './definition';
import { GraphQLString, GraphQLBoolean } from './scalars';
import { applyToStringTag } from '../jsutils/symbolSupport';
import instanceOf from '../jsutils/instanceOf';
import invariant from '../jsutils/invariant';
import type { DirectiveDefinitionNode } from '../language/ast';
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/type/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from './directives';
import type { GraphQLError } from '../error/GraphQLError';
import { __Schema } from './introspection';
import { applyToStringTag } from '../jsutils/symbolSupport';
import find from '../jsutils/find';
import instanceOf from '../jsutils/instanceOf';
import invariant from '../jsutils/invariant';
Expand Down Expand Up @@ -230,6 +231,9 @@ export class GraphQLSchema {
}
}

// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
applyToStringTag(GraphQLSchema);

type TypeMap = ObjMap<GraphQLNamedType>;

export type GraphQLSchemaValidationOptions = {|
Expand Down

0 comments on commit 1c5bd38

Please sign in to comment.