Skip to content

Commit

Permalink
Assert proper Enum configuration.
Browse files Browse the repository at this point in the history
This asserts that Enums have properly configured values.

This also asserts that isTypeOf and resolveType are functions, if provided.

Finally, this changes the signature of EnumType.getValues() to return an array instead of an Object map, since every single use case was calling Object.keys.

This fixes #122
  • Loading branch information
leebyron committed Aug 14, 2015
1 parent fec9862 commit 2e29b78
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 65 deletions.
2 changes: 1 addition & 1 deletion src/type/__tests__/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ var ObjectType = new GraphQLObjectType({
});
var InterfaceType = new GraphQLInterfaceType({ name: 'Interface' });
var UnionType = new GraphQLUnionType({ name: 'Union', types: [ ObjectType ] });
var EnumType = new GraphQLEnumType({ name: 'Enum' });
var EnumType = new GraphQLEnumType({ name: 'Enum', values: { foo: {} } });
var InputObjectType = new GraphQLInputObjectType({ name: 'InputObject' });

describe('Type System: Example', () => {
Expand Down
142 changes: 142 additions & 0 deletions src/type/__tests__/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,33 @@ describe('Type System: Input Objects must have fields', () => {
});


describe('Type System: Object types must be assertable', () => {

it('accepts an Object type with an isTypeOf function', () => {
expect(() => {
schemaWithFieldType(new GraphQLObjectType({
name: 'AnotherObject',
isTypeOf: () => true,
fields: { f: { type: GraphQLString } }
}));
}).not.to.throw();
});

it('rejects an Object type with an incorrect type for isTypeOf', () => {
expect(() => {
schemaWithFieldType(new GraphQLObjectType({
name: 'AnotherObject',
isTypeOf: {},
fields: { f: { type: GraphQLString } }
}));
}).to.throw(
'AnotherObject must provide "isTypeOf" as a function.'
);
});

});


describe('Type System: Interface types must be resolvable', () => {

it('accepts an Interface type defining resolveType', () => {
Expand Down Expand Up @@ -669,6 +696,18 @@ describe('Type System: Interface types must be resolvable', () => {
}).not.to.throw();
});

it('rejects an Interface type with an incorrect type for resolveType', () => {
expect(() =>
new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: {},
fields: { f: { type: GraphQLString } }
})
).to.throw(
'AnotherInterface must provide "resolveType" as a function.'
);
});

it('rejects an Interface type not defining resolveType with implementing type not defining isTypeOf', () => {
expect(() => {
var InterfaceTypeWithoutResolveType = new GraphQLInterfaceType({
Expand Down Expand Up @@ -723,6 +762,18 @@ describe('Type System: Union types must be resolvable', () => {
).not.to.throw();
});

it('rejects an Interface type with an incorrect type for resolveType', () => {
expect(() =>
schemaWithFieldType(new GraphQLUnionType({
name: 'SomeUnion',
resolveType: {},
types: [ ObjectWithIsTypeOf ],
}))
).to.throw(
'SomeUnion must provide "resolveType" as a function.'
);
});

it('rejects a Union type not defining resolveType of Object types not defining isTypeOf', () => {
expect(() =>
schemaWithFieldType(new GraphQLUnionType({
Expand Down Expand Up @@ -826,6 +877,97 @@ describe('Type System: Scalar types must be serializable', () => {
});


describe('Type System: Enum types must be well defined', () => {

it('accepts a well defined Enum type with empty value definition', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
values: {
FOO: {},
BAR: {},
}
})
).not.to.throw();
});

it('accepts a well defined Enum type with internal value definition', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
values: {
FOO: { value: 10 },
BAR: { value: 20 },
}
})
).not.to.throw();
});

it('rejects an Enum type without values', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
})
).to.throw(
'SomeEnum values must be an object with value names as keys.'
);
});

it('rejects an Enum type with empty values', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
values: {}
})
).to.throw(
'SomeEnum values must be an object with value names as keys.'
);
});

