├── .babelrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── bin └── parse ├── package.json ├── rollup.config.js ├── src └── graphql-shorthand.pegjs └── test ├── comment.js ├── enum.js ├── input.js ├── interface.js └── type.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015-rollup", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cameron Hunter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Shorthand Parser 2 | 3 | [![Build Status](https://travis-ci.org/cameronhunter/graphql-shorthand-parser.svg?branch=master)](https://travis-ci.org/cameronhunter/graphql-shorthand-parser) [![NPM Version](https://img.shields.io/npm/v/graphql-shorthand-parser.svg)](https://npmjs.org/package/graphql-shorthand-parser) [![License](https://img.shields.io/npm/l/graphql-shorthand-parser.svg)](https://github.com/cameronhunter/graphql-shorthand-parser/blob/master/LICENSE.md) 4 | 5 | Parse GraphQL shorthand notation into a JSON object that can be used to 6 | auto-generate schema files. 7 | 8 | ### Shorthand notation 9 | ``` 10 | // One of the films in the Star Wars Trilogy 11 | enum Episode { 12 | NEWHOPE 13 | EMPIRE 14 | JEDI 15 | } 16 | 17 | // A character in the Star Wars Trilogy 18 | interface Character { 19 | id: String! 20 | name: String 21 | friends: [Character] 22 | appearsIn: [Episode] 23 | } 24 | 25 | // A humanoid creature in the Star Wars universe 26 | type Human : Character { 27 | id: String! 28 | name: String 29 | friends: [Character] 30 | appearsIn: [Episode] 31 | homePlanet: String 32 | } 33 | 34 | // A mechanical creature in the Star Wars universe 35 | type Droid : Character { 36 | id: String! 37 | name: String 38 | friends: [Character] 39 | appearsIn: [Episode] 40 | primaryFunction: String 41 | } 42 | 43 | type Query { 44 | hero(episode: Episode): Character 45 | human(id: String!): Human 46 | droid(id: String!): Droid 47 | } 48 | ``` 49 | 50 | ### Parsed notation to JSON 51 | ```json 52 | [ 53 | { 54 | "type": "ENUM", 55 | "name": "Episode", 56 | "description": "One of the films in the Star Wars Trilogy", 57 | "values": [ 58 | "NEWHOPE", 59 | "EMPIRE", 60 | "JEDI" 61 | ] 62 | }, 63 | { 64 | "type": "INTERFACE", 65 | "name": "Character", 66 | "description": "A character in the Star Wars Trilogy", 67 | "fields": { 68 | "id": { 69 | "type": "String", 70 | "required": true 71 | }, 72 | "name": { 73 | "type": "String" 74 | }, 75 | "friends": { 76 | "type": "Character", 77 | "list": true 78 | }, 79 | "appearsIn": { 80 | "type": "Episode", 81 | "list": true 82 | } 83 | } 84 | }, 85 | { 86 | "type": "TYPE", 87 | "name": "Human", 88 | "description": "A humanoid creature in the Star Wars universe", 89 | "fields": { 90 | "id": { 91 | "type": "String", 92 | "required": true 93 | }, 94 | "name": { 95 | "type": "String" 96 | }, 97 | "friends": { 98 | "type": "Character", 99 | "list": true 100 | }, 101 | "appearsIn": { 102 | "type": "Episode", 103 | "list": true 104 | }, 105 | "homePlanet": { 106 | "type": "String" 107 | } 108 | }, 109 | "interfaces": [ 110 | "Character" 111 | ] 112 | }, 113 | { 114 | "type": "TYPE", 115 | "name": "Droid", 116 | "description": "A mechanical creature in the Star Wars universe", 117 | "fields": { 118 | "id": { 119 | "type": "String", 120 | "required": true 121 | }, 122 | "name": { 123 | "type": "String" 124 | }, 125 | "friends": { 126 | "type": "Character", 127 | "list": true 128 | }, 129 | "appearsIn": { 130 | "type": "Episode", 131 | "list": true 132 | }, 133 | "primaryFunction": { 134 | "type": "String" 135 | } 136 | }, 137 | "interfaces": [ 138 | "Character" 139 | ] 140 | }, 141 | { 142 | "type": "TYPE", 143 | "name": "Query", 144 | "fields": { 145 | "hero": { 146 | "type": "Character", 147 | "args": { 148 | "episode": { 149 | "type": "Episode" 150 | } 151 | } 152 | }, 153 | "human": { 154 | "type": "Human", 155 | "args": { 156 | "id": { 157 | "type": "String", 158 | "required": true 159 | } 160 | } 161 | }, 162 | "droid": { 163 | "type": "Droid", 164 | "args": { 165 | "id": { 166 | "type": "String", 167 | "required": true 168 | } 169 | } 170 | } 171 | } 172 | } 173 | ] 174 | ``` -------------------------------------------------------------------------------- /bin/parse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require("path"); 4 | var fs = require("fs"); 5 | var parser = require(".."); 6 | 7 | fs.readFile(process.argv[2], "utf8", function(error, shorthand) { 8 | console.log(JSON.stringify(parser.parse(shorthand), null, 2)); 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-shorthand-parser", 3 | "version": "0.0.6", 4 | "description": "Parse GraphQL schemas from shorthand notation", 5 | "main": "dist/graphql-shorthand.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "pretest": "npm run build", 9 | "test": "ava --tap | tap-diff", 10 | "postversion": "git push && git push --tags", 11 | "prepublish": "npm run test", 12 | "preparse": "npm run build", 13 | "parse": "./bin/parse" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "keywords": [ 19 | "graphql" 20 | ], 21 | "author": "Cameron Hunter ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "ava": "^0.16.0", 25 | "babel-preset-es2015-rollup": "^1.2.0", 26 | "babel-preset-stage-1": "^6.16.0", 27 | "rollup": "^0.36.3", 28 | "rollup-plugin-babel": "^2.6.1", 29 | "rollup-plugin-pegjs": "^2.1.1", 30 | "tap-diff": "^0.1.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import pegjs from "rollup-plugin-pegjs"; 3 | 4 | export default { 5 | entry: "src/graphql-shorthand.pegjs", 6 | dest: "dist/graphql-shorthand.js", 7 | format: "cjs", 8 | plugins: [ 9 | pegjs(), 10 | babel() 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/graphql-shorthand.pegjs: -------------------------------------------------------------------------------- 1 | start 2 | = WS* definitions:(Enum / Interface / Object / InputObject)* WS* 3 | { return definitions; } 4 | 5 | Ident = $([a-z]([a-z0-9_]i)*) 6 | TypeIdent = $([A-Z]([a-z0-9_]i)*) 7 | EnumIdent = $([A-Z][A-Z0-9_]*) 8 | NumberIdent = $([.+-]?[0-9]+([.][0-9]+)?) 9 | 10 | Enum 11 | = description:Comment? "enum" SPACE name:TypeIdent BEGIN_BODY values:EnumIdentList CLOSE_BODY 12 | { return { type: "ENUM", name, ...(description && { description }), values }; } 13 | 14 | Interface 15 | = description:Comment? "interface" SPACE name:TypeIdent BEGIN_BODY fields:FieldList CLOSE_BODY 16 | { return { type: "INTERFACE", name, ...(description && { description }), fields }; } 17 | 18 | Object 19 | = description:Comment? "type" SPACE name:TypeIdent interfaces:(COLON list:TypeList { return list; })? BEGIN_BODY fields:FieldList CLOSE_BODY 20 | { return { type: "TYPE", name, ...(description && { description }), fields, ...(interfaces && { interfaces }) }; } 21 | 22 | InputObject 23 | = description:Comment? "input" SPACE name:TypeIdent interfaces:(COLON list:TypeList { return list; })? BEGIN_BODY fields:InputFieldList CLOSE_BODY 24 | { return { type: "INPUT", name, ...(description && { description }), fields, ...(interfaces && { interfaces }) }; } 25 | 26 | ReturnType 27 | = type:TypeIdent required:"!"? 28 | { return { type, ...(required && { required: !!required }) }; } 29 | / "[" type:TypeIdent "]" 30 | { return { type, list: true }; } 31 | 32 | TypeList 33 | = head:TypeIdent tail:(COMMA_SEP type:TypeIdent { return type; })* 34 | { return [head, ...tail]; } 35 | 36 | Field 37 | = description:Comment? name:Ident args:(BEGIN_ARGS fields:FieldList CLOSE_ARGS { return fields; })? COLON type:ReturnType 38 | { return { [name]: { ...type, ...(args && { args }), ...(description && { description }) } }; } 39 | 40 | FieldList 41 | = head:Field tail:(EOL_SEP field:Field { return field; })* 42 | { return [head, ...tail].reduce((result, field) => ({ ...result, ...field }), {}); } 43 | 44 | InputField 45 | = description:Comment? name:Ident args:(BEGIN_ARGS fields:FieldList CLOSE_ARGS { return fields; })? COLON type:ReturnType defaultValue:(EQUAL value:Literal { return value; })? 46 | { return { [name]: { ...type, ...(args && { args }), ...(description && { description }), ...(defaultValue && { defaultValue }) } }; } 47 | 48 | InputFieldList 49 | = head:InputField tail:(EOL_SEP field:InputField { return field; })* 50 | { return [head, ...tail].reduce((result, field) => ({ ...result, ...field }), {}); } 51 | 52 | EnumIdentList 53 | = head:EnumIdent tail:(EOL_SEP value:EnumIdent { return value; })* 54 | { return [head, ...tail]; } 55 | 56 | Comment 57 | = LINE_COMMENT comment:(!EOL char:CHAR { return char; })* EOL_SEP 58 | { return comment.join("").trim(); } 59 | / "/*" comment:(!"*/" char:CHAR { return char; })* "*/" EOL_SEP 60 | { return comment.join("").replace(/\n\s*[*]?\s*/g, " ").replace(/\s+/, " ").trim(); } 61 | 62 | Literal 63 | = StringLiteral / BooleanLiteral / NumericLiteral 64 | 65 | StringLiteral 66 | = '"' chars:DoubleStringCharacter* '"' { return chars.join(""); } 67 | 68 | DoubleStringCharacter 69 | = !('"' / "\\" / EOL) . { return text(); } 70 | 71 | BooleanLiteral 72 | = "true" { return true } 73 | / "false" { return false } 74 | 75 | NumericLiteral 76 | = value:NumberIdent { return Number(value.replace(/^[.]/, '0.')); } 77 | 78 | LINE_COMMENT = "#" / "//" 79 | 80 | BEGIN_BODY = WS* "{" WS* 81 | CLOSE_BODY = WS* "}" WS* 82 | 83 | BEGIN_ARGS = WS* "(" WS* 84 | CLOSE_ARGS = WS* ")" WS* 85 | 86 | CHAR = . 87 | 88 | WS = (SPACE / EOL)+ 89 | 90 | COLON = WS* ":" WS* 91 | EQUAL = WS* "=" WS* 92 | 93 | COMMA_SEP = WS* "," WS* 94 | EOL_SEP = SPACE* EOL SPACE* 95 | 96 | SPACE = [ \t]+ 97 | EOL = "\n" / "\r\n" / "\r" / "\u2028" / "\u2029" 98 | -------------------------------------------------------------------------------- /test/comment.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { parse } from ".."; 3 | 4 | test("add '#' comments as description", t => { 5 | const [actual] = parse(` 6 | # FOO as in Foobar 7 | enum Bar { FOO } 8 | `); 9 | 10 | const expected = { 11 | type: "ENUM", 12 | name: "Bar", 13 | description: "FOO as in Foobar", 14 | values: ["FOO"] 15 | }; 16 | 17 | return t.deepEqual(actual, expected); 18 | }); 19 | 20 | test("add '//' comments as description", t => { 21 | const [actual] = parse(` 22 | // FOO as in Foobar 23 | enum Bar { FOO } 24 | `); 25 | 26 | const expected = { 27 | type: "ENUM", 28 | name: "Bar", 29 | description: "FOO as in Foobar", 30 | values: ["FOO"] 31 | }; 32 | 33 | return t.deepEqual(actual, expected); 34 | }); 35 | 36 | test("add '/**/' comments as description", t => { 37 | const [actual] = parse(` 38 | /* 39 | FOO as in Foobar 40 | */ 41 | enum Bar { FOO } 42 | `); 43 | 44 | const expected = { 45 | type: "ENUM", 46 | name: "Bar", 47 | description: "FOO as in Foobar", 48 | values: ["FOO"] 49 | }; 50 | 51 | return t.deepEqual(actual, expected); 52 | }); 53 | 54 | test("add comments as field description", t => { 55 | const [actual] = parse(` 56 | // A humanoid creature in the Star Wars universe 57 | type Human : Character { 58 | # the id 59 | id: String! 60 | // the name 61 | name: String 62 | friends: [Character] 63 | appearsIn: [Episode] 64 | /* the home planet */ 65 | homePlanet: String 66 | } 67 | `); 68 | 69 | const expected = { 70 | type: "TYPE", 71 | name: "Human", 72 | description: "A humanoid creature in the Star Wars universe", 73 | interfaces: ["Character"], 74 | fields: { 75 | id: { type: "String", required: true, description: "the id" }, 76 | name: { type: "String", description: "the name" }, 77 | friends: { type: "Character", list: true }, 78 | appearsIn: { type: "Episode", list: true }, 79 | homePlanet: { type: "String", description: "the home planet" } 80 | } 81 | }; 82 | 83 | return t.deepEqual(actual, expected); 84 | }); 85 | -------------------------------------------------------------------------------- /test/enum.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { parse } from ".."; 3 | 4 | test("enum definition", t => { 5 | const [actual] = parse(` 6 | // One of the films in the Star Wars Trilogy 7 | enum Episode { 8 | NEWHOPE 9 | EMPIRE 10 | JEDI 11 | } 12 | `); 13 | 14 | const expected = { 15 | type: "ENUM", 16 | name: "Episode", 17 | description: "One of the films in the Star Wars Trilogy", 18 | values: ["NEWHOPE", "EMPIRE", "JEDI"] 19 | }; 20 | 21 | return t.deepEqual(actual, expected); 22 | }); 23 | -------------------------------------------------------------------------------- /test/input.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { parse } from ".."; 3 | 4 | test("input definition", t => { 5 | const [actual] = parse(` 6 | // A Person 7 | input Person { 8 | id: String! 9 | // The full name 10 | name: String 11 | } 12 | `); 13 | 14 | const expected = { 15 | type: "INPUT", 16 | name: "Person", 17 | description: "A Person", 18 | fields: { 19 | id: { type: "String", required: true }, 20 | name: { type: "String", description: "The full name" } 21 | } 22 | }; 23 | 24 | return t.deepEqual(actual, expected); 25 | }); 26 | 27 | test("input with boolean defaultValue", t => { 28 | const [actual] = parse(` 29 | input Person { 30 | alive: Boolean = true 31 | } 32 | `); 33 | 34 | const expected = { 35 | type: "INPUT", 36 | name: "Person", 37 | fields: { 38 | alive: { type: "Boolean", defaultValue: true }, 39 | } 40 | }; 41 | 42 | return t.deepEqual(actual, expected); 43 | }); 44 | 45 | 46 | test("input with string default values", t => { 47 | const [actual] = parse(` 48 | // A Person 49 | input Person { 50 | id: String! 51 | firstname: String = "Hans" 52 | lastname: String = "Wurst" 53 | } 54 | `); 55 | 56 | const expected = { 57 | type: "INPUT", 58 | name: "Person", 59 | description: "A Person", 60 | fields: { 61 | id: { type: "String", required: true }, 62 | firstname: { type: "String", defaultValue: "Hans" }, 63 | lastname: { type: "String", defaultValue: "Wurst" } 64 | } 65 | }; 66 | 67 | return t.deepEqual(actual, expected); 68 | }); 69 | 70 | test("input with integer defaultValue", t => { 71 | const [actual] = parse(` 72 | input Person { 73 | age: Int = 32 74 | } 75 | `); 76 | 77 | const expected = { 78 | type: "INPUT", 79 | name: "Person", 80 | fields: { 81 | age: { type: "Int", defaultValue: 32 }, 82 | } 83 | }; 84 | 85 | return t.deepEqual(actual, expected); 86 | }); 87 | 88 | test("input with float defaultValue", t => { 89 | const [actual] = parse(` 90 | input Person { 91 | height: Float = 1.82 92 | iq: Float = .5 93 | } 94 | `); 95 | 96 | const expected = { 97 | type: "INPUT", 98 | name: "Person", 99 | fields: { 100 | height: { type: "Float", defaultValue: 1.82 }, 101 | iq: { type: "Float", defaultValue: 0.5 }, 102 | } 103 | }; 104 | 105 | return t.deepEqual(actual, expected); 106 | }); 107 | -------------------------------------------------------------------------------- /test/interface.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { parse } from ".."; 3 | 4 | test("interface definition", t => { 5 | const [actual] = parse(` 6 | // A character in the Star Wars Trilogy 7 | interface Character { 8 | id: String! 9 | name: String 10 | friends: [Character] 11 | appearsIn: [Episode] 12 | } 13 | `); 14 | 15 | const expected = { 16 | type: "INTERFACE", 17 | name: "Character", 18 | description: "A character in the Star Wars Trilogy", 19 | fields: { 20 | id: { type: "String", required: true }, 21 | name: { type: "String" }, 22 | friends: { type: "Character", list: true }, 23 | appearsIn: { type: "Episode", list: true } 24 | } 25 | }; 26 | 27 | return t.deepEqual(actual, expected); 28 | }); 29 | -------------------------------------------------------------------------------- /test/type.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { parse } from ".."; 3 | 4 | test("type definition", t => { 5 | const [actual] = parse(` 6 | // A humanoid creature in the Star Wars universe 7 | type Human : Character { 8 | id: String! 9 | name: String 10 | friends: [Character] 11 | appearsIn: [Episode] 12 | homePlanet: String 13 | } 14 | `); 15 | 16 | const expected = { 17 | type: "TYPE", 18 | name: "Human", 19 | description: "A humanoid creature in the Star Wars universe", 20 | interfaces: ["Character"], 21 | fields: { 22 | id: { type: "String", required: true }, 23 | name: { type: "String" }, 24 | friends: { type: "Character", list: true }, 25 | appearsIn: { type: "Episode", list: true }, 26 | homePlanet: { type: "String" } 27 | } 28 | }; 29 | 30 | return t.deepEqual(actual, expected); 31 | }); 32 | 33 | test("type definition with parameters", t => { 34 | const [actual] = parse(` 35 | type Query { 36 | hero(episode: Episode): Character 37 | human(id: String!): Human 38 | droid(id: String!): Droid 39 | } 40 | `); 41 | 42 | const expected = { 43 | type: "TYPE", 44 | name: "Query", 45 | fields: { 46 | hero: { 47 | type: "Character", 48 | args: { episode: { type: "Episode" } } 49 | }, 50 | human: { 51 | type: "Human", 52 | args: { id: { type: "String", required: true } } 53 | }, 54 | droid: { 55 | type: "Droid", 56 | args: { id: { type: "String", required: true } } 57 | } 58 | } 59 | }; 60 | 61 | return t.deepEqual(actual, expected); 62 | }); 63 | 64 | 65 | test("type definition with multiple interfaces", t => { 66 | const [actual] = parse(` 67 | type Human : Character, AnotherThing { 68 | id: String! 69 | name: String 70 | friends: [Character] 71 | appearsIn: [Episode] 72 | homePlanet: String 73 | } 74 | `); 75 | 76 | const expected = { 77 | type: "TYPE", 78 | name: "Human", 79 | interfaces: ["Character", "AnotherThing"], 80 | fields: { 81 | id: { type: "String", required: true }, 82 | name: { type: "String" }, 83 | friends: { type: "Character", list: true }, 84 | appearsIn: { type: "Episode", list: true }, 85 | homePlanet: { type: "String" } 86 | } 87 | }; 88 | 89 | return t.deepEqual(actual, expected); 90 | }); 91 | --------------------------------------------------------------------------------