├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── example └── index.js ├── package.json ├── src ├── create-schema-generator.js ├── create-type-generator.js ├── index.js └── parser.js └── tests ├── index.js └── parser.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "optional": ["es7.objectRestSpread"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | npm-debug.log 4 | node_modules 5 | /lib 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | npm-debug.log 4 | node_modules 5 | /src 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Forbes Lindesay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-schema-gen 2 | 3 | Generate JavaScript GraphQL schema from the GraphQL language 4 | 5 | [![Build Status](https://img.shields.io/travis/ForbesLindesay/graphql-schema-gen/master.svg)](https://travis-ci.org/ForbesLindesay/graphql-schema-gen) 6 | [![Dependency Status](https://img.shields.io/david/ForbesLindesay/graphql-schema-gen.svg)](https://david-dm.org/ForbesLindesay/graphql-schema-gen) 7 | [![NPM version](https://img.shields.io/npm/v/graphql-schema-gen.svg)](https://www.npmjs.org/package/graphql-schema-gen) 8 | 9 | ## Installation 10 | 11 | npm install graphql-schema-gen 12 | 13 | 14 | ## Usage 15 | 16 | ```js 17 | var createQueryFn = require('graphql-schema-gen'); 18 | 19 | var query = createQueryFn(` 20 | scalar Date 21 | 22 | union Animal = 23 | Cat | 24 | Dog 25 | 26 | enum Role { 27 | # Means a user has full administrative access to everything 28 | Admin 29 | # Means a user has ability to moderate comments 30 | Moderator 31 | # Means a user has read only access 32 | None 33 | } 34 | 35 | type Cat { 36 | nightVision: String 37 | } 38 | type Dog { 39 | woof: String 40 | } 41 | 42 | interface Node { 43 | id: ID! 44 | } 45 | 46 | # A type to represent a person 47 | type Person implements Node { 48 | id: GlobalID 49 | name: String 50 | dateOfBirth: Date 51 | role: Role 52 | friends(): [Person]! 53 | pet(): Animal 54 | hello(name: String = "World"): String 55 | } 56 | 57 | type Query { 58 | me(): Node 59 | } 60 | `, { 61 | Date: { 62 | serialize(value) { 63 | return /^\d\d\d\d\-\d\d\-\d\d$/.test(value) ? value : null; 64 | }, 65 | parse(value) { 66 | return /^\d\d\d\d\-\d\d\-\d\d$/.test(value) ? value : null; 67 | } 68 | }, 69 | Node: { 70 | resolveType(node, info) { 71 | return info.schema.getType(node.type); 72 | } 73 | }, 74 | Animal: { 75 | resolveType(node, info) { 76 | return info.schema.getType(node.type); 77 | } 78 | }, 79 | Query: { 80 | me() { 81 | return {name: 'Forbes Lindesay', dateOfBirth: '1830-06-11', role: 'Admin', type: 'Person'}; 82 | } 83 | }, 84 | Person: { 85 | id(obj) { 86 | return obj.name; 87 | }, 88 | pet() { 89 | return { 90 | woof: 'Woof Woof!', 91 | type: 'Dog' 92 | }; 93 | }, 94 | friends() { 95 | return [{name: 'jonn', dateOfBirth: 'whatever', role: 'None', type: 'Person'}]; 96 | }, 97 | hello(person, {name}, context) { 98 | return 'Hello ' + name; 99 | } 100 | } 101 | }); 102 | 103 | query(` 104 | { 105 | me { 106 | id 107 | ...on Person { 108 | role 109 | dateOfBirth 110 | name, 111 | friends { id, name, dateOfBirth, role } 112 | pet { ...on Dog {woof}} 113 | hello 114 | helloForbes: hello(name: "Forbes") 115 | } 116 | } 117 | } 118 | `, {}).then(function (result) { 119 | if (result.errors) { 120 | setTimeout(() => { throw result.errors[0]; }, 0); 121 | } 122 | console.dir(result, {colors: true, depth: 100}); 123 | }); 124 | ``` 125 | 126 | ## License 127 | 128 | MIT 129 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var createQueryFn = require('../src/'); 2 | 3 | var query = createQueryFn(` 4 | scalar Date 5 | 6 | union Animal = 7 | Cat | 8 | Dog 9 | 10 | enum Role { 11 | # Means a user has full administrative access to everything 12 | Admin 13 | # Means a user has ability to moderate comments 14 | Moderator 15 | # Means a user has read only access 16 | None 17 | } 18 | 19 | type Cat { 20 | nightVision: String 21 | } 22 | type Dog { 23 | woof: String 24 | } 25 | 26 | interface Node { 27 | id: ID! 28 | } 29 | 30 | # A type to represent a person 31 | type Person implements Node { 32 | id: GlobalID 33 | name: String 34 | dateOfBirth: Date 35 | role: Role 36 | friends(): [Person]! 37 | pet(): Animal 38 | hello(name: String = "World"): String 39 | } 40 | 41 | type Query { 42 | me(): Node 43 | } 44 | `, { 45 | Date: { 46 | serialize(value) { 47 | return /^\d\d\d\d\-\d\d\-\d\d$/.test(value) ? value : null; 48 | }, 49 | parse(value) { 50 | return /^\d\d\d\d\-\d\d\-\d\d$/.test(value) ? value : null; 51 | } 52 | }, 53 | Node: { 54 | resolveType(node, info) { 55 | return info.schema.getType(node.type); 56 | } 57 | }, 58 | Animal: { 59 | resolveType(node, info) { 60 | return info.schema.getType(node.type); 61 | } 62 | }, 63 | Query: { 64 | me() { 65 | return {name: 'Forbes Lindesay', dateOfBirth: '1830-06-11', role: 'Admin', type: 'Person'}; 66 | } 67 | }, 68 | Person: { 69 | id(obj) { 70 | return obj.name; 71 | }, 72 | pet() { 73 | return { 74 | woof: 'Woof Woof!', 75 | type: 'Dog' 76 | }; 77 | }, 78 | friends() { 79 | return [{name: 'jonn', dateOfBirth: 'whatever', role: 'None', type: 'Person'}]; 80 | }, 81 | hello(person, {name}, context) { 82 | return 'Hello ' + name; 83 | } 84 | } 85 | }); 86 | 87 | query(` 88 | { 89 | me { 90 | id 91 | ...on Person { 92 | role 93 | dateOfBirth 94 | name, 95 | friends { id, name, dateOfBirth, role } 96 | pet { ...on Dog {woof}} 97 | hello 98 | helloForbes: hello(name: "Forbes") 99 | } 100 | } 101 | } 102 | `, {}).then(function (result) { 103 | if (result.errors) { 104 | setTimeout(() => { throw result.errors[0]; }, 0); 105 | } 106 | console.dir(result, {colors: true, depth: 100}); 107 | }); 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-schema-gen", 3 | "version": "0.0.1", 4 | "description": "Generate JavaScript GraphQL schema from the GraphQL language", 5 | "main": "./lib/index.js", 6 | "keywords": [], 7 | "dependencies": { 8 | "graphql": "^0.4.4" 9 | }, 10 | "devDependencies": { 11 | "babel": "^5.8.23", 12 | "babel-istanbul": "^0.3.20", 13 | "chalk": "^1.1.1", 14 | "diff": "^2.1.1", 15 | "testit": "^2.0.2" 16 | }, 17 | "scripts": { 18 | "prepublish": "npm run build", 19 | "build": "babel src --out-dir lib", 20 | "test": "babel-node tests/index.js", 21 | "coverage": "babel-istanbul cover tests/index.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/ForbesLindesay/graphql-schema-gen.git" 26 | }, 27 | "author": "ForbesLindesay", 28 | "license": "MIT" 29 | } -------------------------------------------------------------------------------- /src/create-schema-generator.js: -------------------------------------------------------------------------------- 1 | import createTypeGenerator from './create-type-generator'; 2 | 3 | export default function createSchemaGenerator(graphql) { 4 | var typeGenerator = createTypeGenerator(graphql); 5 | return function (document, implementation) { 6 | var {objectTypes} = typeGenerator(document, implementation); 7 | return new GraphQLSchema({ 8 | query: objectTypes['Query'] 9 | }); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/create-type-generator.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export default function createTypeGenerator(graphql) { 4 | let { 5 | GraphQLEnumType: EnumType, 6 | GraphQLInterfaceType: InterfaceType, 7 | GraphQLList: ListType, 8 | GraphQLNonNull: NonNullType, 9 | GraphQLObjectType: ObjectType, 10 | GraphQLScalarType: ScalarType, 11 | GraphQLUnionType: UnionType 12 | } = graphql; 13 | 14 | var builtInScalars = { 15 | 'String': graphql.GraphQLString, 16 | 'Int': graphql.GraphQLInt, 17 | 'Float': graphql.GraphQLFloat, 18 | 'Boolean': graphql.GraphQLBoolean, 19 | 'ID': graphql.GraphQLID, 20 | }; 21 | 22 | 23 | function toGlobalId(type, id) { 24 | if (typeof id !== 'string') { 25 | throw new Error('id of ' + type + ' must be a string'); 26 | } 27 | // return [type, id].join(':'); 28 | return new Buffer([type, id].join(':')).toString('base64'); 29 | } 30 | var builtInExtraTypes = { 31 | 'GlobalID': function (node, field, implementation) { 32 | return { 33 | name: field.name.value, 34 | type: new NonNullType(graphql.GraphQLID), 35 | description: 'The id of an object', 36 | resolve: function (obj) { 37 | return toGlobalId(node.name.value, implementation ? implementation(obj) : obj.id); 38 | } 39 | }; 40 | } 41 | }; 42 | 43 | return function generate(document, implementations, options) { 44 | var description = []; 45 | function getDescription() { 46 | if (description.length === 0) return null; 47 | var prefix = description.reduce(function (a, b) { 48 | return Math.min(a, /^\s*/.exec(b)[0].length); 49 | }, Infinity); 50 | var result = description.map(function (str) { 51 | return str.substr(prefix); 52 | }).join('\n'); 53 | description = []; 54 | return result; 55 | } 56 | var objectTypes = {}; 57 | var interfaceTypes = {}; 58 | var unionTypes = {}; 59 | var scalarTypes = {}; 60 | var enumTypes = {}; 61 | function getOutputType(node) { 62 | if (node.kind === 'NamedType') { 63 | var t = ( 64 | builtInScalars[node.name.value] || 65 | objectTypes[node.name.value] || 66 | interfaceTypes[node.name.value] || 67 | unionTypes[node.name.value] || 68 | scalarTypes[node.name.value] || 69 | enumTypes[node.name.value] 70 | ); 71 | if (!t) { 72 | throw new Error(node.name.value + ' is not implemented.'); 73 | } 74 | return t; 75 | } 76 | if (node.kind === 'ListType') { 77 | return new ListType(getOutputType(node.type)); 78 | } 79 | if (node.kind === 'NonNullType') { 80 | return new NonNullType(getOutputType(node.type)); 81 | } 82 | console.dir(node); 83 | throw new Error(node.kind + ' is not supported'); 84 | } 85 | function getInputType(node) { 86 | if (node.kind === 'NamedType') { 87 | var t = ( 88 | builtInScalars[node.name.value] || 89 | interfaceTypes[node.name.value] || 90 | unionTypes[node.name.value] || 91 | scalarTypes[node.name.value] || 92 | enumTypes[node.name.value] 93 | ); 94 | if (!t) { 95 | throw new Error(node.name.value + ' is not implemented.'); 96 | } 97 | return t; 98 | } 99 | if (node.kind === 'ListType') { 100 | return new ListType(getOutputType(node.values)); 101 | } 102 | if (node.kind === 'NonNullType') { 103 | return new NonNullType(getOutputType(node.type)); 104 | } 105 | console.dir(node); 106 | throw new Error(node.kind + ' is not supported'); 107 | } 108 | function getRawValue(node) { 109 | switch (node.kind) { 110 | case 'NumberValue': 111 | case 'StringValue': 112 | case 'BooleanValue': 113 | return node.value; 114 | case 'EnumValue': 115 | return node.name.value; 116 | case 'ListValue': 117 | return node.values.map(getRawValue); 118 | case 'ObjectValue': 119 | var res = {}; 120 | node.fields.forEach(function (field) { 121 | res[field.name.value] = getRawValue(field.value); 122 | }); 123 | return res; 124 | default: 125 | console.dir(node); 126 | throw new Error(node.kind + ' is not supported'); 127 | } 128 | } 129 | function getRawValueFromOfficialSchema(node) { 130 | switch (node.kind) { 131 | case 'IntValue': 132 | case 'FloatValue': 133 | return JSON.parse(node.value); 134 | case 'StringValue': 135 | case 'BooleanValue': 136 | case 'EnumValue': 137 | return node.value; 138 | case 'ListValue': 139 | return node.values.map(getRawValueFromOfficialSchema); 140 | case 'ObjectValue': 141 | var res = {}; 142 | node.fields.forEach(function (field) { 143 | res[field.name.value] = getRawValueFromOfficialSchema(field.value); 144 | }); 145 | return res; 146 | default: 147 | console.dir(node); 148 | throw new Error(node.kind + ' is not supported'); 149 | } 150 | } 151 | function getInterface(node) { 152 | assert(node.kind === 'NamedType'); 153 | var t = interfaceTypes[node.name.value]; 154 | if (!t) { 155 | throw new Error(node.name.value + ' is not defined.'); 156 | } 157 | return t; 158 | } 159 | function getFieldDefinitions(node, isInterface) { 160 | var typeName = node.name.value; 161 | var fields = {}; 162 | node.fields.forEach(function (field) { 163 | switch(field.kind) { 164 | case 'Comment': 165 | description.push(field.value.substr(1)); 166 | break; 167 | case 'FieldDefinition': 168 | if ( 169 | !isInterface && 170 | field.type.kind === 'NamedType' && 171 | builtInExtraTypes[field.type.name.value] 172 | ) { 173 | fields[field.name.value] = builtInExtraTypes[field.type.name.value]( 174 | node, 175 | field, 176 | implementations[typeName] && implementations[typeName][field.name.value] 177 | ); 178 | break; 179 | } 180 | if ( 181 | !isInterface && 182 | field.arguments && 183 | !(implementations[typeName] && implementations[typeName][field.name.value])) { 184 | throw new Error(typeName + '.' + field.name.value + ' is calculated (i.e. it ' + 185 | 'accepts arguments) but does not have an implementation'); 186 | } 187 | var args = undefined; 188 | if (field.arguments && field.arguments.length) { 189 | args = {}; 190 | field.arguments.forEach(function (arg) { 191 | if (arg.kind === 'Comment') return; 192 | args[arg.name.value] = { 193 | type: getInputType(arg.type), 194 | defaultValue: arg.defaultValue && getRawValue(arg.defaultValue) 195 | }; 196 | }); 197 | } 198 | fields[field.name.value] = { 199 | name: field.name.value, 200 | type: getOutputType(field.type), 201 | description: getDescription(), 202 | args: args, 203 | resolve: !isInterface && field.arguments 204 | ? implementations[typeName][field.name.value] 205 | : undefined 206 | // TODO: deprecationReason: string 207 | }; 208 | break; 209 | default: 210 | throw new Error('Unexpected node type ' + field.kind); 211 | } 212 | }); 213 | return fields; 214 | } 215 | function getObjectTypeDefinition(node) { 216 | var typeName = node.name.value; 217 | return new ObjectType({ 218 | name: typeName, 219 | description: getDescription(), 220 | // TODO: interfaces 221 | interfaces: node.interfaces 222 | ? () => node.interfaces.map(getInterface) 223 | : null, 224 | fields: function () { 225 | return getFieldDefinitions(node, false); 226 | } 227 | }); 228 | } 229 | function getInterfaceTypeDefinition(node) { 230 | return new InterfaceType({ 231 | name: node.name.value, 232 | description: getDescription(), 233 | // resolveType?: (value: any, info?: GraphQLResolveInfo) => ?GraphQLObjectType 234 | resolveType: implementations[node.name.value] && implementations[node.name.value]['resolveType'], 235 | fields: function () { 236 | return getFieldDefinitions(node, true); 237 | } 238 | }); 239 | } 240 | function getUnionTypeDefinition(node) { 241 | return new UnionType({ 242 | name: node.name.value, 243 | description: getDescription(), 244 | types: node.types.map(getOutputType), 245 | resolveType: implementations[node.name.value] && implementations[node.name.value]['resolveType'] 246 | }); 247 | } 248 | function getScalarTypeDefinition(node) { 249 | var imp = implementations[node.name.value]; 250 | return new ScalarType({ 251 | name: node.name.value, 252 | description: getDescription(), 253 | serialize: imp && imp.serialize, 254 | parseValue: imp && (imp.parseValue || imp.parse), 255 | parseLiteral: imp && imp.parseLiteral 256 | ? imp.parseLiteral 257 | : imp && (imp.parseValue || imp.parse) 258 | ? (ast) => (imp.parseValue || imp.parse)(getRawValueFromOfficialSchema(ast)) 259 | : undefined, 260 | }); 261 | } 262 | function getEnumTypeDefinition(node){ 263 | var d = getDescription(); 264 | var values = {}; 265 | node.values.forEach(function (value) { 266 | switch (value.kind) { 267 | case 'Comment': 268 | description.push(value.value.substr(1)); 269 | break; 270 | case 'EnumValueDefinition': 271 | values[value.name.value] = { 272 | description: getDescription(), 273 | value: implementations[node.name.value] && implementations[node.name.value][value.name.value] 274 | // deprecationReason?: string; 275 | }; 276 | break; 277 | default: 278 | throw new Error('Unexpected node type ' + value.kind); 279 | } 280 | }); 281 | return new EnumType({ 282 | name: node.name.value, 283 | description: d, 284 | values: values 285 | }); 286 | } 287 | var unions = []; 288 | document.definitions.forEach(function (node) { 289 | switch(node.kind) { 290 | case 'Comment': 291 | description.push(node.value.substr(1)); 292 | break; 293 | case 'ObjectTypeDefinition': 294 | objectTypes[node.name.value] = getObjectTypeDefinition(node); 295 | break; 296 | case 'InterfaceTypeDefinition': 297 | interfaceTypes[node.name.value] = getInterfaceTypeDefinition(node); 298 | break; 299 | case 'UnionTypeDefinition': 300 | unions.push(node); 301 | break; 302 | case 'ScalarTypeDefinition': 303 | scalarTypes[node.name.value] = getScalarTypeDefinition(node); 304 | break; 305 | case 'EnumTypeDefinition': 306 | enumTypes[node.name.value] = getEnumTypeDefinition(node); 307 | break; 308 | default: 309 | throw new Error('Unexpected node type ' + node.kind); 310 | } 311 | }); 312 | // Delay unions until all other types exist, otherwise circular deps will cause problems 313 | unions.forEach(function (node) { 314 | unionTypes[node.name.value] = getUnionTypeDefinition(node); 315 | }); 316 | return { 317 | objectTypes, 318 | interfaceTypes, 319 | unionTypes, 320 | scalarTypes, 321 | enumTypes 322 | }; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | var graphql = require('graphql'); 3 | 4 | var parse = require('./parser'); 5 | var createTypeGenerator = require('./create-type-generator'); 6 | var typeGenerator = createTypeGenerator(graphql); 7 | 8 | function createQueryFn(source, implementation) { 9 | let {objectTypes} = typeGenerator(parse(source), implementation); 10 | var schema = new graphql.GraphQLSchema({ 11 | query: objectTypes['Query'] 12 | }); 13 | return (query, context) => graphql.graphql(schema, query, context); 14 | } 15 | createQueryFn.parse = parse; 16 | createQueryFn.createTypeGenerator = createTypeGenerator; 17 | createQueryFn.typeGenerator = typeGenerator; 18 | module.exports = createQueryFn; 19 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | var reserved = [ 2 | 'type', 3 | 'interface', 4 | 'union', 5 | 'scalar', 6 | 'enum', 7 | 'input', 8 | 'extend', 9 | 'null' 10 | ]; 11 | function isReserved(name) { 12 | return reserved.indexOf(name) !== -1; 13 | } 14 | 15 | export default function parse(str, name) { 16 | var Source = {body: str, name: name || 'GraphQL'}; 17 | var originalString = str; 18 | function withPosition(fn) { 19 | return function () { 20 | ignored(); 21 | var start = originalString.length - str.length; 22 | var res = fn.apply(this, arguments); 23 | var end = originalString.length - str.length; 24 | end = start + originalString.replace(/\,/g, ' ').substring(start, end).trim().length; 25 | ignored(); 26 | if (res && !res.loc) { 27 | res.loc = {start, end, source: Source}; 28 | } 29 | return res; 30 | } 31 | } 32 | function list(nodeType) { 33 | var result = []; 34 | while (true) { 35 | var node = comment() || nodeType(); 36 | if (node) result.push(node); 37 | else break; 38 | } 39 | return result; 40 | } 41 | 42 | function match(pattern) { 43 | if (typeof pattern === 'string') { 44 | if (str.substr(0, pattern.length) === pattern) { 45 | str = str.substr(pattern.length); 46 | return pattern; 47 | } 48 | } else { 49 | var match = pattern.exec(str); 50 | if (match) { 51 | str = str.substr(match[0].length); 52 | return match[0]; 53 | } 54 | } 55 | } 56 | function required(value, name) { 57 | if (value) { 58 | return value; 59 | } else { 60 | throw new Error('Expected ' + name + ' but got "' + str[0] + '"'); 61 | } 62 | } 63 | function expect(str) { 64 | required(match(str), '"' + str + '"'); 65 | } 66 | 67 | function ignored() { 68 | // newline 69 | if (str[0] === '\n') { 70 | str = str.substr(1); 71 | return ignored(); 72 | } 73 | // comma 74 | if (str[0] === ',') { 75 | str = str.substr(1); 76 | return ignored(); 77 | } 78 | // white space (except newline) 79 | var m = /^[ \f\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/.exec(str); 80 | if (m) { 81 | str = str.substr(m[0].length); 82 | return ignored(); 83 | } 84 | } 85 | 86 | var name = withPosition(() => { 87 | var state = {str}; 88 | var name = match(/^[_A-Za-z][_0-9A-Za-z]*/); 89 | if (isReserved(name)) { 90 | str = state.str; 91 | return; 92 | } 93 | if (name) { 94 | return { 95 | kind: 'Name', 96 | value: name 97 | }; 98 | } 99 | }); 100 | var namedType = withPosition(() => { 101 | var n = name(); 102 | if (n) { 103 | return { 104 | kind: 'NamedType', 105 | name: n, 106 | loc: n.loc 107 | }; 108 | } 109 | }); 110 | var listType = withPosition(() => { 111 | if (match('[')) { 112 | var node = { 113 | kind: 'ListType', 114 | type: required(type(), 'Type') 115 | }; 116 | expect(']'); 117 | return node; 118 | } 119 | }); 120 | var type = withPosition(() => { 121 | var t = namedType() || listType(); 122 | if (match('!')) { 123 | return {kind: 'NonNullType', type: t}; 124 | } 125 | return t; 126 | }); 127 | 128 | var variable = withPosition(() => { 129 | if (match('$')) { 130 | return { 131 | kind: 'Varialbe', 132 | name: require(name(), 'Name') 133 | }; 134 | } 135 | }); 136 | var numberValue = withPosition(() => { 137 | var str = match(/^(\-?0|\-?[1-9][0-9]*)(\.[0-9]+)?([Ee](\+|\-)?[0-9]+)?/); 138 | if (str) { 139 | return { 140 | kind: 'NumberValue', 141 | value: JSON.parse(str) 142 | }; 143 | } 144 | }); 145 | var stringValue = withPosition(() => { 146 | var str = match(/^\"([^\"\\\n]|\\\\|\\\")*\"/); 147 | if (str) { 148 | return { 149 | kind: 'StringValue', 150 | value: JSON.parse(str) 151 | }; 152 | } 153 | }); 154 | var booleanValue = withPosition(() => { 155 | var TRUE = match('true'); 156 | var FALSE = match('false'); 157 | if (TRUE || FALSE) { 158 | return { 159 | kind: 'BooleanValue', 160 | value: TRUE ? true : false 161 | }; 162 | } 163 | }); 164 | var enumValue = withPosition(() => { 165 | var n = name(); 166 | if (n) { 167 | return { 168 | kind: 'EnumValue', 169 | name: n 170 | }; 171 | } 172 | }); 173 | var listValue = withPosition((isConst) => { 174 | if (match('[')) { 175 | var node = {kind: 'ListValue'}; 176 | node.values = list(value.bind(null, isConst)); 177 | expect(']'); 178 | return node; 179 | } 180 | }); 181 | var objectField = withPosition((isConst) => { 182 | var n = name(); 183 | if (n) { 184 | expect(':'); 185 | return { 186 | kind: 'ObjectField', 187 | name: n, 188 | value: require(value(isConst), 'Value') 189 | }; 190 | } 191 | }); 192 | var objectValue = withPosition((isConst) => { 193 | if (match('{')) { 194 | var node = {kind: 'ObjectValue'}; 195 | node.fields = list(objectField.bind(null, isConst)); 196 | expect('}'); 197 | return node; 198 | } 199 | }); 200 | var value = withPosition((isConst) => { 201 | return ( 202 | (!isConst && variable()) || 203 | numberValue() || 204 | stringValue() || 205 | booleanValue() || 206 | enumValue() || 207 | listValue(isConst) || 208 | objectValue(isConst) 209 | ); 210 | }); 211 | var defaultValue = withPosition(() => { 212 | if (match('=')) { 213 | return required(value(true), 'Value'); 214 | } 215 | }); 216 | 217 | var document = withPosition(() => { 218 | var definitions = list(definition); 219 | if (str.length) { 220 | throw new Error('Unexpected character "' + str[0] + '", expected comment or definition'); 221 | } 222 | return {kind: 'Document', definitions}; 223 | }); 224 | 225 | var comment = withPosition(() => { 226 | var value = match(/^\#[^\n]*/); 227 | if (value) return {kind: 'Comment', value: value}; 228 | }); 229 | 230 | var definition = withPosition(() => { 231 | // N.B. we don't support operations or fragments 232 | return typeDefinition(); 233 | }); 234 | 235 | var typeDefinition = withPosition(() => { 236 | return ( 237 | objectTypeDefinition() || 238 | interfaceTypeDefinition() || 239 | unionTypeDefinition() || 240 | scalarTypeDefinition() || 241 | enumTypeDefinition() || 242 | inputObjectTypeDefinition() || 243 | typeExtensionDefinition() 244 | ); 245 | }); 246 | var objectTypeDefinition = withPosition(() => { 247 | if (match('type')) { 248 | var node = {kind: 'ObjectTypeDefinition'}; 249 | node.name = required(name(), 'name'); 250 | node.interfaces = implementsInterfaces() || []; 251 | expect('{'); 252 | node.fields = list(fieldDefinition); 253 | expect('}'); 254 | return node; 255 | } 256 | }); 257 | var implementsInterfaces = withPosition(() => { 258 | if (match('implements')) { 259 | return list(namedType); 260 | } 261 | }); 262 | var fieldDefinition = withPosition(() => { 263 | var node = {kind: 'FieldDefinition'}; 264 | node.name = name(); 265 | if (!node.name) return; 266 | node.arguments = argumentsDefinition(); 267 | expect(':'); 268 | node.type = required(type(), 'type'); 269 | return node; 270 | }); 271 | var argumentsDefinition = () => { 272 | if (!match('(')) return null; 273 | var args = list(inputValueDefinition); 274 | expect(')'); 275 | return args || []; 276 | }; 277 | var inputValueDefinition = withPosition(() => { 278 | var node = {kind: 'InputValueDefinition'}; 279 | node.name = name(); 280 | if (!node.name) return; 281 | expect(':'); 282 | node.type = required(type(), 'type'); 283 | node.defaultValue = defaultValue() || null; 284 | return node; 285 | }); 286 | var interfaceTypeDefinition = withPosition(() => { 287 | if (match('interface')) { 288 | var node = {kind: 'InterfaceTypeDefinition'}; 289 | node.name = required(name(), 'Name'); 290 | expect('{'); 291 | node.fields = list(fieldDefinition); 292 | expect('}'); 293 | return node; 294 | } 295 | }); 296 | var unionTypeDefinition = withPosition(() => { 297 | if (match('union')) { 298 | var node = {kind: 'UnionTypeDefinition'}; 299 | node.name = required(name(), 'Name'); 300 | expect('='); 301 | var types = []; 302 | types.push(required(namedType(), 'NamedType')); 303 | while (match('|')) { 304 | types.push(required(namedType(), 'NamedType')); 305 | } 306 | node.types = types; 307 | return node; 308 | } 309 | }); 310 | var scalarTypeDefinition = withPosition(() => { 311 | if (match('scalar')) { 312 | var node = {kind: 'ScalarTypeDefinition'}; 313 | node.name = required(name(), 'Name'); 314 | return node; 315 | } 316 | }); 317 | var enumTypeDefinition = withPosition(() => { 318 | if (match('enum')) { 319 | var node = {kind: 'EnumTypeDefinition'}; 320 | node.name = required(name(), 'Name'); 321 | expect('{'); 322 | node.values = list(enumValueDefinition); 323 | expect('}'); 324 | return node; 325 | } 326 | }); 327 | var enumValueDefinition = withPosition(() => { 328 | var n = name(); 329 | if (n) { 330 | return {kind: 'EnumValueDefinition', name: n}; 331 | } 332 | }); 333 | var inputObjectTypeDefinition = withPosition(() => { 334 | if (match('input')) { 335 | var node = {kind: 'InputObjectTypeDefinition'}; 336 | node.name = required(name(), 'Name'); 337 | expect('{'); 338 | node.fields = list(inputValueDefinition); 339 | expect('}'); 340 | return node; 341 | } 342 | }); 343 | var typeExtensionDefinition = withPosition(() => { 344 | if (match('extend')) { 345 | var node = {kind: 'TypeExtensionDefinition'}; 346 | node.definition = required(objectTypeDefinition(), 'ObjectTypeDefinition'); 347 | return node; 348 | } 349 | }); 350 | return document(); 351 | } 352 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import './parser.js'; 2 | -------------------------------------------------------------------------------- /tests/parser.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import test from 'testit'; 3 | import {diffLines} from 'diff'; 4 | import chalk from 'chalk'; 5 | import parse from '../src/parser.js'; 6 | 7 | function expect(input) { 8 | return { 9 | to: { 10 | equal(expected) { 11 | if (input !== expected) { 12 | var diff = diffLines(input, expected); 13 | var err = ''; 14 | diff.forEach(function (chunk) { 15 | err += chunk.added ? chalk.red(chunk.value) : chunk.removed ? chalk.green(chunk.value) : chunk.value; 16 | }); 17 | throw err; 18 | } 19 | }, 20 | throw() { 21 | try { 22 | input(); 23 | } catch (ex) { 24 | return; 25 | } 26 | throw new Error('Expected an error'); 27 | } 28 | } 29 | }; 30 | } 31 | 32 | function createLocFn(body) { 33 | return (start, end) => ({ 34 | start, 35 | end, 36 | source: { 37 | body, 38 | name: 'GraphQL', 39 | }, 40 | }); 41 | } 42 | 43 | function printJson(obj) { 44 | return JSON.stringify(obj, null, 2); 45 | } 46 | 47 | function typeNode(name, loc) { 48 | return { 49 | kind: 'NamedType', 50 | name: nameNode(name, loc), 51 | loc, 52 | }; 53 | } 54 | 55 | function nameNode(name, loc) { 56 | return { 57 | kind: 'Name', 58 | value: name, 59 | loc, 60 | }; 61 | } 62 | 63 | function fieldNode(name, type, loc) { 64 | return { 65 | kind: 'FieldDefinition', 66 | name, 67 | arguments: null, 68 | type, 69 | loc, 70 | }; 71 | } 72 | 73 | function fieldNodeWithArgs(name, type, args, loc) { 74 | return { 75 | kind: 'FieldDefinition', 76 | name, 77 | arguments: args, 78 | type, 79 | loc, 80 | }; 81 | } 82 | 83 | function enumValueNode(name, loc) { 84 | return { 85 | kind: 'EnumValueDefinition', 86 | name: nameNode(name, loc), 87 | loc, 88 | }; 89 | } 90 | 91 | function inputValueNode(name, type, defaultValue, loc) { 92 | return { 93 | kind: 'InputValueDefinition', 94 | name, 95 | type, 96 | defaultValue, 97 | loc, 98 | }; 99 | } 100 | 101 | 102 | test('parser', () => { 103 | test('Simple type', () => { 104 | var body = ` 105 | type Hello { 106 | world: String 107 | }`; 108 | var doc = parse(body); 109 | var loc = createLocFn(body); 110 | var expected = { 111 | kind: 'Document', 112 | definitions: [ 113 | { 114 | kind: 'ObjectTypeDefinition', 115 | name: nameNode('Hello', loc(6, 11)), 116 | interfaces: [], 117 | fields: [ 118 | fieldNode( 119 | nameNode('world', loc(16, 21)), 120 | typeNode('String', loc(23, 29)), 121 | loc(16, 29) 122 | ) 123 | ], 124 | loc: loc(1, 31), 125 | } 126 | ], 127 | loc: loc(1, 31), 128 | }; 129 | expect(printJson(doc)).to.equal(printJson(expected)); 130 | }); 131 | 132 | test('Simple extension', () => { 133 | var body = ` 134 | extend type Hello { 135 | world: String 136 | }`; 137 | var doc = parse(body); 138 | var loc = createLocFn(body); 139 | var expected = { 140 | kind: 'Document', 141 | definitions: [ 142 | { 143 | kind: 'TypeExtensionDefinition', 144 | definition: { 145 | kind: 'ObjectTypeDefinition', 146 | name: nameNode('Hello', loc(13, 18)), 147 | interfaces: [], 148 | fields: [ 149 | fieldNode( 150 | nameNode('world', loc(23, 28)), 151 | typeNode('String', loc(30, 36)), 152 | loc(23, 36) 153 | ) 154 | ], 155 | loc: loc(8, 38), 156 | }, 157 | loc: loc(1, 38), 158 | } 159 | ], 160 | loc: loc(1, 38) 161 | }; 162 | expect(printJson(doc)).to.equal(printJson(expected)); 163 | }); 164 | 165 | test('Simple non-null type', () => { 166 | var body = ` 167 | type Hello { 168 | world: String! 169 | }`; 170 | var loc = createLocFn(body); 171 | var doc = parse(body); 172 | var expected = { 173 | kind: 'Document', 174 | definitions: [ 175 | { 176 | kind: 'ObjectTypeDefinition', 177 | name: nameNode('Hello', loc(6, 11)), 178 | interfaces: [], 179 | fields: [ 180 | fieldNode( 181 | nameNode('world', loc(16, 21)), 182 | { 183 | kind: 'NonNullType', 184 | type: typeNode('String', loc(23, 29)), 185 | loc: loc(23, 30), 186 | }, 187 | loc(16, 30) 188 | ) 189 | ], 190 | loc: loc(1, 32), 191 | } 192 | ], 193 | loc: loc(1, 32), 194 | }; 195 | expect(printJson(doc)).to.equal(printJson(expected)); 196 | }); 197 | 198 | 199 | test('Simple type inheriting interface', () => { 200 | var body = `type Hello implements World { }`; 201 | var loc = createLocFn(body); 202 | var doc = parse(body); 203 | var expected = { 204 | kind: 'Document', 205 | definitions: [ 206 | { 207 | kind: 'ObjectTypeDefinition', 208 | name: nameNode('Hello', loc(5, 10)), 209 | interfaces: [ typeNode('World', loc(22, 27)) ], 210 | fields: [], 211 | loc: loc(0, 31), 212 | } 213 | ], 214 | loc: loc(0, 31), 215 | }; 216 | expect(printJson(doc)).to.equal(printJson(expected)); 217 | }); 218 | 219 | test('Simple type inheriting multiple interfaces', () => { 220 | var body = `type Hello implements Wo, rld { }`; 221 | var loc = createLocFn(body); 222 | var doc = parse(body); 223 | var expected = { 224 | kind: 'Document', 225 | definitions: [ 226 | { 227 | kind: 'ObjectTypeDefinition', 228 | name: nameNode('Hello', loc(5, 10)), 229 | interfaces: [ 230 | typeNode('Wo', loc(22, 24)), 231 | typeNode('rld', loc(26, 29)) 232 | ], 233 | fields: [], 234 | loc: loc(0, 33), 235 | } 236 | ], 237 | loc: loc(0, 33), 238 | }; 239 | expect(printJson(doc)).to.equal(printJson(expected)); 240 | }); 241 | 242 | test('Single value enum', () => { 243 | var body = `enum Hello { WORLD }`; 244 | var loc = createLocFn(body); 245 | var doc = parse(body); 246 | var expected = { 247 | kind: 'Document', 248 | definitions: [ 249 | { 250 | kind: 'EnumTypeDefinition', 251 | name: nameNode('Hello', loc(5, 10)), 252 | values: [ enumValueNode('WORLD', loc(13, 18)) ], 253 | loc: loc(0, 20), 254 | } 255 | ], 256 | loc: loc(0, 20), 257 | }; 258 | expect(printJson(doc)).to.equal(printJson(expected)); 259 | }); 260 | 261 | test('Double value enum', () => { 262 | var body = `enum Hello { WO, RLD }`; 263 | var loc = createLocFn(body); 264 | var doc = parse(body); 265 | var expected = { 266 | kind: 'Document', 267 | definitions: [ 268 | { 269 | kind: 'EnumTypeDefinition', 270 | name: nameNode('Hello', loc(5, 10)), 271 | values: [ 272 | enumValueNode('WO', loc(13, 15)), 273 | enumValueNode('RLD', loc(17, 20)), 274 | ], 275 | loc: loc(0, 22), 276 | } 277 | ], 278 | loc: loc(0, 22), 279 | }; 280 | expect(printJson(doc)).to.equal(printJson(expected)); 281 | }); 282 | 283 | test('Simple interface', () => { 284 | var body = ` 285 | interface Hello { 286 | world: String 287 | }`; 288 | var doc = parse(body); 289 | var loc = createLocFn(body); 290 | var expected = { 291 | kind: 'Document', 292 | definitions: [ 293 | { 294 | kind: 'InterfaceTypeDefinition', 295 | name: nameNode('Hello', loc(11, 16)), 296 | fields: [ 297 | fieldNode( 298 | nameNode('world', loc(21, 26)), 299 | typeNode('String', loc(28, 34)), 300 | loc(21, 34) 301 | ) 302 | ], 303 | loc: loc(1, 36), 304 | } 305 | ], 306 | loc: loc(1, 36), 307 | }; 308 | expect(printJson(doc)).to.equal(printJson(expected)); 309 | }); 310 | 311 | test('Simple field with arg', () => { 312 | var body = ` 313 | type Hello { 314 | world(flag: Boolean): String 315 | }`; 316 | var doc = parse(body); 317 | var loc = createLocFn(body); 318 | var expected = { 319 | kind: 'Document', 320 | definitions: [ 321 | { 322 | kind: 'ObjectTypeDefinition', 323 | name: nameNode('Hello', loc(6, 11)), 324 | interfaces: [], 325 | fields: [ 326 | fieldNodeWithArgs( 327 | nameNode('world', loc(16, 21)), 328 | typeNode('String', loc(38, 44)), 329 | [ 330 | inputValueNode( 331 | nameNode('flag', loc(22, 26)), 332 | typeNode('Boolean', loc(28, 35)), 333 | null, 334 | loc(22, 35) 335 | ) 336 | ], 337 | loc(16, 44) 338 | ) 339 | ], 340 | loc: loc(1, 46), 341 | } 342 | ], 343 | loc: loc(1, 46), 344 | }; 345 | expect(printJson(doc)).to.equal(printJson(expected)); 346 | }); 347 | 348 | test('Simple field with arg with default value', () => { 349 | var body = ` 350 | type Hello { 351 | world(flag: Boolean = true): String 352 | }`; 353 | var doc = parse(body); 354 | var loc = createLocFn(body); 355 | var expected = { 356 | kind: 'Document', 357 | definitions: [ 358 | { 359 | kind: 'ObjectTypeDefinition', 360 | name: nameNode('Hello', loc(6, 11)), 361 | interfaces: [], 362 | fields: [ 363 | fieldNodeWithArgs( 364 | nameNode('world', loc(16, 21)), 365 | typeNode('String', loc(45, 51)), 366 | [ 367 | inputValueNode( 368 | nameNode('flag', loc(22, 26)), 369 | typeNode('Boolean', loc(28, 35)), 370 | { 371 | kind: 'BooleanValue', 372 | value: true, 373 | loc: loc(38, 42), 374 | }, 375 | loc(22, 42) 376 | ) 377 | ], 378 | loc(16, 51) 379 | ) 380 | ], 381 | loc: loc(1, 53), 382 | } 383 | ], 384 | loc: loc(1, 53), 385 | }; 386 | expect(printJson(doc)).to.equal(printJson(expected)); 387 | }); 388 | 389 | test('Simple field with list arg', () => { 390 | var body = ` 391 | type Hello { 392 | world(things: [String]): String 393 | }`; 394 | var doc = parse(body); 395 | var loc = createLocFn(body); 396 | var expected = { 397 | kind: 'Document', 398 | definitions: [ 399 | { 400 | kind: 'ObjectTypeDefinition', 401 | name: nameNode('Hello', loc(6, 11)), 402 | interfaces: [], 403 | fields: [ 404 | fieldNodeWithArgs( 405 | nameNode('world', loc(16, 21)), 406 | typeNode('String', loc(41, 47)), 407 | [ 408 | inputValueNode( 409 | nameNode('things', loc(22, 28)), 410 | { 411 | kind: 'ListType', 412 | type: typeNode('String', loc(31, 37)), 413 | loc: loc(30, 38) 414 | }, 415 | null, 416 | loc(22, 38) 417 | ) 418 | ], 419 | loc(16, 47) 420 | ) 421 | ], 422 | loc: loc(1, 49), 423 | } 424 | ], 425 | loc: loc(1, 49), 426 | }; 427 | expect(printJson(doc)).to.equal(printJson(expected)); 428 | }); 429 | 430 | test('Simple field with two args', () => { 431 | var body = ` 432 | type Hello { 433 | world(argOne: Boolean, argTwo: Int): String 434 | }`; 435 | var doc = parse(body); 436 | var loc = createLocFn(body); 437 | var expected = { 438 | kind: 'Document', 439 | definitions: [ 440 | { 441 | kind: 'ObjectTypeDefinition', 442 | name: nameNode('Hello', loc(6, 11)), 443 | interfaces: [], 444 | fields: [ 445 | fieldNodeWithArgs( 446 | nameNode('world', loc(16, 21)), 447 | typeNode('String', loc(53, 59)), 448 | [ 449 | inputValueNode( 450 | nameNode('argOne', loc(22, 28)), 451 | typeNode('Boolean', loc(30, 37)), 452 | null, 453 | loc(22, 37) 454 | ), 455 | inputValueNode( 456 | nameNode('argTwo', loc(39, 45)), 457 | typeNode('Int', loc(47, 50)), 458 | null, 459 | loc(39, 50) 460 | ), 461 | ], 462 | loc(16, 59) 463 | ) 464 | ], 465 | loc: loc(1, 61), 466 | } 467 | ], 468 | loc: loc(1, 61), 469 | }; 470 | expect(printJson(doc)).to.equal(printJson(expected)); 471 | }); 472 | 473 | test('Simple union', () => { 474 | var body = `union Hello = World`; 475 | var doc = parse(body); 476 | var loc = createLocFn(body); 477 | var expected = { 478 | kind: 'Document', 479 | definitions: [ 480 | { 481 | kind: 'UnionTypeDefinition', 482 | name: nameNode('Hello', loc(6, 11)), 483 | types: [ typeNode('World', loc(14, 19)) ], 484 | loc: loc(0, 19), 485 | } 486 | ], 487 | loc: loc(0, 19), 488 | }; 489 | expect(printJson(doc)).to.equal(printJson(expected)); 490 | }); 491 | 492 | test('Union with two types', () => { 493 | var body = `union Hello = Wo | Rld`; 494 | var doc = parse(body); 495 | var loc = createLocFn(body); 496 | var expected = { 497 | kind: 'Document', 498 | definitions: [ 499 | { 500 | kind: 'UnionTypeDefinition', 501 | name: nameNode('Hello', loc(6, 11)), 502 | types: [ 503 | typeNode('Wo', loc(14, 16)), 504 | typeNode('Rld', loc(19, 22)), 505 | ], 506 | loc: loc(0, 22), 507 | } 508 | ], 509 | loc: loc(0, 22), 510 | }; 511 | expect(printJson(doc)).to.equal(printJson(expected)); 512 | }); 513 | 514 | test('Scalar', () => { 515 | var body = `scalar Hello`; 516 | var doc = parse(body); 517 | var loc = createLocFn(body); 518 | var expected = { 519 | kind: 'Document', 520 | definitions: [ 521 | { 522 | kind: 'ScalarTypeDefinition', 523 | name: nameNode('Hello', loc(7, 12)), 524 | loc: loc(0, 12), 525 | } 526 | ], 527 | loc: loc(0, 12), 528 | }; 529 | expect(printJson(doc)).to.equal(printJson(expected)); 530 | }); 531 | 532 | test('Simple input object', () => { 533 | var body = ` 534 | input Hello { 535 | world: String 536 | }`; 537 | var doc = parse(body); 538 | var loc = createLocFn(body); 539 | var expected = { 540 | kind: 'Document', 541 | definitions: [ 542 | { 543 | kind: 'InputObjectTypeDefinition', 544 | name: nameNode('Hello', loc(7, 12)), 545 | fields: [ 546 | inputValueNode( 547 | nameNode('world', loc(17, 22)), 548 | typeNode('String', loc(24, 30)), 549 | null, 550 | loc(17, 30) 551 | ) 552 | ], 553 | loc: loc(1, 32), 554 | } 555 | ], 556 | loc: loc(1, 32), 557 | }; 558 | expect(printJson(doc)).to.equal(printJson(expected)); 559 | }); 560 | 561 | test('Simple input object with args should fail', () => { 562 | var body = ` 563 | input Hello { 564 | world(foo: Int): String 565 | }`; 566 | expect(() => parse(body)).to.throw('Error'); 567 | }); 568 | 569 | }); 570 | --------------------------------------------------------------------------------