it('rejects an Enum type with incorrectly typed values', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
values: [
{ FOO: 10 }
]
})
).to.throw(
'SomeEnum values must be an object with value names as keys.'
);
});

it('rejects an Enum type with missing value definition', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
values: {
FOO: null
}
})
).to.throw(
'SomeEnum.FOO must refer to an object with a "value" key representing ' +
'an internal value but got: null.'
);
});

it('rejects an Enum type with incorrectly typed value definition', () => {
expect(() =>
new GraphQLEnumType({
name: 'SomeEnum',
values: {
FOO: 10
}
})
).to.throw(
'SomeEnum.FOO must refer to an object with a "value" key representing ' +
'an internal value but got: 10.'
);
});

});


describe('Type System: Object fields must have output types', () => {

function schemaWithObjectFieldOfType(fieldType) {
Expand Down
100 changes: 66 additions & 34 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import invariant from '../jsutils/invariant';
import isNullish from '../jsutils/isNullish';
import { ENUM } from '../language/kinds';
import type {
OperationDefinition,
Expand Down Expand Up @@ -296,6 +297,12 @@ export class GraphQLObjectType {
invariant(config.name, 'Type must be named.');
this.name = config.name;
this.description = config.description;
if (config.isTypeOf) {
invariant(
typeof config.isTypeOf === 'function',
`${this} must provide "isTypeOf" as a function.`
);
}
this.isTypeOf = config.isTypeOf;
this._typeConfig = config;
addImplementationToInterfaces(this);
Expand Down Expand Up @@ -359,7 +366,7 @@ function defineFieldMap(
): GraphQLFieldDefinitionMap {
var fieldMap: any = resolveMaybeThunk(fields);
invariant(
typeof fieldMap === 'object' && !Array.isArray(fieldMap),
isPlainObj(fieldMap),
`${type} fields must be an object with field names as keys or a ` +
`function which returns such an object.`
);
Expand All @@ -381,7 +388,7 @@ function defineFieldMap(
field.args = [];
} else {
invariant(
typeof field.args === 'object' && !Array.isArray(field.args),
isPlainObj(field.args),
`${type}.${fieldName} args must be an object with argument names ` +
`as keys.`
);
Expand All @@ -404,6 +411,10 @@ function defineFieldMap(
return fieldMap;
}

function isPlainObj(obj) {
return obj && typeof obj === 'object' && !Array.isArray(obj);
}

/**
* Update the interfaces to know about this implementation.
* This is an rare and unfortunate use of mutation in the type definition
Expand Down Expand Up @@ -521,6 +532,12 @@ export class GraphQLInterfaceType {
invariant(config.name, 'Type must be named.');
this.name = config.name;
this.description = config.description;
if (config.resolveType) {
invariant(
typeof config.resolveType === 'function',
`${this} must provide "resolveType" as a function.`
);
}
this.resolveType = config.resolveType;
this._typeConfig = config;
this._implementations = [];
Expand Down Expand Up @@ -621,6 +638,12 @@ export class GraphQLUnionType {
invariant(config.name, 'Type must be named.');
this.name = config.name;
this.description = config.description;
if (config.resolveType) {
invariant(
typeof config.resolveType === 'function',
`${this} must provide "resolveType" as a function.`
);
}
this.resolveType = config.resolveType;
invariant(
Array.isArray(config.types) && config.types.length > 0,
Expand Down Expand Up @@ -711,18 +734,19 @@ export class GraphQLEnumType/* <T> */ {
description: ?string;

_enumConfig: GraphQLEnumTypeConfig/* <T> */;
_values: GraphQLEnumValueDefinitionMap/* <T> */;
_values: Array<GraphQLEnumValueDefinition/* <T> */>;
_valueLookup: Map<any/* T */, GraphQLEnumValueDefinition>;
_nameLookup: Map<string, GraphQLEnumValueDefinition>;
_nameLookup: { [valueName: string]: GraphQLEnumValueDefinition };

constructor(config: GraphQLEnumTypeConfig/* <T> */) {
this.name = config.name;
this.description = config.description;
this._values = defineEnumValues(this, config.values);
this._enumConfig = config;
}

getValues(): GraphQLEnumValueDefinitionMap/* <T> */ {
return this._values || (this._values = this._defineValueMap());
getValues(): Array<GraphQLEnumValueDefinition/* <T> */> {
return this._values;
}

serialize(value: any/* T */): ?string {
Expand All @@ -731,53 +755,37 @@ export class GraphQLEnumType/* <T> */ {
}

parseValue(value: any): ?any/* T */ {
var enumValue = this._getNameLookup().get(value);
var enumValue = this._getNameLookup()[value];
if (enumValue) {
return enumValue.value;
}
}

parseLiteral(valueAST: Value): ?any/* T */ {
if (valueAST.kind === ENUM) {
var enumValue = this._getNameLookup().get(valueAST.value);
var enumValue = this._getNameLookup()[valueAST.value];
if (enumValue) {
return enumValue.value;
}
}
}

_defineValueMap(): GraphQLEnumValueDefinitionMap/* <T> */ {
var valueMap = (this._enumConfig.values: any);
Object.keys(valueMap).forEach(valueName => {
var value = valueMap[valueName];
value.name = valueName;
if (!value.hasOwnProperty('value')) {
value.value = valueName;
}
});
return valueMap;
}

_getValueLookup(): Map<any/* T */, GraphQLEnumValueDefinition> {
if (!this._valueLookup) {
var lookup = new Map();
var values = this.getValues();
Object.keys(values).forEach(valueName => {
var value = values[valueName];
this.getValues().forEach(value => {
lookup.set(value.value, value);
});
this._valueLookup = lookup;
}
return this._valueLookup;
}

_getNameLookup(): Map<string, GraphQLEnumValueDefinition> {
_getNameLookup(): { [valueName: string]: GraphQLEnumValueDefinition } {
if (!this._nameLookup) {
var lookup = new Map();
var values = this.getValues();
Object.keys(values).forEach(valueName => {
var value = values[valueName];
lookup.set(value.name, value);
var lookup = Object.create(null);
this.getValues().forEach(value => {
lookup[value.name] = value;
});
this._nameLookup = lookup;
}
Expand All @@ -789,6 +797,34 @@ export class GraphQLEnumType/* <T> */ {
}
}

function defineEnumValues(
type: GraphQLEnumType,
valueMap: GraphQLEnumValueConfigMap/* <T> */
): Array<GraphQLEnumValueDefinition/* <T> */> {
invariant(
isPlainObj(valueMap),
`${type} values must be an object with value names as keys.`
);
var valueNames = Object.keys(valueMap);
invariant(
valueNames.length > 0,
`${type} values must be an object with value names as keys.`
);
return valueNames.map(valueName => {
var value = valueMap[valueName];
invariant(
isPlainObj(value),
`${type}.${valueName} must refer to an object with a "value" key ` +
`representing an internal value but got: ${value}.`
);
value.name = valueName;
if (isNullish(value.value)) {
value.value = valueName;
}
return value;
});
}

export type GraphQLEnumTypeConfig/* <T> */ = {
name: string;
values: GraphQLEnumValueConfigMap/* <T> */;
Expand All @@ -805,10 +841,6 @@ export type GraphQLEnumValueConfig/* <T> */ = {
description?: ?string;
}

export type GraphQLEnumValueDefinitionMap/* <T> */ = {
[valueName: string]: GraphQLEnumValueDefinition/* <T> */;
};

export type GraphQLEnumValueDefinition/* <T> */ = {
name: string;
value?: any/* T */;
Expand Down Expand Up @@ -859,7 +891,7 @@ export class GraphQLInputObjectType {
_defineFieldMap(): InputObjectFieldMap {
var fieldMap: any = resolveMaybeThunk(this._typeConfig.fields);
invariant(
typeof fieldMap === 'object' && !Array.isArray(fieldMap),
isPlainObj(fieldMap),
`${this} fields must be an object with field names as keys or a ` +
`function which returns such an object.`
);
Expand Down
Loading

0 comments on commit 2e29b78

Please sign in to comment.