├── LICENSE ├── README.md ├── graphql-0.0.1-1.rockspec ├── graphql-0.0.2-1.rockspec ├── graphql.lua ├── graphql ├── execute.lua ├── init.lua ├── introspection.lua ├── package.lua ├── parse.lua ├── rules.lua ├── schema.lua ├── types.lua ├── util.lua └── validate.lua └── tests ├── data └── schema.lua ├── lust.lua ├── parse.lua ├── rules.lua └── runner.lua /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Bjorn Swenson 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 all 11 | 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 THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GraphQL Lua [![Join the chat at https://gitter.im/bjornbytes/graphql-lua](https://badges.gitter.im/bjornbytes/graphql-lua.svg)](https://gitter.im/bjornbytes/graphql-lua?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | === 3 | 4 | Lua implementation of GraphQL. 5 | 6 | Installation 7 | ------------ 8 | 9 | 1. Using luvit 10 | 11 | lit install bjornbytes/graphql 12 | 13 | 2. Using luarocks 14 | 15 | luarocks install graphql 16 | 17 | Example 18 | --- 19 | 20 | ```lua 21 | local parse = require 'graphql.parse' 22 | local schema = require 'graphql.schema' 23 | local types = require 'graphql.types' 24 | local validate = require 'graphql.validate' 25 | local execute = require 'graphql.execute' 26 | 27 | -- Parse a query 28 | local ast = parse [[ 29 | query getUser($id: ID) { 30 | person(id: $id) { 31 | firstName 32 | lastName 33 | } 34 | } 35 | ]] 36 | 37 | -- Create a type 38 | local Person = types.object { 39 | name = 'Person', 40 | fields = { 41 | id = types.id.nonNull, 42 | firstName = types.string.nonNull, 43 | middleName = types.string, 44 | lastName = types.string.nonNull, 45 | age = types.int.nonNull 46 | } 47 | } 48 | 49 | -- Create a schema 50 | local schema = schema.create { 51 | query = types.object { 52 | name = 'Query', 53 | fields = { 54 | person = { 55 | kind = Person, 56 | arguments = { 57 | id = types.id 58 | }, 59 | resolve = function(rootValue, arguments) 60 | if arguments.id ~= 1 then return nil end 61 | 62 | return { 63 | id = 1, 64 | firstName = 'Bob', 65 | lastName = 'Ross', 66 | age = 52 67 | } 68 | end 69 | } 70 | } 71 | } 72 | } 73 | 74 | -- Validate a parsed query against a schema 75 | validate(schema, ast) 76 | 77 | -- Execution 78 | local rootValue = {} 79 | local variables = { id = 1 } 80 | local operationName = 'getUser' 81 | 82 | execute(schema, ast, rootValue, variables, operationName) 83 | 84 | --[[ 85 | { 86 | person = { 87 | firstName = 'Bob', 88 | lastName = 'Ross' 89 | } 90 | } 91 | ]] 92 | ``` 93 | 94 | Status 95 | --- 96 | 97 | - [x] Parsing 98 | - [ ] Improve error messages 99 | - [x] Type system 100 | - [x] Introspection 101 | - [x] Validation 102 | - [x] Execution 103 | - [ ] Asynchronous execution (coroutines) 104 | - [ ] Example server 105 | 106 | Running tests 107 | --- 108 | 109 | ```lua 110 | lua tests/runner.lua 111 | ``` 112 | 113 | License 114 | --- 115 | 116 | MIT 117 | -------------------------------------------------------------------------------- /graphql-0.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'graphql' 2 | version = '0.0.1-1' 3 | 4 | source = { 5 | url = 'git://github.com/bjornbytes/graphql-lua.git' 6 | } 7 | 8 | description = { 9 | summary = 'Lua GraphQL implementation', 10 | homepage = 'https://github.com/bjornbytes/graphql-lua', 11 | maintainer = 'https://github.com/bjornbytes', 12 | license = 'MIT' 13 | } 14 | 15 | dependencies = { 16 | 'lua >= 5.1', 17 | 'lpeg' 18 | } 19 | 20 | build = { 21 | type = 'builtin', 22 | modules = { 23 | ['graphql'] = 'graphql/init.lua', 24 | ['graphql.parse'] = 'graphql/parse.lua', 25 | ['graphql.types'] = 'graphql/types.lua', 26 | ['graphql.schema'] = 'graphql/schema.lua', 27 | ['graphql.validate'] = 'graphql/validate.lua', 28 | ['graphql.rules'] = 'graphql/rules.lua', 29 | ['graphql.execute'] = 'graphql/execute.lua', 30 | ['graphql.util'] = 'graphql/util.lua' 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /graphql-0.0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'graphql' 2 | version = '0.0.2-1' 3 | 4 | source = { 5 | url = 'git://github.com/bjornbytes/graphql-lua.git' 6 | } 7 | 8 | description = { 9 | summary = 'Lua GraphQL implementation', 10 | homepage = 'https://github.com/bjornbytes/graphql-lua', 11 | maintainer = 'https://github.com/bjornbytes', 12 | license = 'MIT' 13 | } 14 | 15 | dependencies = { 16 | 'lua >= 5.1', 17 | 'lpeg' 18 | } 19 | 20 | build = { 21 | type = 'builtin', 22 | modules = { 23 | ['graphql'] = 'graphql/init.lua', 24 | ['graphql.parse'] = 'graphql/parse.lua', 25 | ['graphql.types'] = 'graphql/types.lua', 26 | ['graphql.introspection'] = 'graphql/introspection.lua', 27 | ['graphql.schema'] = 'graphql/schema.lua', 28 | ['graphql.validate'] = 'graphql/validate.lua', 29 | ['graphql.rules'] = 'graphql/rules.lua', 30 | ['graphql.execute'] = 'graphql/execute.lua', 31 | ['graphql.util'] = 'graphql/util.lua' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /graphql.lua: -------------------------------------------------------------------------------- 1 | return require(... .. '.init') 2 | -------------------------------------------------------------------------------- /graphql/execute.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.[^%.]+$', '') 2 | local types = require(path .. '.types') 3 | local util = require(path .. '.util') 4 | local introspection = require(path .. '.introspection') 5 | 6 | local function typeFromAST(node, schema) 7 | local innerType 8 | if node.kind == 'listType' then 9 | innerType = typeFromAST(node.type) 10 | return innerType and types.list(innerType) 11 | elseif node.kind == 'nonNullType' then 12 | innerType = typeFromAST(node.type) 13 | return innerType and types.nonNull(innerType) 14 | else 15 | assert(node.kind == 'namedType', 'Variable must be a named type') 16 | return schema:getType(node.name.value) 17 | end 18 | end 19 | 20 | local function getFieldResponseKey(field) 21 | return field.alias and field.alias.name.value or field.name.value 22 | end 23 | 24 | local function shouldIncludeNode(selection, context) 25 | if selection.directives then 26 | local function isDirectiveActive(key, _type) 27 | local directive = util.find(selection.directives, function(directive) 28 | return directive.name.value == key 29 | end) 30 | 31 | if not directive then return end 32 | 33 | local ifArgument = util.find(directive.arguments, function(argument) 34 | return argument.name.value == 'if' 35 | end) 36 | 37 | if not ifArgument then return end 38 | 39 | return util.coerceValue(ifArgument.value, _type.arguments['if'], context.variables) 40 | end 41 | 42 | if isDirectiveActive('skip', types.skip) then return false end 43 | if isDirectiveActive('include', types.include) == false then return false end 44 | end 45 | 46 | return true 47 | end 48 | 49 | local function doesFragmentApply(fragment, type, context) 50 | if not fragment.typeCondition then return true end 51 | 52 | local innerType = typeFromAST(fragment.typeCondition, context.schema) 53 | 54 | if innerType == type then 55 | return true 56 | elseif innerType.__type == 'Interface' then 57 | local implementors = context.schema:getImplementors(innerType.name) 58 | return implementors and implementors[type] 59 | elseif innerType.__type == 'Union' then 60 | return util.find(innerType.types, function(member) 61 | return member == type 62 | end) 63 | end 64 | end 65 | 66 | local function mergeSelectionSets(fields) 67 | local selections = {} 68 | 69 | for i = 1, #fields do 70 | local selectionSet = fields[i].selectionSet 71 | if selectionSet then 72 | for j = 1, #selectionSet.selections do 73 | table.insert(selections, selectionSet.selections[j]) 74 | end 75 | end 76 | end 77 | 78 | return selections 79 | end 80 | 81 | local function defaultResolver(object, arguments, info) 82 | return object[info.fieldASTs[1].name.value] 83 | end 84 | 85 | local function buildContext(schema, tree, rootValue, variables, operationName) 86 | local context = { 87 | schema = schema, 88 | rootValue = rootValue, 89 | variables = variables, 90 | operation = nil, 91 | fragmentMap = {} 92 | } 93 | 94 | for _, definition in ipairs(tree.definitions) do 95 | if definition.kind == 'operation' then 96 | if not operationName and context.operation then 97 | error('Operation name must be specified if more than one operation exists.') 98 | end 99 | 100 | if not operationName or definition.name.value == operationName then 101 | context.operation = definition 102 | end 103 | elseif definition.kind == 'fragmentDefinition' then 104 | context.fragmentMap[definition.name.value] = definition 105 | end 106 | end 107 | 108 | if not context.operation then 109 | if operationName then 110 | error('Unknown operation "' .. operationName .. '"') 111 | else 112 | error('Must provide an operation') 113 | end 114 | end 115 | 116 | return context 117 | end 118 | 119 | local function collectFields(objectType, selections, visitedFragments, result, context) 120 | for _, selection in ipairs(selections) do 121 | if selection.kind == 'field' then 122 | if shouldIncludeNode(selection, context) then 123 | local name = getFieldResponseKey(selection) 124 | result[name] = result[name] or {} 125 | table.insert(result[name], selection) 126 | end 127 | elseif selection.kind == 'inlineFragment' then 128 | if shouldIncludeNode(selection, context) and doesFragmentApply(selection, objectType, context) then 129 | collectFields(objectType, selection.selectionSet.selections, visitedFragments, result, context) 130 | end 131 | elseif selection.kind == 'fragmentSpread' then 132 | local fragmentName = selection.name.value 133 | if shouldIncludeNode(selection, context) and not visitedFragments[fragmentName] then 134 | visitedFragments[fragmentName] = true 135 | local fragment = context.fragmentMap[fragmentName] 136 | if fragment and shouldIncludeNode(fragment, context) and doesFragmentApply(fragment, objectType, context) then 137 | collectFields(objectType, fragment.selectionSet.selections, visitedFragments, result, context) 138 | end 139 | end 140 | end 141 | end 142 | 143 | return result 144 | end 145 | 146 | local evaluateSelections 147 | 148 | local function completeValue(fieldType, result, subSelections, context) 149 | local fieldTypeName = fieldType.__type 150 | 151 | if fieldTypeName == 'NonNull' then 152 | local innerType = fieldType.ofType 153 | local completedResult = completeValue(innerType, result, subSelections, context) 154 | 155 | if completedResult == nil then 156 | error('No value provided for non-null ' .. (innerType.name or innerType.__type)) 157 | end 158 | 159 | return completedResult 160 | end 161 | 162 | if result == nil then 163 | return nil 164 | end 165 | 166 | if fieldTypeName == 'List' then 167 | local innerType = fieldType.ofType 168 | 169 | if type(result) ~= 'table' then 170 | error('Expected a table for ' .. innerType.name .. ' list') 171 | end 172 | 173 | local values = {} 174 | for i, value in ipairs(result) do 175 | values[i] = completeValue(innerType, value, subSelections, context) 176 | end 177 | 178 | return next(values) and values or context.schema.__emptyList 179 | end 180 | 181 | if fieldTypeName == 'Scalar' or fieldTypeName == 'Enum' then 182 | return fieldType.serialize(result) 183 | end 184 | 185 | if fieldTypeName == 'Object' then 186 | local fields = evaluateSelections(fieldType, result, subSelections, context) 187 | return next(fields) and fields or context.schema.__emptyObject 188 | elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then 189 | local objectType = fieldType.resolveType(result) 190 | return evaluateSelections(objectType, result, subSelections, context) 191 | end 192 | 193 | error('Unknown type "' .. fieldTypeName .. '" for field "' .. field.name .. '"') 194 | end 195 | 196 | local function getFieldEntry(objectType, object, fields, context) 197 | local firstField = fields[1] 198 | local fieldName = firstField.name.value 199 | local responseKey = getFieldResponseKey(firstField) 200 | local fieldType = introspection.fieldMap[fieldName] or objectType.fields[fieldName] 201 | 202 | if fieldType == nil then 203 | return nil 204 | end 205 | 206 | local argumentMap = {} 207 | for _, argument in ipairs(firstField.arguments or {}) do 208 | argumentMap[argument.name.value] = argument 209 | end 210 | 211 | local arguments = util.map(fieldType.arguments or {}, function(argument, name) 212 | local supplied = argumentMap[name] and argumentMap[name].value 213 | return supplied and util.coerceValue(supplied, argument, context.variables) or argument.defaultValue 214 | end) 215 | 216 | local info = { 217 | fieldName = fieldName, 218 | fieldASTs = fields, 219 | returnType = fieldType.kind, 220 | parentType = objectType, 221 | schema = context.schema, 222 | fragments = context.fragmentMap, 223 | rootValue = context.rootValue, 224 | operation = context.operation, 225 | variableValues = context.variables 226 | } 227 | 228 | local resolvedObject = (fieldType.resolve or defaultResolver)(object, arguments, info) 229 | local subSelections = mergeSelectionSets(fields) 230 | 231 | return completeValue(fieldType.kind, resolvedObject, subSelections, context) 232 | end 233 | 234 | evaluateSelections = function(objectType, object, selections, context) 235 | local groupedFieldSet = collectFields(objectType, selections, {}, {}, context) 236 | 237 | return util.map(groupedFieldSet, function(fields) 238 | return getFieldEntry(objectType, object, fields, context) 239 | end) 240 | end 241 | 242 | return function(schema, tree, rootValue, variables, operationName) 243 | local context = buildContext(schema, tree, rootValue, variables, operationName) 244 | local rootType = schema[context.operation.operation] 245 | 246 | if not rootType then 247 | error('Unsupported operation "' .. context.operation.operation .. '"') 248 | end 249 | 250 | return evaluateSelections(rootType, rootValue, context.operation.selectionSet.selections, context) 251 | end 252 | -------------------------------------------------------------------------------- /graphql/init.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.init$', '') 2 | 3 | local graphql = {} 4 | 5 | graphql.parse = require(path .. '.parse') 6 | graphql.types = require(path .. '.types') 7 | graphql.schema = require(path .. '.schema') 8 | graphql.validate = require(path .. '.validate') 9 | graphql.execute = require(path .. '.execute') 10 | 11 | return graphql 12 | -------------------------------------------------------------------------------- /graphql/introspection.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.[^%.]+$', '') 2 | local types = require(path .. '.types') 3 | local util = require(path .. '.util') 4 | 5 | local __Schema, __Directive, __DirectiveLocation, __Type, __Field, __InputValue,__EnumValue, __TypeKind 6 | 7 | local function resolveArgs(field) 8 | local function transformArg(arg, name) 9 | if arg.__type then 10 | return { kind = arg, name = name } 11 | elseif arg.name then 12 | return arg 13 | else 14 | local result = { name = name } 15 | 16 | for k, v in pairs(arg) do 17 | result[k] = v 18 | end 19 | 20 | return result 21 | end 22 | end 23 | 24 | return util.values(util.map(field.arguments or {}, transformArg)) 25 | end 26 | 27 | __Schema = types.object({ 28 | name = '__Schema', 29 | 30 | description = util.trim [[ 31 | A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types 32 | and directives on the server, as well as the entry points for query and mutation operations. 33 | ]], 34 | 35 | fields = function() 36 | return { 37 | types = { 38 | description = 'A list of all types supported by this server.', 39 | kind = types.nonNull(types.list(types.nonNull(__Type))), 40 | resolve = function(schema) 41 | return util.values(schema:getTypeMap()) 42 | end 43 | }, 44 | 45 | queryType = { 46 | description = 'The type that query operations will be rooted at.', 47 | kind = __Type.nonNull, 48 | resolve = function(schema) 49 | return schema:getQueryType() 50 | end 51 | }, 52 | 53 | mutationType = { 54 | description = 'If this server supports mutation, the type that mutation operations will be rooted at.', 55 | kind = __Type, 56 | resolve = function(schema) 57 | return schema:getMutationType() 58 | end 59 | }, 60 | 61 | directives = { 62 | description = 'A list of all directives supported by this server.', 63 | kind = types.nonNull(types.list(types.nonNull(__Directive))), 64 | resolve = function(schema) 65 | return schema.directives 66 | end 67 | }, 68 | 69 | subscriptionType = { 70 | description = 'If this server supports subscriptions, the type that subscription operations will be rooted at.', 71 | kind = __Type, 72 | resolve = function(schema) 73 | return schema:getSubscriptionType() 74 | end 75 | } 76 | } 77 | end 78 | }) 79 | 80 | __Directive = types.object({ 81 | name = '__Directive', 82 | 83 | description = util.trim [[ 84 | A Directive provides a way to describe alternate runtime execution and type validation behavior 85 | in a GraphQL document. 86 | 87 | In some cases, you need to provide options to alter GraphQL’s execution 88 | behavior in ways field arguments will not suffice, such as conditionally including or skipping a 89 | field. Directives provide this by describing additional information to the executor. 90 | ]], 91 | 92 | fields = function() 93 | return { 94 | name = types.nonNull(types.string), 95 | 96 | description = types.string, 97 | 98 | locations = { 99 | kind = types.nonNull(types.list(types.nonNull( 100 | __DirectiveLocation 101 | ))), 102 | resolve = function(directive) 103 | local res = {} 104 | 105 | if directive.onQuery then table.insert(res, 'QUERY') end 106 | if directive.onMutation then table.insert(res, 'MUTATION') end 107 | if directive.onField then table.insert(res, 'FIELD') end 108 | if directive.onFragmentDefinition then table.insert(res, 'FRAGMENT_DEFINITION') end 109 | if directive.onFragmentSpread then table.insert(res, 'FRAGMENT_SPREAD') end 110 | if directive.onInlineFragment then table.insert(res, 'INLINE_FRAGMENT') end 111 | 112 | return res 113 | end 114 | }, 115 | 116 | args = { 117 | kind = types.nonNull(types.list(types.nonNull(__InputValue))), 118 | resolve = resolveArgs 119 | } 120 | } 121 | end 122 | }) 123 | 124 | __DirectiveLocation = types.enum({ 125 | name = '__DirectiveLocation', 126 | 127 | description = util.trim [[ 128 | A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation 129 | describes one such possible adjacencies. 130 | ]], 131 | 132 | values = { 133 | QUERY = { 134 | value = 'QUERY', 135 | description = 'Location adjacent to a query operation.' 136 | }, 137 | 138 | MUTATION = { 139 | value = 'MUTATION', 140 | description = 'Location adjacent to a mutation operation.' 141 | }, 142 | 143 | FIELD = { 144 | value = 'FIELD', 145 | description = 'Location adjacent to a field.' 146 | }, 147 | 148 | FRAGMENT_DEFINITION = { 149 | value = 'FRAGMENT_DEFINITION', 150 | description = 'Location adjacent to a fragment definition.' 151 | }, 152 | 153 | FRAGMENT_SPREAD = { 154 | value = 'FRAGMENT_SPREAD', 155 | description = 'Location adjacent to a fragment spread.' 156 | }, 157 | 158 | INLINE_FRAGMENT = { 159 | value = 'INLINE_FRAGMENT', 160 | description = 'Location adjacent to an inline fragment.' 161 | } 162 | } 163 | }) 164 | 165 | __Type = types.object({ 166 | name = '__Type', 167 | 168 | description = util.trim [[ 169 | The fundamental unit of any GraphQL Schema is the type. There are 170 | many kinds of types in GraphQL as represented by the `__TypeKind` enum. 171 | 172 | Depending on the kind of a type, certain fields describe 173 | information about that type. Scalar types provide no information 174 | beyond a name and description, while Enum types provide their values. 175 | Object and Interface types provide the fields they describe. Abstract 176 | types, Union and Interface, provide the Object types possible 177 | at runtime. List and NonNull types compose other types. 178 | ]], 179 | 180 | fields = function() 181 | return { 182 | name = types.string, 183 | description = types.string, 184 | 185 | kind = { 186 | kind = __TypeKind.nonNull, 187 | resolve = function(kind) 188 | if kind.__type == 'Scalar' then 189 | return 'SCALAR' 190 | elseif kind.__type == 'Object' then 191 | return 'OBJECT' 192 | elseif kind.__type == 'Interface' then 193 | return 'INTERFACE' 194 | elseif kind.__type == 'Union' then 195 | return 'UNION' 196 | elseif kind.__type == 'Enum' then 197 | return 'ENUM' 198 | elseif kind.__type == 'InputObject' then 199 | return 'INPUT_OBJECT' 200 | elseif kind.__type == 'List' then 201 | return 'LIST' 202 | elseif kind.__type == 'NonNull' then 203 | return 'NON_NULL' 204 | end 205 | 206 | error('Unknown type ' .. kind) 207 | end 208 | }, 209 | 210 | fields = { 211 | kind = types.list(types.nonNull(__Field)), 212 | arguments = { 213 | includeDeprecated = { 214 | kind = types.boolean, 215 | defaultValue = false 216 | } 217 | }, 218 | resolve = function(kind, arguments) 219 | if kind.__type == 'Object' or kind.__type == 'Interface' then 220 | return util.filter(util.values(kind.fields), function(field) 221 | return arguments.includeDeprecated or field.deprecationReason == nil 222 | end) 223 | end 224 | 225 | return nil 226 | end 227 | }, 228 | 229 | interfaces = { 230 | kind = types.list(types.nonNull(__Type)), 231 | resolve = function(kind) 232 | if kind.__type == 'Object' then 233 | return kind.interfaces 234 | end 235 | end 236 | }, 237 | 238 | possibleTypes = { 239 | kind = types.list(types.nonNull(__Type)), 240 | resolve = function(kind, arguments, context) 241 | if kind.__type == 'Interface' or kind.__type == 'Union' then 242 | return context.schema:getPossibleTypes(kind) 243 | end 244 | end 245 | }, 246 | 247 | enumValues = { 248 | kind = types.list(types.nonNull(__EnumValue)), 249 | arguments = { 250 | includeDeprecated = { kind = types.boolean, defaultValue = false } 251 | }, 252 | resolve = function(kind, arguments) 253 | if kind.__type == 'Enum' then 254 | return util.filter(util.values(kind.values), function(value) 255 | return arguments.includeDeprecated or not value.deprecationReason 256 | end) 257 | end 258 | end 259 | }, 260 | 261 | inputFields = { 262 | kind = types.list(types.nonNull(__InputValue)), 263 | resolve = function(kind) 264 | if kind.__type == 'InputObject' then 265 | return util.values(kind.fields) 266 | end 267 | end 268 | }, 269 | 270 | ofType = { 271 | kind = __Type 272 | } 273 | } 274 | end 275 | }) 276 | 277 | __Field = types.object({ 278 | name = '__Field', 279 | 280 | description = util.trim [[ 281 | Object and Interface types are described by a list of Fields, each of 282 | which has a name, potentially a list of arguments, and a return type. 283 | ]], 284 | 285 | fields = function() 286 | return { 287 | name = types.string.nonNull, 288 | description = types.string, 289 | 290 | args = { 291 | kind = types.nonNull(types.list(types.nonNull(__InputValue))), 292 | resolve = resolveArgs 293 | }, 294 | 295 | type = { 296 | kind = __Type.nonNull, 297 | resolve = function(field) 298 | return field.kind 299 | end 300 | }, 301 | 302 | isDeprecated = { 303 | kind = types.boolean.nonNull, 304 | resolve = function(field) 305 | return field.deprecationReason ~= nil 306 | end 307 | }, 308 | 309 | deprecationReason = types.string 310 | } 311 | end 312 | }) 313 | 314 | __InputValue = types.object({ 315 | name = '__InputValue', 316 | 317 | description = util.trim [[ 318 | Arguments provided to Fields or Directives and the input fields of an 319 | InputObject are represented as Input Values which describe their type 320 | and optionally a default value. 321 | ]], 322 | 323 | fields = function() 324 | return { 325 | name = types.string.nonNull, 326 | description = types.string, 327 | 328 | type = { 329 | kind = types.nonNull(__Type), 330 | resolve = function(field) 331 | return field.kind 332 | end 333 | }, 334 | 335 | defaultValue = { 336 | kind = types.string, 337 | description = 'A GraphQL-formatted string representing the default value for this input value.', 338 | resolve = function(inputVal) 339 | return inputVal.defaultValue and tostring(inputVal.defaultValue) -- TODO improve serialization a lot 340 | end 341 | } 342 | } 343 | end 344 | }) 345 | 346 | __EnumValue = types.object({ 347 | name = '__EnumValue', 348 | 349 | description = util.trim [[ 350 | One possible value for a given Enum. Enum values are unique values, not 351 | a placeholder for a string or numeric value. However an Enum value is 352 | returned in a JSON response as a string. 353 | ]], 354 | 355 | fields = function() 356 | return { 357 | name = types.string.nonNull, 358 | description = types.string, 359 | isDeprecated = { 360 | kind = types.boolean.nonNull, 361 | resolve = function(enumValue) return enumValue.deprecationReason ~= nil end 362 | }, 363 | deprecationReason = types.string 364 | } 365 | end 366 | }) 367 | 368 | __TypeKind = types.enum({ 369 | name = '__TypeKind', 370 | description = 'An enum describing what kind of type a given `__Type` is.', 371 | values = { 372 | SCALAR = { 373 | value = 'SCALAR', 374 | description = 'Indicates this type is a scalar.' 375 | }, 376 | 377 | OBJECT = { 378 | value = 'OBJECT', 379 | description = 'Indicates this type is an object. `fields` and `interfaces` are valid fields.' 380 | }, 381 | 382 | INTERFACE = { 383 | value = 'INTERFACE', 384 | description = 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.' 385 | }, 386 | 387 | UNION = { 388 | value = 'UNION', 389 | description = 'Indicates this type is a union. `possibleTypes` is a valid field.' 390 | }, 391 | 392 | ENUM = { 393 | value = 'ENUM', 394 | description = 'Indicates this type is an enum. `enumValues` is a valid field.' 395 | }, 396 | 397 | INPUT_OBJECT = { 398 | value = 'INPUT_OBJECT', 399 | description = 'Indicates this type is an input object. `inputFields` is a valid field.' 400 | }, 401 | 402 | LIST = { 403 | value = 'LIST', 404 | description = 'Indicates this type is a list. `ofType` is a valid field.' 405 | }, 406 | 407 | NON_NULL = { 408 | value = 'NON_NULL', 409 | description = 'Indicates this type is a non-null. `ofType` is a valid field.' 410 | } 411 | } 412 | }) 413 | 414 | local Schema = { 415 | name = '__schema', 416 | kind = __Schema.nonNull, 417 | description = 'Access the current type schema of this server.', 418 | arguments = {}, 419 | resolve = function(_, _, info) 420 | return info.schema 421 | end 422 | } 423 | 424 | local Type = { 425 | name = '__type', 426 | kind = __Type, 427 | description = 'Request the type information of a single type.', 428 | arguments = { 429 | name = types.string.nonNull 430 | }, 431 | resolve = function(_, arguments, info) 432 | return info.schema:getType(arguments.name) 433 | end 434 | } 435 | 436 | local TypeName = { 437 | name = '__typename', 438 | kind = types.string.nonNull, 439 | description = 'The name of the current Object type at runtime.', 440 | arguments = {}, 441 | resolve = function(_, _, info) 442 | return info.parentType.name 443 | end 444 | } 445 | 446 | return { 447 | __Schema = __Schema, 448 | __Directive = __Directive, 449 | __DirectiveLocation = __DirectiveLocation, 450 | __Type = __Type, 451 | __Field = __Field, 452 | __EnumValue = __EnumValue, 453 | __TypeKind = __TypeKind, 454 | Schema = Schema, 455 | Type = Type, 456 | TypeName = TypeName, 457 | fieldMap = { 458 | __schema = Schema, 459 | __type = Type, 460 | __typename = TypeName 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /graphql/package.lua: -------------------------------------------------------------------------------- 1 | return { 2 | name = 'bjornbytes/graphql', 3 | version = '0.0.2', 4 | description = 'Lua GraphQL implementation', 5 | license = 'MIT', 6 | author = { url = 'https://github.com/bjornbytes' }, 7 | homepage = 'https://github.com/bjornbytes/graphql-lua', 8 | files = { 9 | '*.lua' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /graphql/parse.lua: -------------------------------------------------------------------------------- 1 | local lpeg = require 'lpeg' 2 | local P, R, S, V = lpeg.P, lpeg.R, lpeg.S, lpeg.V 3 | local C, Ct, Cmt, Cg, Cc, Cf, Cmt = lpeg.C, lpeg.Ct, lpeg.Cmt, lpeg.Cg, lpeg.Cc, lpeg.Cf, lpeg.Cmt 4 | 5 | local line 6 | local lastLinePos 7 | 8 | local function pack(...) 9 | return { n = select('#', ...), ... } 10 | end 11 | 12 | -- Utility 13 | local ws = Cmt(S(' \t\r\n') ^ 0, function(str, pos) 14 | str = str:sub(lastLinePos, pos) 15 | while str:find('\n') do 16 | line = line + 1 17 | lastLinePos = pos 18 | str = str:sub(str:find('\n') + 1) 19 | end 20 | 21 | return true 22 | end) 23 | 24 | local comma = P(',') ^ 0 25 | 26 | local _ = V 27 | 28 | local function maybe(pattern) 29 | if type(pattern) == 'string' then pattern = V(pattern) end 30 | return pattern ^ -1 31 | end 32 | 33 | local function list(pattern, min) 34 | if type(pattern) == 'string' then pattern = V(pattern) end 35 | min = min or 0 36 | return Ct((pattern * ws * comma * ws) ^ min) 37 | end 38 | 39 | -- Formatters 40 | local function simpleValue(key) 41 | return function(value) 42 | return { 43 | kind = key, 44 | value = value 45 | } 46 | end 47 | end 48 | 49 | local cName = simpleValue('name') 50 | local cInt = simpleValue('int') 51 | local cFloat = simpleValue('float') 52 | local cBoolean = simpleValue('boolean') 53 | local cEnum = simpleValue('enum') 54 | 55 | local cString = function(value) 56 | return { 57 | kind = 'string', 58 | value = value:gsub('\\"', '"') 59 | } 60 | end 61 | 62 | local function cList(value) 63 | return { 64 | kind = 'list', 65 | values = value 66 | } 67 | end 68 | 69 | local function cObjectField(name, value) 70 | return { 71 | name = name, 72 | value = value 73 | } 74 | end 75 | 76 | local function cObject(fields) 77 | return { 78 | kind = 'inputObject', 79 | values = fields 80 | } 81 | end 82 | 83 | local function cAlias(name) 84 | return { 85 | kind = 'alias', 86 | name = name 87 | } 88 | end 89 | 90 | local function cArgument(name, value) 91 | return { 92 | kind = 'argument', 93 | name = name, 94 | value = value 95 | } 96 | end 97 | 98 | local function cField(...) 99 | local tokens = pack(...) 100 | local field = { kind = 'field' } 101 | 102 | for i = 1, #tokens do 103 | local key = tokens[i].kind 104 | if not key then 105 | if tokens[i][1].kind == 'argument' then 106 | key = 'arguments' 107 | elseif tokens[i][1].kind == 'directive' then 108 | key = 'directives' 109 | end 110 | end 111 | 112 | field[key] = tokens[i] 113 | end 114 | 115 | return field 116 | end 117 | 118 | local function cSelectionSet(selections) 119 | return { 120 | kind = 'selectionSet', 121 | selections = selections 122 | } 123 | end 124 | 125 | local function cFragmentSpread(name, directives) 126 | return { 127 | kind = 'fragmentSpread', 128 | name = name, 129 | directives = directives 130 | } 131 | end 132 | 133 | local function cOperation(...) 134 | local args = pack(...) 135 | if args[1].kind == 'selectionSet' then 136 | return { 137 | kind = 'operation', 138 | operation = 'query', 139 | selectionSet = args[1] 140 | } 141 | else 142 | local result = { 143 | kind = 'operation', 144 | operation = args[1] 145 | } 146 | 147 | for i = 2, #args do 148 | local key = args[i].kind 149 | if not key then 150 | if args[i][1].kind == 'variableDefinition' then 151 | key = 'variableDefinitions' 152 | elseif args[i][1].kind == 'directive' then 153 | key = 'directives' 154 | end 155 | end 156 | 157 | result[key] = args[i] 158 | end 159 | 160 | return result 161 | end 162 | end 163 | 164 | local function cDocument(definitions) 165 | return { 166 | kind = 'document', 167 | definitions = definitions 168 | } 169 | end 170 | 171 | local function cFragmentDefinition(name, typeCondition, selectionSet) 172 | return { 173 | kind = 'fragmentDefinition', 174 | name = name, 175 | typeCondition = typeCondition, 176 | selectionSet = selectionSet 177 | } 178 | end 179 | 180 | local function cNamedType(name) 181 | return { 182 | kind = 'namedType', 183 | name = name 184 | } 185 | end 186 | 187 | local function cListType(type) 188 | return { 189 | kind = 'listType', 190 | type = type 191 | } 192 | end 193 | 194 | local function cNonNullType(type) 195 | return { 196 | kind = 'nonNullType', 197 | type = type 198 | } 199 | end 200 | 201 | local function cInlineFragment(...) 202 | local args = pack(...) 203 | local result = { kind = 'inlineFragment' } 204 | result.selectionSet = args[#args] 205 | for i = 1, #args - 1 do 206 | if args[i].kind == 'namedType' or args[i].kind == 'listType' or args[i].kind == 'nonNullType' then 207 | result.typeCondition = args[i] 208 | elseif args[i][1] and args[i][1].kind == 'directive' then 209 | result.directives = args[i] 210 | end 211 | end 212 | return result 213 | end 214 | 215 | local function cVariable(name) 216 | return { 217 | kind = 'variable', 218 | name = name 219 | } 220 | end 221 | 222 | local function cVariableDefinition(variable, type, defaultValue) 223 | return { 224 | kind = 'variableDefinition', 225 | variable = variable, 226 | type = type, 227 | defaultValue = defaultValue 228 | } 229 | end 230 | 231 | local function cDirective(name, arguments) 232 | return { 233 | kind = 'directive', 234 | name = name, 235 | arguments = arguments 236 | } 237 | end 238 | 239 | -- Simple types 240 | local rawName = (P'_' + R('az', 'AZ')) * (P'_' + R'09' + R('az', 'AZ')) ^ 0 241 | local name = rawName / cName 242 | local fragmentName = (rawName - ('on' * -rawName)) / cName 243 | local alias = ws * name * P':' * ws / cAlias 244 | 245 | local integerPart = P'-' ^ -1 * ('0' + R'19' * R'09' ^ 0) 246 | local intValue = integerPart / cInt 247 | local fractionalPart = '.' * R'09' ^ 1 248 | local exponentialPart = S'Ee' * S'+-' ^ -1 * R'09' ^ 1 249 | local floatValue = integerPart * ((fractionalPart * exponentialPart) + fractionalPart + exponentialPart) / cFloat 250 | 251 | local booleanValue = (P'true' + P'false') / cBoolean 252 | local stringValue = P'"' * C((P'\\"' + 1 - S'"\n') ^ 0) * P'"' / cString 253 | local enumValue = (rawName - 'true' - 'false' - 'null') / cEnum 254 | local variable = ws * '$' * name / cVariable 255 | 256 | -- Grammar 257 | local graphQL = P { 258 | 'document', 259 | document = ws * list('definition') / cDocument * -1, 260 | definition = _'operation' + _'fragmentDefinition', 261 | 262 | operationType = C(P'query' + P'mutation'), 263 | operation = (_'operationType' * ws * maybe(name) * maybe('variableDefinitions') * maybe('directives') * _'selectionSet' + _'selectionSet') / cOperation, 264 | fragmentDefinition = 'fragment' * ws * fragmentName * ws * _'typeCondition' * ws * _'selectionSet' / cFragmentDefinition, 265 | 266 | selectionSet = ws * '{' * ws * list('selection') * ws * '}' / cSelectionSet, 267 | selection = ws * (_'field' + _'fragmentSpread' + _'inlineFragment'), 268 | 269 | field = ws * maybe(alias) * name * maybe('arguments') * maybe('directives') * maybe('selectionSet') / cField, 270 | fragmentSpread = ws * '...' * ws * fragmentName * maybe('directives') / cFragmentSpread, 271 | inlineFragment = ws * '...' * ws * maybe('typeCondition') * maybe('directives') * _'selectionSet' / cInlineFragment, 272 | typeCondition = 'on' * ws * _'namedType', 273 | 274 | argument = ws * name * ':' * _'value' / cArgument, 275 | arguments = '(' * list('argument', 1) * ')', 276 | 277 | directive = '@' * name * maybe('arguments') / cDirective, 278 | directives = ws * list('directive', 1) * ws, 279 | 280 | variableDefinition = ws * variable * ws * ':' * ws * _'type' * (ws * '=' * _'value') ^ -1 * comma * ws / cVariableDefinition, 281 | variableDefinitions = ws * '(' * list('variableDefinition', 1) * ')', 282 | 283 | value = ws * (variable + _'objectValue' + _'listValue' + enumValue + stringValue + booleanValue + floatValue + intValue), 284 | listValue = '[' * list('value') * ']' / cList, 285 | objectFieldValue = ws * C(rawName) * ws * ':' * ws * _'value' * comma / cObjectField, 286 | objectValue = '{' * ws * list('objectFieldValue') * ws * '}' / cObject, 287 | 288 | type = _'nonNullType' + _'listType' + _'namedType', 289 | namedType = name / cNamedType, 290 | listType = '[' * ws * _'type' * ws * ']' / cListType, 291 | nonNullType = (_'namedType' + _'listType') * '!' / cNonNullType 292 | } 293 | 294 | -- TODO doesn't handle quotes that immediately follow escaped backslashes. 295 | local function stripComments(str) 296 | return (str .. '\n'):gsub('(.-\n)', function(line) 297 | local index = 1 298 | while line:find('#', index) do 299 | local pos = line:find('#', index) - 1 300 | local chunk = line:sub(1, pos) 301 | local _, quotes = chunk:gsub('([^\\]")', '') 302 | if quotes % 2 == 0 then 303 | return chunk .. '\n' 304 | else 305 | index = pos + 2 306 | end 307 | end 308 | 309 | return line 310 | end):sub(1, -2) 311 | end 312 | 313 | return function(str) 314 | assert(type(str) == 'string', 'parser expects a string') 315 | str = stripComments(str) 316 | line, lastLinePos = 1, 1 317 | return graphQL:match(str) or error('Syntax error near line ' .. line, 2) 318 | end 319 | -------------------------------------------------------------------------------- /graphql/rules.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.[^%.]+$', '') 2 | local types = require(path .. '.types') 3 | local util = require(path .. '.util') 4 | local schema = require(path .. '.schema') 5 | local introspection = require(path .. '.introspection') 6 | 7 | local function getParentField(context, name, count) 8 | if introspection.fieldMap[name] then return introspection.fieldMap[name] end 9 | 10 | count = count or 1 11 | local parent = context.objects[#context.objects - count] 12 | 13 | -- Unwrap lists and non-null types 14 | while parent.ofType do 15 | parent = parent.ofType 16 | end 17 | 18 | return parent.fields[name] 19 | end 20 | 21 | local rules = {} 22 | 23 | function rules.uniqueOperationNames(node, context) 24 | local name = node.name and node.name.value 25 | 26 | if name then 27 | if context.operationNames[name] then 28 | error('Multiple operations exist named "' .. name .. '"') 29 | end 30 | 31 | context.operationNames[name] = true 32 | end 33 | end 34 | 35 | function rules.loneAnonymousOperation(node, context) 36 | local name = node.name and node.name.value 37 | 38 | if context.hasAnonymousOperation or (not name and next(context.operationNames)) then 39 | error('Cannot have more than one operation when using anonymous operations') 40 | end 41 | 42 | if not name then 43 | context.hasAnonymousOperation = true 44 | end 45 | end 46 | 47 | function rules.fieldsDefinedOnType(node, context) 48 | if context.objects[#context.objects] == false then 49 | local parent = context.objects[#context.objects - 1] 50 | while parent.ofType do parent = parent.ofType end 51 | error('Field "' .. node.name.value .. '" is not defined on type "' .. parent.name .. '"') 52 | end 53 | end 54 | 55 | function rules.argumentsDefinedOnType(node, context) 56 | if node.arguments then 57 | local parentField = getParentField(context, node.name.value) 58 | for _, argument in pairs(node.arguments) do 59 | local name = argument.name.value 60 | if not parentField.arguments[name] then 61 | error('Non-existent argument "' .. name .. '"') 62 | end 63 | end 64 | end 65 | end 66 | 67 | function rules.scalarFieldsAreLeaves(node, context) 68 | if context.objects[#context.objects].__type == 'Scalar' and node.selectionSet then 69 | error('Scalar values cannot have subselections') 70 | end 71 | end 72 | 73 | function rules.compositeFieldsAreNotLeaves(node, context) 74 | local _type = context.objects[#context.objects].__type 75 | local isCompositeType = _type == 'Object' or _type == 'Interface' or _type == 'Union' 76 | 77 | if isCompositeType and not node.selectionSet then 78 | error('Composite types must have subselections') 79 | end 80 | end 81 | 82 | function rules.unambiguousSelections(node, context) 83 | local selectionMap = {} 84 | local seen = {} 85 | 86 | local function findConflict(entryA, entryB) 87 | 88 | -- Parent types can't overlap if they're different objects. 89 | -- Interface and union types may overlap. 90 | if entryA.parent ~= entryB.parent and entryA.__type == 'Object' and entryB.__type == 'Object' then 91 | return 92 | end 93 | 94 | -- Error if there are aliases that map two different fields to the same name. 95 | if entryA.field.name.value ~= entryB.field.name.value then 96 | return 'Type name mismatch' 97 | end 98 | 99 | -- Error if there are fields with the same name that have different return types. 100 | if entryA.definition and entryB.definition and entryA.definition ~= entryB.definition then 101 | return 'Return type mismatch' 102 | end 103 | 104 | -- Error if arguments are not identical for two fields with the same name. 105 | local argsA = entryA.field.arguments or {} 106 | local argsB = entryB.field.arguments or {} 107 | 108 | if #argsA ~= #argsB then 109 | return 'Argument mismatch' 110 | end 111 | 112 | local argMap = {} 113 | 114 | for i = 1, #argsA do 115 | argMap[argsA[i].name.value] = argsA[i].value 116 | end 117 | 118 | for i = 1, #argsB do 119 | local name = argsB[i].name.value 120 | if not argMap[name] then 121 | return 'Argument mismatch' 122 | elseif argMap[name].kind ~= argsB[i].value.kind then 123 | return 'Argument mismatch' 124 | elseif argMap[name].value ~= argsB[i].value.value then 125 | return 'Argument mismatch' 126 | end 127 | end 128 | end 129 | 130 | local function validateField(key, entry) 131 | if selectionMap[key] then 132 | for i = 1, #selectionMap[key] do 133 | local conflict = findConflict(selectionMap[key][i], entry) 134 | if conflict then 135 | error(conflict) 136 | end 137 | end 138 | 139 | table.insert(selectionMap[key], entry) 140 | else 141 | selectionMap[key] = { entry } 142 | end 143 | end 144 | 145 | -- Recursively make sure that there are no ambiguous selections with the same name. 146 | local function validateSelectionSet(selectionSet, parentType) 147 | for _, selection in ipairs(selectionSet.selections) do 148 | if selection.kind == 'field' then 149 | if not parentType or not parentType.fields or not parentType.fields[selection.name.value] then return end 150 | 151 | local key = selection.alias and selection.alias.name.value or selection.name.value 152 | local definition = parentType.fields[selection.name.value].kind 153 | 154 | local fieldEntry = { 155 | parent = parentType, 156 | field = selection, 157 | definition = definition 158 | } 159 | 160 | validateField(key, fieldEntry) 161 | elseif selection.kind == 'inlineFragment' then 162 | local parentType = selection.typeCondition and context.schema:getType(selection.typeCondition.name.value) or parentType 163 | validateSelectionSet(selection.selectionSet, parentType) 164 | elseif selection.kind == 'fragmentSpread' then 165 | local fragmentDefinition = context.fragmentMap[selection.name.value] 166 | if fragmentDefinition and not seen[fragmentDefinition] then 167 | seen[fragmentDefinition] = true 168 | if fragmentDefinition and fragmentDefinition.typeCondition then 169 | local parentType = context.schema:getType(fragmentDefinition.typeCondition.name.value) 170 | validateSelectionSet(fragmentDefinition.selectionSet, parentType) 171 | end 172 | end 173 | end 174 | end 175 | end 176 | 177 | validateSelectionSet(node, context.objects[#context.objects]) 178 | end 179 | 180 | function rules.uniqueArgumentNames(node, context) 181 | if node.arguments then 182 | local arguments = {} 183 | for _, argument in ipairs(node.arguments) do 184 | local name = argument.name.value 185 | if arguments[name] then 186 | error('Encountered multiple arguments named "' .. name .. '"') 187 | end 188 | arguments[name] = true 189 | end 190 | end 191 | end 192 | 193 | function rules.argumentsOfCorrectType(node, context) 194 | if node.arguments then 195 | local parentField = getParentField(context, node.name.value) 196 | for _, argument in pairs(node.arguments) do 197 | local name = argument.name.value 198 | local argumentType = parentField.arguments[name] 199 | util.coerceValue(argument.value, argumentType.kind or argumentType) 200 | end 201 | end 202 | end 203 | 204 | function rules.requiredArgumentsPresent(node, context) 205 | local arguments = node.arguments or {} 206 | local parentField = getParentField(context, node.name.value) 207 | for name, argument in pairs(parentField.arguments) do 208 | if argument.__type == 'NonNull' then 209 | local present = util.find(arguments, function(argument) 210 | return argument.name.value == name 211 | end) 212 | 213 | if not present then 214 | error('Required argument "' .. name .. '" was not supplied.') 215 | end 216 | end 217 | end 218 | end 219 | 220 | function rules.uniqueFragmentNames(node, context) 221 | local fragments = {} 222 | for _, definition in ipairs(node.definitions) do 223 | if definition.kind == 'fragmentDefinition' then 224 | local name = definition.name.value 225 | if fragments[name] then 226 | error('Encountered multiple fragments named "' .. name .. '"') 227 | end 228 | fragments[name] = true 229 | end 230 | end 231 | end 232 | 233 | function rules.fragmentHasValidType(node, context) 234 | if not node.typeCondition then return end 235 | 236 | local name = node.typeCondition.name.value 237 | local kind = context.schema:getType(name) 238 | 239 | if not kind then 240 | error('Fragment refers to non-existent type "' .. name .. '"') 241 | end 242 | 243 | if kind.__type ~= 'Object' and kind.__type ~= 'Interface' and kind.__type ~= 'Union' then 244 | error('Fragment type must be an Object, Interface, or Union, got ' .. kind.__type) 245 | end 246 | end 247 | 248 | function rules.noUnusedFragments(node, context) 249 | for _, definition in ipairs(node.definitions) do 250 | if definition.kind == 'fragmentDefinition' then 251 | local name = definition.name.value 252 | if not context.usedFragments[name] then 253 | error('Fragment "' .. name .. '" was not used.') 254 | end 255 | end 256 | end 257 | end 258 | 259 | function rules.fragmentSpreadTargetDefined(node, context) 260 | if not context.fragmentMap[node.name.value] then 261 | error('Fragment spread refers to non-existent fragment "' .. node.name.value .. '"') 262 | end 263 | end 264 | 265 | function rules.fragmentDefinitionHasNoCycles(node, context) 266 | local seen = { [node.name.value] = true } 267 | 268 | local function detectCycles(selectionSet) 269 | for _, selection in ipairs(selectionSet.selections) do 270 | if selection.kind == 'inlineFragment' then 271 | detectCycles(selection.selectionSet) 272 | elseif selection.kind == 'fragmentSpread' then 273 | if seen[selection.name.value] then 274 | error('Fragment definition has cycles') 275 | end 276 | 277 | seen[selection.name.value] = true 278 | 279 | local fragmentDefinition = context.fragmentMap[selection.name.value] 280 | if fragmentDefinition and fragmentDefinition.typeCondition then 281 | detectCycles(fragmentDefinition.selectionSet) 282 | end 283 | end 284 | end 285 | end 286 | 287 | detectCycles(node.selectionSet) 288 | end 289 | 290 | function rules.fragmentSpreadIsPossible(node, context) 291 | local fragment = node.kind == 'inlineFragment' and node or context.fragmentMap[node.name.value] 292 | 293 | local parentType = context.objects[#context.objects - 1] 294 | while parentType.ofType do parentType = parentType.ofType end 295 | 296 | local fragmentType 297 | if node.kind == 'inlineFragment' then 298 | fragmentType = node.typeCondition and context.schema:getType(node.typeCondition.name.value) or parentType 299 | else 300 | fragmentType = context.schema:getType(fragment.typeCondition.name.value) 301 | end 302 | 303 | -- Some types are not present in the schema. Let other rules handle this. 304 | if not parentType or not fragmentType then return end 305 | 306 | local function getTypes(kind) 307 | if kind.__type == 'Object' then 308 | return { [kind] = kind } 309 | elseif kind.__type == 'Interface' then 310 | return context.schema:getImplementors(kind.name) 311 | elseif kind.__type == 'Union' then 312 | local types = {} 313 | for i = 1, #kind.types do 314 | types[kind.types[i]] = kind.types[i] 315 | end 316 | return types 317 | else 318 | return {} 319 | end 320 | end 321 | 322 | local parentTypes = getTypes(parentType) 323 | local fragmentTypes = getTypes(fragmentType) 324 | 325 | local valid = util.find(parentTypes, function(kind) 326 | return fragmentTypes[kind] 327 | end) 328 | 329 | if not valid then 330 | error('Fragment type condition is not possible for given type') 331 | end 332 | end 333 | 334 | function rules.uniqueInputObjectFields(node, context) 335 | local function validateValue(value) 336 | if value.kind == 'listType' or value.kind == 'nonNullType' then 337 | return validateValue(value.type) 338 | elseif value.kind == 'inputObject' then 339 | local fieldMap = {} 340 | for _, field in ipairs(value.values) do 341 | if fieldMap[field.name] then 342 | error('Multiple input object fields named "' .. field.name .. '"') 343 | end 344 | 345 | fieldMap[field.name] = true 346 | 347 | validateValue(field.value) 348 | end 349 | end 350 | end 351 | 352 | validateValue(node.value) 353 | end 354 | 355 | function rules.directivesAreDefined(node, context) 356 | if not node.directives then return end 357 | 358 | for _, directive in pairs(node.directives) do 359 | if not context.schema:getDirective(directive.name.value) then 360 | error('Unknown directive "' .. directive.name.value .. '"') 361 | end 362 | end 363 | end 364 | 365 | function rules.variablesHaveCorrectType(node, context) 366 | local function validateType(type) 367 | if type.kind == 'listType' or type.kind == 'nonNullType' then 368 | validateType(type.type) 369 | elseif type.kind == 'namedType' then 370 | local schemaType = context.schema:getType(type.name.value) 371 | if not schemaType then 372 | error('Variable specifies unknown type "' .. tostring(type.name.value) .. '"') 373 | elseif schemaType.__type ~= 'Scalar' and schemaType.__type ~= 'Enum' and schemaType.__type ~= 'InputObject' then 374 | error('Variable types must be scalars, enums, or input objects, got "' .. schemaType.__type .. '"') 375 | end 376 | end 377 | end 378 | 379 | if node.variableDefinitions then 380 | for _, definition in ipairs(node.variableDefinitions) do 381 | validateType(definition.type) 382 | end 383 | end 384 | end 385 | 386 | function rules.variableDefaultValuesHaveCorrectType(node, context) 387 | if node.variableDefinitions then 388 | for _, definition in ipairs(node.variableDefinitions) do 389 | if definition.type.kind == 'nonNullType' and definition.defaultValue then 390 | error('Non-null variables can not have default values') 391 | elseif definition.defaultValue then 392 | util.coerceValue(definition.defaultValue, context.schema:getType(definition.type.name.value)) 393 | end 394 | end 395 | end 396 | end 397 | 398 | function rules.variablesAreUsed(node, context) 399 | if node.variableDefinitions then 400 | for _, definition in ipairs(node.variableDefinitions) do 401 | local variableName = definition.variable.name.value 402 | if not context.variableReferences[variableName] then 403 | error('Unused variable "' .. variableName .. '"') 404 | end 405 | end 406 | end 407 | end 408 | 409 | function rules.variablesAreDefined(node, context) 410 | if context.variableReferences then 411 | local variableMap = {} 412 | for _, definition in ipairs(node.variableDefinitions or {}) do 413 | variableMap[definition.variable.name.value] = true 414 | end 415 | 416 | for variable in pairs(context.variableReferences) do 417 | if not variableMap[variable] then 418 | error('Unknown variable "' .. variable .. '"') 419 | end 420 | end 421 | end 422 | end 423 | 424 | function rules.variableUsageAllowed(node, context) 425 | if context.currentOperation then 426 | local variableMap = {} 427 | for _, definition in ipairs(context.currentOperation.variableDefinitions or {}) do 428 | variableMap[definition.variable.name.value] = definition 429 | end 430 | 431 | local arguments 432 | 433 | if node.kind == 'field' then 434 | arguments = { [node.name.value] = node.arguments } 435 | elseif node.kind == 'fragmentSpread' then 436 | local seen = {} 437 | local function collectArguments(referencedNode) 438 | if referencedNode.kind == 'selectionSet' then 439 | for _, selection in ipairs(referencedNode.selections) do 440 | if not seen[selection] then 441 | seen[selection] = true 442 | collectArguments(selection) 443 | end 444 | end 445 | elseif referencedNode.kind == 'field' and referencedNode.arguments then 446 | local fieldName = referencedNode.name.value 447 | arguments[fieldName] = arguments[fieldName] or {} 448 | for _, argument in ipairs(referencedNode.arguments) do 449 | table.insert(arguments[fieldName], argument) 450 | end 451 | elseif referencedNode.kind == 'inlineFragment' then 452 | return collectArguments(referencedNode.selectionSet) 453 | elseif referencedNode.kind == 'fragmentSpread' then 454 | local fragment = context.fragmentMap[referencedNode.name.value] 455 | return fragment and collectArguments(fragment.selectionSet) 456 | end 457 | end 458 | 459 | local fragment = context.fragmentMap[node.name.value] 460 | if fragment then 461 | arguments = {} 462 | collectArguments(fragment.selectionSet) 463 | end 464 | end 465 | 466 | if not arguments then return end 467 | 468 | for field in pairs(arguments) do 469 | local parentField = getParentField(context, field) 470 | for i = 1, #arguments[field] do 471 | local argument = arguments[field][i] 472 | if argument.value.kind == 'variable' then 473 | local argumentType = parentField.arguments[argument.name.value] 474 | 475 | local variableName = argument.value.name.value 476 | local variableDefinition = variableMap[variableName] 477 | local hasDefault = variableDefinition.defaultValue ~= nil 478 | 479 | local function typeFromAST(variable) 480 | local innerType 481 | if variable.kind == 'listType' then 482 | innerType = typeFromAST(variable.type) 483 | return innerType and types.list(innerType) 484 | elseif variable.kind == 'nonNullType' then 485 | innerType = typeFromAST(variable.type) 486 | return innerType and types.nonNull(innerType) 487 | else 488 | assert(variable.kind == 'namedType', 'Variable must be a named type') 489 | return context.schema:getType(variable.name.value) 490 | end 491 | end 492 | 493 | local variableType = typeFromAST(variableDefinition.type) 494 | 495 | if hasDefault and variableType.__type ~= 'NonNull' then 496 | variableType = types.nonNull(variableType) 497 | end 498 | 499 | local function isTypeSubTypeOf(subType, superType) 500 | if subType == superType then return true end 501 | 502 | if superType.__type == 'NonNull' then 503 | if subType.__type == 'NonNull' then 504 | return isTypeSubTypeOf(subType.ofType, superType.ofType) 505 | end 506 | 507 | return false 508 | elseif subType.__type == 'NonNull' then 509 | return isTypeSubTypeOf(subType.ofType, superType) 510 | end 511 | 512 | if superType.__type == 'List' then 513 | if subType.__type == 'List' then 514 | return isTypeSubTypeOf(subType.ofType, superType.ofType) 515 | end 516 | 517 | return false 518 | elseif subType.__type == 'List' then 519 | return false 520 | end 521 | 522 | if subType.__type ~= 'Object' then return false end 523 | 524 | if superType.__type == 'Interface' then 525 | local implementors = context.schema:getImplementors(superType.name) 526 | return implementors and implementors[context.schema:getType(subType.name)] 527 | elseif superType.__type == 'Union' then 528 | local types = superType.types 529 | for i = 1, #types do 530 | if types[i] == subType then 531 | return true 532 | end 533 | end 534 | 535 | return false 536 | end 537 | 538 | return false 539 | end 540 | 541 | if not isTypeSubTypeOf(variableType, argumentType) then 542 | error('Variable type mismatch') 543 | end 544 | end 545 | end 546 | end 547 | end 548 | end 549 | 550 | return rules 551 | -------------------------------------------------------------------------------- /graphql/schema.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.[^%.]+$', '') 2 | local types = require(path .. '.types') 3 | local introspection = require(path .. '.introspection') 4 | 5 | local schema = {} 6 | schema.__index = schema 7 | 8 | schema.__emptyList = {} 9 | schema.__emptyObject = {} 10 | 11 | function schema.create(config) 12 | assert(type(config.query) == 'table', 'must provide query object') 13 | assert(not config.mutation or type(config.mutation) == 'table', 'mutation must be a table if provided') 14 | 15 | local self = setmetatable({}, schema) 16 | 17 | for k, v in pairs(config) do 18 | self[k] = v 19 | end 20 | 21 | self.directives = self.directives or { 22 | types.include, 23 | types.skip 24 | } 25 | 26 | self.typeMap = {} 27 | self.interfaceMap = {} 28 | self.directiveMap = {} 29 | 30 | self:generateTypeMap(self.query) 31 | self:generateTypeMap(self.mutation) 32 | self:generateTypeMap(self.subscription) 33 | self:generateTypeMap(introspection.__Schema) 34 | self:generateDirectiveMap() 35 | 36 | return self 37 | end 38 | 39 | function schema:generateTypeMap(node) 40 | if not node or (self.typeMap[node.name] and self.typeMap[node.name] == node) then return end 41 | 42 | if node.__type == 'NonNull' or node.__type == 'List' then 43 | return self:generateTypeMap(node.ofType) 44 | end 45 | 46 | if self.typeMap[node.name] and self.typeMap[node.name] ~= node then 47 | error('Encountered multiple types named "' .. node.name .. '"') 48 | end 49 | 50 | node.fields = type(node.fields) == 'function' and node.fields() or node.fields 51 | self.typeMap[node.name] = node 52 | 53 | if node.__type == 'Object' and node.interfaces then 54 | for _, interface in ipairs(node.interfaces) do 55 | self:generateTypeMap(interface) 56 | self.interfaceMap[interface.name] = self.interfaceMap[interface.name] or {} 57 | self.interfaceMap[interface.name][node] = node 58 | end 59 | end 60 | 61 | if node.__type == 'Object' or node.__type == 'Interface' or node.__type == 'InputObject' then 62 | for fieldName, field in pairs(node.fields) do 63 | if field.arguments then 64 | for name, argument in pairs(field.arguments) do 65 | local argumentType = argument.__type and argument or argument.kind 66 | assert(argumentType, 'Must supply type for argument "' .. name .. '" on "' .. fieldName .. '"') 67 | self:generateTypeMap(argumentType) 68 | end 69 | end 70 | 71 | self:generateTypeMap(field.kind) 72 | end 73 | end 74 | end 75 | 76 | function schema:generateDirectiveMap() 77 | for _, directive in ipairs(self.directives) do 78 | self.directiveMap[directive.name] = directive 79 | end 80 | end 81 | 82 | function schema:getType(name) 83 | if not name then return end 84 | return self.typeMap[name] 85 | end 86 | 87 | function schema:getImplementors(interface) 88 | local kind = self:getType(interface) 89 | local isInterface = kind and kind.__type == 'Interface' 90 | return self.interfaceMap[interface] or (isInterface and {} or nil) 91 | end 92 | 93 | function schema:getDirective(name) 94 | if not name then return false end 95 | return self.directiveMap[name] 96 | end 97 | 98 | function schema:getQueryType() 99 | return self.query 100 | end 101 | 102 | function schema:getMutationType() 103 | return self.mutation 104 | end 105 | 106 | function schema:getSubscriptionType() 107 | return self.subscription 108 | end 109 | 110 | function schema:getTypeMap() 111 | return self.typeMap 112 | end 113 | 114 | function schema:getPossibleTypes(abstractType) 115 | if abstractType.__type == 'Union' then 116 | return abstractType.types 117 | end 118 | 119 | return self:getImplementors(abstractType) 120 | end 121 | 122 | return schema 123 | -------------------------------------------------------------------------------- /graphql/types.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.[^%.]+$', '') 2 | local util = require(path .. '.util') 3 | 4 | local types = {} 5 | 6 | function types.nonNull(kind) 7 | assert(kind, 'Must provide a type') 8 | 9 | return { 10 | __type = 'NonNull', 11 | ofType = kind 12 | } 13 | end 14 | 15 | function types.list(kind) 16 | assert(kind, 'Must provide a type') 17 | 18 | return { 19 | __type = 'List', 20 | ofType = kind 21 | } 22 | end 23 | 24 | function types.scalar(config) 25 | assert(type(config.name) == 'string', 'type name must be provided as a string') 26 | assert(type(config.serialize) == 'function', 'serialize must be a function') 27 | if config.parseValue or config.parseLiteral then 28 | assert( 29 | type(config.parseValue) == 'function' and type(config.parseLiteral) == 'function', 30 | 'must provide both parseValue and parseLiteral to scalar type' 31 | ) 32 | end 33 | 34 | local instance = { 35 | __type = 'Scalar', 36 | name = config.name, 37 | description = config.description, 38 | serialize = config.serialize, 39 | parseValue = config.parseValue, 40 | parseLiteral = config.parseLiteral 41 | } 42 | 43 | instance.nonNull = types.nonNull(instance) 44 | 45 | return instance 46 | end 47 | 48 | function types.object(config) 49 | assert(type(config.name) == 'string', 'type name must be provided as a string') 50 | if config.isTypeOf then 51 | assert(type(config.isTypeOf) == 'function', 'must provide isTypeOf as a function') 52 | end 53 | 54 | local fields 55 | if type(config.fields) == 'function' then 56 | fields = util.compose(util.bind1(initFields, 'Object'), config.fields) 57 | else 58 | fields = initFields('Object', config.fields) 59 | end 60 | 61 | local instance = { 62 | __type = 'Object', 63 | name = config.name, 64 | description = config.description, 65 | isTypeOf = config.isTypeOf, 66 | fields = fields, 67 | interfaces = config.interfaces 68 | } 69 | 70 | instance.nonNull = types.nonNull(instance) 71 | 72 | return instance 73 | end 74 | 75 | function types.interface(config) 76 | assert(type(config.name) == 'string', 'type name must be provided as a string') 77 | assert(type(config.fields) == 'table', 'fields table must be provided') 78 | if config.resolveType then 79 | assert(type(config.resolveType) == 'function', 'must provide resolveType as a function') 80 | end 81 | 82 | local fields 83 | if type(config.fields) == 'function' then 84 | fields = util.compose(util.bind1(initFields, 'Interface'), config.fields) 85 | else 86 | fields = initFields('Interface', config.fields) 87 | end 88 | 89 | local instance = { 90 | __type = 'Interface', 91 | name = config.name, 92 | description = config.description, 93 | fields = fields, 94 | resolveType = config.resolveType 95 | } 96 | 97 | instance.nonNull = types.nonNull(instance) 98 | 99 | return instance 100 | end 101 | 102 | function initFields(kind, fields) 103 | assert(type(fields) == 'table', 'fields table must be provided') 104 | 105 | local result = {} 106 | 107 | for fieldName, field in pairs(fields) do 108 | field = field.__type and { kind = field } or field 109 | result[fieldName] = { 110 | name = fieldName, 111 | kind = field.kind, 112 | description = field.description, 113 | deprecationReason = field.deprecationReason, 114 | arguments = field.arguments or {}, 115 | resolve = kind == 'Object' and field.resolve or nil 116 | } 117 | end 118 | 119 | return result 120 | end 121 | 122 | function types.enum(config) 123 | assert(type(config.name) == 'string', 'type name must be provided as a string') 124 | assert(type(config.values) == 'table', 'values table must be provided') 125 | 126 | local instance 127 | local values = {} 128 | 129 | for name, entry in pairs(config.values) do 130 | entry = type(entry) == 'table' and entry or { value = entry } 131 | 132 | values[name] = { 133 | name = name, 134 | description = entry.description, 135 | deprecationReason = entry.deprecationReason, 136 | value = entry.value 137 | } 138 | end 139 | 140 | instance = { 141 | __type = 'Enum', 142 | name = config.name, 143 | description = config.description, 144 | values = values, 145 | serialize = function(name) 146 | return instance.values[name] and instance.values[name].value or name 147 | end 148 | } 149 | 150 | instance.nonNull = types.nonNull(instance) 151 | 152 | return instance 153 | end 154 | 155 | function types.union(config) 156 | assert(type(config.name) == 'string', 'type name must be provided as a string') 157 | assert(type(config.types) == 'table', 'types table must be provided') 158 | 159 | local instance = { 160 | __type = 'Union', 161 | name = config.name, 162 | types = config.types 163 | } 164 | 165 | instance.nonNull = types.nonNull(instance) 166 | 167 | return instance 168 | end 169 | 170 | function types.inputObject(config) 171 | assert(type(config.name) == 'string', 'type name must be provided as a string') 172 | 173 | local fields = {} 174 | for fieldName, field in pairs(config.fields) do 175 | field = field.__type and { kind = field } or field 176 | fields[fieldName] = { 177 | name = fieldName, 178 | kind = field.kind 179 | } 180 | end 181 | 182 | local instance = { 183 | __type = 'InputObject', 184 | name = config.name, 185 | description = config.description, 186 | fields = fields 187 | } 188 | 189 | return instance 190 | end 191 | 192 | local coerceInt = function(value) 193 | value = tonumber(value) 194 | 195 | if not value then return end 196 | 197 | if value == value and value < 2 ^ 32 and value >= -2 ^ 32 then 198 | return value < 0 and math.ceil(value) or math.floor(value) 199 | end 200 | end 201 | 202 | types.int = types.scalar({ 203 | name = 'Int', 204 | description = "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", 205 | serialize = coerceInt, 206 | parseValue = coerceInt, 207 | parseLiteral = function(node) 208 | if node.kind == 'int' then 209 | return coerceInt(node.value) 210 | end 211 | end 212 | }) 213 | 214 | types.float = types.scalar({ 215 | name = 'Float', 216 | serialize = tonumber, 217 | parseValue = tonumber, 218 | parseLiteral = function(node) 219 | if node.kind == 'float' or node.kind == 'int' then 220 | return tonumber(node.value) 221 | end 222 | end 223 | }) 224 | 225 | types.string = types.scalar({ 226 | name = 'String', 227 | description = "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 228 | serialize = tostring, 229 | parseValue = tostring, 230 | parseLiteral = function(node) 231 | if node.kind == 'string' then 232 | return node.value 233 | end 234 | end 235 | }) 236 | 237 | local function toboolean(x) 238 | return (x and x ~= 'false') and true or false 239 | end 240 | 241 | types.boolean = types.scalar({ 242 | name = 'Boolean', 243 | description = "The `Boolean` scalar type represents `true` or `false`.", 244 | serialize = toboolean, 245 | parseValue = toboolean, 246 | parseLiteral = function(node) 247 | if node.kind == 'boolean' then 248 | return toboolean(node.value) 249 | else 250 | return nil 251 | end 252 | end 253 | }) 254 | 255 | types.id = types.scalar({ 256 | name = 'ID', 257 | serialize = tostring, 258 | parseValue = tostring, 259 | parseLiteral = function(node) 260 | return node.kind == 'string' or node.kind == 'int' and node.value or nil 261 | end 262 | }) 263 | 264 | function types.directive(config) 265 | assert(type(config.name) == 'string', 'type name must be provided as a string') 266 | 267 | local instance = { 268 | __type = 'Directive', 269 | name = config.name, 270 | description = config.description, 271 | arguments = config.arguments, 272 | onQuery = config.onQuery, 273 | onMutation = config.onMutation, 274 | onField = config.onField, 275 | onFragmentDefinition = config.onFragmentDefinition, 276 | onFragmentSpread = config.onFragmentSpread, 277 | onInlineFragment = config.onInlineFragment 278 | } 279 | 280 | return instance 281 | end 282 | 283 | types.include = types.directive({ 284 | name = 'include', 285 | description = 'Directs the executor to include this field or fragment only when the `if` argument is true.', 286 | arguments = { 287 | ['if'] = { kind = types.boolean.nonNull, description = 'Included when true.'} 288 | }, 289 | onField = true, 290 | onFragmentSpread = true, 291 | onInlineFragment = true 292 | }) 293 | 294 | types.skip = types.directive({ 295 | name = 'skip', 296 | description = 'Directs the executor to skip this field or fragment when the `if` argument is true.', 297 | arguments = { 298 | ['if'] = { kind = types.boolean.nonNull, description = 'Skipped when true.' } 299 | }, 300 | onField = true, 301 | onFragmentSpread = true, 302 | onInlineFragment = true 303 | }) 304 | 305 | return types 306 | -------------------------------------------------------------------------------- /graphql/util.lua: -------------------------------------------------------------------------------- 1 | local util = {} 2 | 3 | function util.map(t, fn) 4 | local res = {} 5 | for k, v in pairs(t) do res[k] = fn(v, k) end 6 | return res 7 | end 8 | 9 | function util.find(t, fn) 10 | local res = {} 11 | for k, v in pairs(t) do 12 | if fn(v, k) then return v end 13 | end 14 | end 15 | 16 | function util.filter(t, fn) 17 | local res = {} 18 | for k,v in pairs(t) do 19 | if fn(v) then 20 | table.insert(res, v) 21 | end 22 | end 23 | return res 24 | end 25 | 26 | function util.values(t) 27 | local res = {} 28 | for _, value in pairs(t) do 29 | table.insert(res, value) 30 | end 31 | return res 32 | end 33 | 34 | function util.compose(f, g) 35 | return function(...) return f(g(...)) end 36 | end 37 | 38 | function util.bind1(func, x) 39 | return function(y) 40 | return func(x, y) 41 | end 42 | end 43 | 44 | function util.trim(s) 45 | return s:gsub('^%s+', ''):gsub('%s$', ''):gsub('%s%s+', ' ') 46 | end 47 | 48 | function util.coerceValue(node, schemaType, variables) 49 | variables = variables or {} 50 | 51 | if schemaType.__type == 'NonNull' then 52 | return util.coerceValue(node, schemaType.ofType, variables) 53 | end 54 | 55 | if not node then 56 | return nil 57 | end 58 | 59 | if node.kind == 'variable' then 60 | return variables[node.name.value] 61 | end 62 | 63 | if schemaType.__type == 'List' then 64 | if node.kind ~= 'list' then 65 | error('Expected a list') 66 | end 67 | 68 | return util.map(node.values, function(value) 69 | return util.coerceValue(value, schemaType.ofType, variables) 70 | end) 71 | end 72 | 73 | if schemaType.__type == 'InputObject' then 74 | if node.kind ~= 'inputObject' then 75 | error('Expected an input object') 76 | end 77 | 78 | return util.map(node.values, function(field) 79 | if not schemaType.fields[field.name] then 80 | error('Unknown input object field "' .. field.name .. '"') 81 | end 82 | 83 | return util.coerceValue(field.value, schemaType.fields[field.name].kind, variables) 84 | end) 85 | end 86 | 87 | if schemaType.__type == 'Enum' then 88 | if node.kind ~= 'enum' then 89 | error('Expected enum value, got ' .. node.kind) 90 | end 91 | 92 | if not schemaType.values[node.value] then 93 | error('Invalid enum value "' .. node.value .. '"') 94 | end 95 | 96 | return node.value 97 | end 98 | 99 | if schemaType.__type == 'Scalar' then 100 | if schemaType.parseLiteral(node) == nil then 101 | error('Could not coerce "' .. tostring(node.value) .. '" to "' .. schemaType.name .. '"') 102 | end 103 | 104 | return schemaType.parseLiteral(node) 105 | end 106 | end 107 | 108 | return util 109 | -------------------------------------------------------------------------------- /graphql/validate.lua: -------------------------------------------------------------------------------- 1 | local path = (...):gsub('%.[^%.]+$', '') 2 | local rules = require(path .. '.rules') 3 | local util = require(path .. '.util') 4 | local introspection = require(path .. '.introspection') 5 | local schema = require(path .. '.schema') 6 | 7 | local function getParentField(context, name, count) 8 | if introspection.fieldMap[name] then return introspection.fieldMap[name] end 9 | 10 | count = count or 1 11 | local parent = context.objects[#context.objects - count] 12 | 13 | -- Unwrap lists and non-null types 14 | while parent.ofType do 15 | parent = parent.ofType 16 | end 17 | 18 | return parent.fields[name] 19 | end 20 | 21 | local visitors = { 22 | document = { 23 | enter = function(node, context) 24 | for _, definition in ipairs(node.definitions) do 25 | if definition.kind == 'fragmentDefinition' then 26 | context.fragmentMap[definition.name.value] = definition 27 | end 28 | end 29 | end, 30 | 31 | children = function(node, context) 32 | return node.definitions 33 | end, 34 | 35 | rules = { rules.uniqueFragmentNames, exit = { rules.noUnusedFragments } } 36 | }, 37 | 38 | operation = { 39 | enter = function(node, context) 40 | table.insert(context.objects, context.schema[node.operation]) 41 | context.currentOperation = node 42 | context.variableReferences = {} 43 | end, 44 | 45 | exit = function(node, context) 46 | table.remove(context.objects) 47 | context.currentOperation = nil 48 | context.variableReferences = nil 49 | end, 50 | 51 | children = function(node) 52 | return { node.selectionSet } 53 | end, 54 | 55 | rules = { 56 | rules.uniqueOperationNames, 57 | rules.loneAnonymousOperation, 58 | rules.directivesAreDefined, 59 | rules.variablesHaveCorrectType, 60 | rules.variableDefaultValuesHaveCorrectType, 61 | exit = { 62 | rules.variablesAreUsed, 63 | rules.variablesAreDefined 64 | } 65 | } 66 | }, 67 | 68 | selectionSet = { 69 | children = function(node) 70 | return node.selections 71 | end, 72 | 73 | rules = { rules.unambiguousSelections } 74 | }, 75 | 76 | field = { 77 | enter = function(node, context) 78 | local name = node.name.value 79 | 80 | if introspection.fieldMap[name] then 81 | table.insert(context.objects, introspection.fieldMap[name].kind) 82 | else 83 | local parentField = getParentField(context, name, 0) 84 | -- false is a special value indicating that the field was not present in the type definition. 85 | table.insert(context.objects, parentField and parentField.kind or false) 86 | end 87 | end, 88 | 89 | exit = function(node, context) 90 | table.remove(context.objects) 91 | end, 92 | 93 | children = function(node) 94 | local children = {} 95 | 96 | if node.arguments then 97 | for i = 1, #node.arguments do 98 | table.insert(children, node.arguments[i]) 99 | end 100 | end 101 | 102 | if node.directives then 103 | for i = 1, #node.directives do 104 | table.insert(children, node.directives[i]) 105 | end 106 | end 107 | 108 | if node.selectionSet then 109 | table.insert(children, node.selectionSet) 110 | end 111 | 112 | return children 113 | end, 114 | 115 | rules = { 116 | rules.fieldsDefinedOnType, 117 | rules.argumentsDefinedOnType, 118 | rules.scalarFieldsAreLeaves, 119 | rules.compositeFieldsAreNotLeaves, 120 | rules.uniqueArgumentNames, 121 | rules.argumentsOfCorrectType, 122 | rules.requiredArgumentsPresent, 123 | rules.directivesAreDefined, 124 | rules.variableUsageAllowed 125 | } 126 | }, 127 | 128 | inlineFragment = { 129 | enter = function(node, context) 130 | local kind = false 131 | 132 | if node.typeCondition then 133 | kind = context.schema:getType(node.typeCondition.name.value) or false 134 | end 135 | 136 | table.insert(context.objects, kind) 137 | end, 138 | 139 | exit = function(node, context) 140 | table.remove(context.objects) 141 | end, 142 | 143 | children = function(node, context) 144 | if node.selectionSet then 145 | return {node.selectionSet} 146 | end 147 | end, 148 | 149 | rules = { 150 | rules.fragmentHasValidType, 151 | rules.fragmentSpreadIsPossible, 152 | rules.directivesAreDefined 153 | } 154 | }, 155 | 156 | fragmentSpread = { 157 | enter = function(node, context) 158 | context.usedFragments[node.name.value] = true 159 | 160 | local fragment = context.fragmentMap[node.name.value] 161 | 162 | if not fragment then return end 163 | 164 | local fragmentType = context.schema:getType(fragment.typeCondition.name.value) or false 165 | 166 | table.insert(context.objects, fragmentType) 167 | 168 | if context.currentOperation then 169 | local seen = {} 170 | local function collectTransitiveVariables(referencedNode) 171 | if not referencedNode then return end 172 | 173 | if referencedNode.kind == 'selectionSet' then 174 | for _, selection in ipairs(referencedNode.selections) do 175 | if not seen[selection] then 176 | seen[selection] = true 177 | collectTransitiveVariables(selection) 178 | end 179 | end 180 | elseif referencedNode.kind == 'field' then 181 | if referencedNode.arguments then 182 | for _, argument in ipairs(referencedNode.arguments) do 183 | collectTransitiveVariables(argument) 184 | end 185 | end 186 | 187 | if referencedNode.selectionSet then 188 | collectTransitiveVariables(referencedNode.selectionSet) 189 | end 190 | elseif referencedNode.kind == 'argument' then 191 | return collectTransitiveVariables(referencedNode.value) 192 | elseif referencedNode.kind == 'listType' or referencedNode.kind == 'nonNullType' then 193 | return collectTransitiveVariables(referencedNode.type) 194 | elseif referencedNode.kind == 'variable' then 195 | context.variableReferences[referencedNode.name.value] = true 196 | elseif referencedNode.kind == 'inlineFragment' then 197 | return collectTransitiveVariables(referencedNode.selectionSet) 198 | elseif referencedNode.kind == 'fragmentSpread' then 199 | local fragment = context.fragmentMap[referencedNode.name.value] 200 | context.usedFragments[referencedNode.name.value] = true 201 | return fragment and collectTransitiveVariables(fragment.selectionSet) 202 | end 203 | end 204 | 205 | collectTransitiveVariables(fragment.selectionSet) 206 | end 207 | end, 208 | 209 | exit = function(node, context) 210 | table.remove(context.objects) 211 | end, 212 | 213 | rules = { 214 | rules.fragmentSpreadTargetDefined, 215 | rules.fragmentSpreadIsPossible, 216 | rules.directivesAreDefined, 217 | rules.variableUsageAllowed 218 | } 219 | }, 220 | 221 | fragmentDefinition = { 222 | enter = function(node, context) 223 | kind = context.schema:getType(node.typeCondition.name.value) or false 224 | table.insert(context.objects, kind) 225 | end, 226 | 227 | exit = function(node, context) 228 | table.remove(context.objects) 229 | end, 230 | 231 | children = function(node) 232 | local children = {} 233 | 234 | for _, selection in ipairs(node.selectionSet) do 235 | table.insert(children, selection) 236 | end 237 | 238 | return children 239 | end, 240 | 241 | rules = { 242 | rules.fragmentHasValidType, 243 | rules.fragmentDefinitionHasNoCycles, 244 | rules.directivesAreDefined 245 | } 246 | }, 247 | 248 | argument = { 249 | enter = function(node, context) 250 | if context.currentOperation then 251 | local value = node.value 252 | while value.kind == 'listType' or value.kind == 'nonNullType' do 253 | value = value.type 254 | end 255 | 256 | if value.kind == 'variable' then 257 | context.variableReferences[value.name.value] = true 258 | end 259 | end 260 | end, 261 | 262 | rules = { rules.uniqueInputObjectFields } 263 | }, 264 | 265 | directive = { 266 | children = function(node, context) 267 | return node.arguments 268 | end 269 | } 270 | } 271 | 272 | return function(schema, tree) 273 | local context = { 274 | schema = schema, 275 | fragmentMap = {}, 276 | operationNames = {}, 277 | hasAnonymousOperation = false, 278 | usedFragments = {}, 279 | objects = {}, 280 | currentOperation = nil, 281 | variableReferences = nil 282 | } 283 | 284 | local function visit(node) 285 | local visitor = node.kind and visitors[node.kind] 286 | 287 | if not visitor then return end 288 | 289 | if visitor.enter then 290 | visitor.enter(node, context) 291 | end 292 | 293 | if visitor.rules then 294 | for i = 1, #visitor.rules do 295 | visitor.rules[i](node, context) 296 | end 297 | end 298 | 299 | if visitor.children then 300 | local children = visitor.children(node) 301 | if children then 302 | for _, child in ipairs(children) do 303 | visit(child) 304 | end 305 | end 306 | end 307 | 308 | if visitor.rules and visitor.rules.exit then 309 | for i = 1, #visitor.rules.exit do 310 | visitor.rules.exit[i](node, context) 311 | end 312 | end 313 | 314 | if visitor.exit then 315 | visitor.exit(node, context) 316 | end 317 | end 318 | 319 | return visit(tree) 320 | end 321 | -------------------------------------------------------------------------------- /tests/data/schema.lua: -------------------------------------------------------------------------------- 1 | local types = require 'graphql.types' 2 | local schema = require 'graphql.schema' 3 | 4 | local dogCommand = types.enum({ 5 | name = 'DogCommand', 6 | values = { 7 | SIT = true, 8 | DOWN = true, 9 | HEEL = true 10 | } 11 | }) 12 | 13 | local pet = types.interface({ 14 | name = 'Pet', 15 | fields = { 16 | name = types.string.nonNull, 17 | nickname = types.int 18 | } 19 | }) 20 | 21 | local dog = types.object({ 22 | name = 'Dog', 23 | interfaces = { pet }, 24 | fields = { 25 | name = types.string, 26 | nickname = types.string, 27 | barkVolume = types.int, 28 | doesKnowCommand = { 29 | kind = types.boolean.nonNull, 30 | arguments = { 31 | dogCommand = dogCommand.nonNull 32 | } 33 | }, 34 | isHouseTrained = { 35 | kind = types.boolean.nonNull, 36 | arguments = { 37 | atOtherHomes = types.boolean 38 | } 39 | }, 40 | complicatedField = { 41 | kind = types.boolean, 42 | arguments = { 43 | complicatedArgument = types.inputObject({ 44 | name = 'complicated', 45 | fields = { 46 | x = types.string, 47 | y = types.integer, 48 | z = types.inputObject({ 49 | name = 'alsoComplicated', 50 | fields = { 51 | x = types.string, 52 | y = types.integer 53 | } 54 | }) 55 | } 56 | }) 57 | } 58 | } 59 | } 60 | }) 61 | 62 | local sentient = types.interface({ 63 | name = 'Sentient', 64 | fields = { 65 | name = types.string.nonNull 66 | } 67 | }) 68 | 69 | local alien = types.object({ 70 | name = 'Alien', 71 | interfaces = sentient, 72 | fields = { 73 | name = types.string.nonNull, 74 | homePlanet = types.string 75 | } 76 | }) 77 | 78 | local human = types.object({ 79 | name = 'Human', 80 | fields = { 81 | name = types.string.nonNull 82 | } 83 | }) 84 | 85 | local cat = types.object({ 86 | name = 'Cat', 87 | fields = { 88 | name = types.string.nonNull, 89 | nickname = types.string, 90 | meowVolume = types.int 91 | } 92 | }) 93 | 94 | local catOrDog = types.union({ 95 | name = 'CatOrDog', 96 | types = {cat, dog} 97 | }) 98 | 99 | local dogOrHuman = types.union({ 100 | name = 'DogOrHuman', 101 | types = {dog, human} 102 | }) 103 | 104 | local humanOrAlien = types.union({ 105 | name = 'HumanOrAlien', 106 | types = {human, alien} 107 | }) 108 | 109 | local query = types.object({ 110 | name = 'Query', 111 | fields = { 112 | dog = { 113 | kind = dog, 114 | args = { 115 | name = { 116 | kind = types.string 117 | } 118 | } 119 | }, 120 | cat = cat, 121 | pet = pet, 122 | sentient = sentient, 123 | catOrDog = catOrDog, 124 | humanOrAlien = humanOrAlien 125 | } 126 | }) 127 | 128 | return schema.create({ 129 | query = query 130 | }) 131 | -------------------------------------------------------------------------------- /tests/lust.lua: -------------------------------------------------------------------------------- 1 | -- lust - Lua test framework 2 | -- https://github.com/bjornbytes/lust 3 | -- License - MIT, see LICENSE for details. 4 | 5 | local lust = {} 6 | lust.level = 0 7 | lust.passes = 0 8 | lust.errors = 0 9 | lust.befores = {} 10 | lust.afters = {} 11 | 12 | local red = string.char(27) .. '[31m' 13 | local green = string.char(27) .. '[32m' 14 | local normal = string.char(27) .. '[0m' 15 | local function indent(level) return string.rep('\t', level or lust.level) end 16 | 17 | function lust.describe(name, fn) 18 | print(indent() .. name) 19 | lust.level = lust.level + 1 20 | fn() 21 | lust.befores[lust.level] = {} 22 | lust.afters[lust.level] = {} 23 | lust.level = lust.level - 1 24 | end 25 | 26 | function lust.it(name, fn) 27 | for level = 1, lust.level do 28 | if lust.befores[level] then 29 | for i = 1, #lust.befores[level] do 30 | lust.befores[level][i](name) 31 | end 32 | end 33 | end 34 | 35 | local success, err = pcall(fn) 36 | if success then lust.passes = lust.passes + 1 37 | else lust.errors = lust.errors + 1 end 38 | local color = success and green or red 39 | local label = success and 'PASS' or 'FAIL' 40 | print(indent() .. color .. label .. normal .. ' ' .. name) 41 | if err then 42 | print(indent(lust.level + 1) .. red .. err .. normal) 43 | end 44 | 45 | for level = 1, lust.level do 46 | if lust.afters[level] then 47 | for i = 1, #lust.afters[level] do 48 | lust.afters[level][i](name) 49 | end 50 | end 51 | end 52 | end 53 | 54 | function lust.before(fn) 55 | lust.befores[lust.level] = lust.befores[lust.level] or {} 56 | table.insert(lust.befores[lust.level], fn) 57 | end 58 | 59 | function lust.after(fn) 60 | lust.afters[lust.level] = lust.afters[lust.level] or {} 61 | table.insert(lust.afters[lust.level], fn) 62 | end 63 | 64 | -- Assertions 65 | local function isa(v, x) 66 | if type(x) == 'string' then return type(v) == x, tostring(v) .. ' is not a ' .. x 67 | elseif type(x) == 'table' then 68 | if type(v) ~= 'table' then return false, tostring(v) .. ' is not a ' .. tostring(x) end 69 | local seen = {} 70 | local meta = v 71 | while meta and not seen[meta] do 72 | if meta == x then return true end 73 | seen[meta] = true 74 | meta = getmetatable(meta) and getmetatable(meta).__index 75 | end 76 | return false, tostring(v) .. ' is not a ' .. tostring(x) 77 | end 78 | return false, 'invalid type ' .. tostring(x) 79 | end 80 | 81 | local function has(t, x) 82 | for k, v in pairs(t) do 83 | if v == x then return true end 84 | end 85 | return false 86 | end 87 | 88 | local function strict_eq(t1, t2) 89 | if type(t1) ~= type(t2) then return false end 90 | if type(t1) ~= 'table' then return t1 == t2 end 91 | if #t1 ~= #t2 then return false end 92 | for k, _ in pairs(t1) do 93 | if not strict_eq(t1[k], t2[k]) then return false end 94 | end 95 | for k, _ in pairs(t2) do 96 | if not strict_eq(t2[k], t1[k]) then return false end 97 | end 98 | return true 99 | end 100 | 101 | local paths = { 102 | [''] = {'to', 'to_not'}, 103 | to = {'have', 'equal', 'be', 'exist', 'fail'}, 104 | to_not = {'have', 'equal', 'be', 'exist', 'fail', chain = function(a) a.negate = not a.negate end}, 105 | be = {'a', 'an', 'truthy', 'falsy', f = function(v, x) 106 | return v == x, tostring(v) .. ' and ' .. tostring(x) .. ' are not equal' 107 | end}, 108 | a = {f = isa}, 109 | an = {f = isa}, 110 | exist = {f = function(v) return v ~= nil, tostring(v) .. ' is nil' end}, 111 | truthy = {f = function(v) return v, tostring(v) .. ' is not truthy' end}, 112 | falsy = {f = function(v) return not v, tostring(v) .. ' is not falsy' end}, 113 | equal = {f = function(v, x) return strict_eq(v, x), tostring(v) .. ' and ' .. tostring(x) .. ' are not strictly equal' end}, 114 | have = { 115 | f = function(v, x) 116 | if type(v) ~= 'table' then return false, 'table "' .. tostring(v) .. '" is not a table' end 117 | return has(v, x), 'table "' .. tostring(v) .. '" does not have ' .. tostring(x) 118 | end 119 | }, 120 | fail = {'with', f = function(v) return not pcall(v), tostring(v) .. ' did not fail' end}, 121 | with = {f = function(v, x) local _, e = pcall(v) return e and e:find(x), tostring(v) .. ' did not fail with ' .. tostring(x) end} 122 | } 123 | 124 | function lust.expect(v) 125 | local assertion = {} 126 | assertion.val = v 127 | assertion.action = '' 128 | assertion.negate = false 129 | 130 | setmetatable(assertion, { 131 | __index = function(t, k) 132 | if has(paths[rawget(t, 'action')], k) then 133 | rawset(t, 'action', k) 134 | local chain = paths[rawget(t, 'action')].chain 135 | if chain then chain(t) end 136 | return t 137 | end 138 | return rawget(t, k) 139 | end, 140 | __call = function(t, ...) 141 | if paths[t.action].f then 142 | local res, err = paths[t.action].f(t.val, ...) 143 | if assertion.negate then res = not res end 144 | if not res then 145 | error(err or 'unknown failure', 2) 146 | end 147 | end 148 | end 149 | }) 150 | 151 | return assertion 152 | end 153 | 154 | function lust.spy(target, name, run) 155 | local spy = {} 156 | local subject 157 | 158 | local function capture(...) 159 | table.insert(spy, {...}) 160 | return subject(...) 161 | end 162 | 163 | if type(target) == 'table' then 164 | subject = target[name] 165 | target[name] = capture 166 | else 167 | run = name 168 | subject = target or function() end 169 | end 170 | 171 | setmetatable(spy, {__call = function(_, ...) return capture(...) end}) 172 | 173 | if run then run() end 174 | 175 | return spy 176 | end 177 | 178 | lust.test = lust.it 179 | lust.paths = paths 180 | 181 | return lust 182 | -------------------------------------------------------------------------------- /tests/parse.lua: -------------------------------------------------------------------------------- 1 | describe('parse', function() 2 | local parse = require 'graphql.parse' 3 | 4 | test('comments', function() 5 | local document 6 | 7 | document = parse('#') 8 | expect(document.definitions).to.equal({}) 9 | 10 | document = parse('#{}') 11 | expect(document.definitions).to.equal({}) 12 | expect(parse('{}').definitions).to_not.equal({}) 13 | 14 | expect(function() parse('{}#a$b@') end).to_not.fail() 15 | expect(function() parse('{a(b:"#")}') end).to_not.fail() 16 | end) 17 | 18 | test('document', function() 19 | local document 20 | 21 | expect(function() parse() end).to.fail() 22 | expect(function() parse('foo') end).to.fail() 23 | expect(function() parse('query') end).to.fail() 24 | expect(function() parse('query{} foo') end).to.fail() 25 | 26 | document = parse('') 27 | expect(document.kind).to.equal('document') 28 | expect(document.definitions).to.equal({}) 29 | 30 | document = parse('query{} mutation{} {}') 31 | expect(document.kind).to.equal('document') 32 | expect(#document.definitions).to.equal(3) 33 | end) 34 | 35 | describe('operation', function() 36 | local operation 37 | 38 | test('shorthand', function() 39 | operation = parse('{}').definitions[1] 40 | expect(operation.kind).to.equal('operation') 41 | expect(operation.name).to_not.exist() 42 | expect(operation.operation).to.equal('query') 43 | end) 44 | 45 | test('operationType', function() 46 | operation = parse('query{}').definitions[1] 47 | expect(operation.operation).to.equal('query') 48 | 49 | operation = parse('mutation{}').definitions[1] 50 | expect(operation.operation).to.equal('mutation') 51 | 52 | expect(function() parse('kneeReplacement{}') end).to.fail() 53 | end) 54 | 55 | test('name', function() 56 | operation = parse('query{}').definitions[1] 57 | expect(operation.name).to_not.exist() 58 | 59 | operation = parse('query queryName{}').definitions[1] 60 | expect(operation.name).to.exist() 61 | expect(operation.name.value).to.equal('queryName') 62 | end) 63 | 64 | test('variableDefinitions', function() 65 | expect(function() parse('query(){}') end).to.fail() 66 | expect(function() parse('query(x){}') end).to.fail() 67 | 68 | operation = parse('query name($a:Int,$b:Int){}').definitions[1] 69 | expect(operation.name.value).to.equal('name') 70 | expect(operation.variableDefinitions).to.exist() 71 | expect(#operation.variableDefinitions).to.equal(2) 72 | 73 | operation = parse('query($a:Int,$b:Int){}').definitions[1] 74 | expect(operation.variableDefinitions).to.exist() 75 | expect(#operation.variableDefinitions).to.equal(2) 76 | end) 77 | 78 | test('directives', function() 79 | local operation = parse('query{}').definitions[1] 80 | expect(operation.directives).to_not.exist() 81 | 82 | local operation = parse('query @a{}').definitions[1] 83 | expect(#operation.directives).to.exist() 84 | 85 | local operation = parse('query name @a{}').definitions[1] 86 | expect(#operation.directives).to.exist() 87 | 88 | local operation = parse('query ($a:Int) @a {}').definitions[1] 89 | expect(#operation.directives).to.exist() 90 | 91 | local operation = parse('query name ($a:Int) @a {}').definitions[1] 92 | expect(#operation.directives).to.exist() 93 | end) 94 | end) 95 | 96 | describe('fragmentDefinition', function() 97 | local fragment 98 | 99 | test('fragmentName', function() 100 | expect(function() parse('fragment {}') end).to.fail() 101 | expect(function() parse('fragment on x {}') end).to.fail() 102 | expect(function() parse('fragment on on x {}') end).to.fail() 103 | 104 | fragment = parse('fragment x on y {}').definitions[1] 105 | expect(fragment.kind).to.equal('fragmentDefinition') 106 | expect(fragment.name.value).to.equal('x') 107 | end) 108 | 109 | test('typeCondition', function() 110 | expect(function() parse('fragment x {}') end).to.fail() 111 | 112 | fragment = parse('fragment x on y {}').definitions[1] 113 | expect(fragment.typeCondition.name.value).to.equal('y') 114 | end) 115 | 116 | test('selectionSet', function() 117 | expect(function() parse('fragment x on y') end).to.fail() 118 | 119 | fragment = parse('fragment x on y {}').definitions[1] 120 | expect(fragment.selectionSet).to.exist() 121 | end) 122 | end) 123 | 124 | test('selectionSet', function() 125 | local selectionSet 126 | 127 | expect(function() parse('{') end).to.fail() 128 | expect(function() parse('}') end).to.fail() 129 | 130 | selectionSet = parse('{}').definitions[1].selectionSet 131 | expect(selectionSet.kind).to.equal('selectionSet') 132 | expect(selectionSet.selections).to.equal({}) 133 | 134 | selectionSet = parse('{a b}').definitions[1].selectionSet 135 | expect(#selectionSet.selections).to.equal(2) 136 | end) 137 | 138 | describe('field', function() 139 | local field 140 | 141 | test('name', function() 142 | expect(function() parse('{$a}') end).to.fail() 143 | expect(function() parse('{@a}') end).to.fail() 144 | expect(function() parse('{.}') end).to.fail() 145 | expect(function() parse('{,}') end).to.fail() 146 | 147 | field = parse('{a}').definitions[1].selectionSet.selections[1] 148 | expect(field.kind).to.equal('field') 149 | expect(field.name.value).to.equal('a') 150 | end) 151 | 152 | test('alias', function() 153 | expect(function() parse('{a:b:}') end).to.fail() 154 | expect(function() parse('{a:b:c}') end).to.fail() 155 | expect(function() parse('{:a}') end).to.fail() 156 | 157 | field = parse('{a}').definitions[1].selectionSet.selections[1] 158 | expect(field.alias).to_not.exist() 159 | 160 | field = parse('{a:b}').definitions[1].selectionSet.selections[1] 161 | expect(field.alias).to.exist() 162 | expect(field.alias.kind).to.equal('alias') 163 | expect(field.alias.name.value).to.equal('a') 164 | expect(field.name.value).to.equal('b') 165 | end) 166 | 167 | test('arguments', function() 168 | expect(function() parse('{a()}') end).to.fail() 169 | 170 | field = parse('{a}').definitions[1].selectionSet.selections[1] 171 | expect(field.arguments).to_not.exist() 172 | 173 | field = parse('{a(b:false)}').definitions[1].selectionSet.selections[1] 174 | expect(field.arguments).to.exist() 175 | end) 176 | 177 | test('directives', function() 178 | expect(function() parse('{a@skip(b:false)(c:true)}') end).to.fail() 179 | 180 | field = parse('{a}').definitions[1].selectionSet.selections[1] 181 | expect(field.directives).to_not.exist() 182 | 183 | field = parse('{a@skip}').definitions[1].selectionSet.selections[1] 184 | expect(field.directives).to.exist() 185 | 186 | field = parse('{a(b:1)@skip}').definitions[1].selectionSet.selections[1] 187 | expect(field.directives).to.exist() 188 | end) 189 | 190 | test('selectionSet', function() 191 | expect(function() parse('{{}}') end).to.fail() 192 | 193 | field = parse('{a}').definitions[1].selectionSet.selections[1] 194 | expect(field.selectionSet).to_not.exist() 195 | 196 | field = parse('{a{}}').definitions[1].selectionSet.selections[1] 197 | expect(field.selectionSet).to.exist() 198 | 199 | field = parse('{a{a}}').definitions[1].selectionSet.selections[1] 200 | expect(field.selectionSet).to.exist() 201 | 202 | field = parse('{a(b:1)@skip{a}}').definitions[1].selectionSet.selections[1] 203 | expect(field.selectionSet).to.exist() 204 | end) 205 | end) 206 | 207 | describe('fragmentSpread', function() 208 | local fragmentSpread 209 | 210 | test('name', function() 211 | expect(function() parse('{..a}') end).to.fail() 212 | expect(function() parse('{...}') end).to.fail() 213 | 214 | fragmentSpread = parse('{...a}').definitions[1].selectionSet.selections[1] 215 | expect(fragmentSpread.kind).to.equal('fragmentSpread') 216 | expect(fragmentSpread.name.value).to.equal('a') 217 | end) 218 | 219 | test('directives', function() 220 | expect(function() parse('{...a@}') end).to.fail() 221 | 222 | fragmentSpread = parse('{...a}').definitions[1].selectionSet.selections[1] 223 | expect(fragmentSpread.directives).to_not.exist() 224 | 225 | fragmentSpread = parse('{...a@skip}').definitions[1].selectionSet.selections[1] 226 | expect(fragmentSpread.directives).to.exist() 227 | end) 228 | end) 229 | 230 | describe('inlineFragment', function() 231 | local inlineFragment 232 | 233 | test('typeCondition', function() 234 | expect(function() parse('{...on{}}') end).to.fail() 235 | 236 | inlineFragment = parse('{...{}}').definitions[1].selectionSet.selections[1] 237 | expect(inlineFragment.kind).to.equal('inlineFragment') 238 | expect(inlineFragment.typeCondition).to_not.exist() 239 | 240 | inlineFragment = parse('{...on a{}}').definitions[1].selectionSet.selections[1] 241 | expect(inlineFragment.typeCondition).to.exist() 242 | expect(inlineFragment.typeCondition.name.value).to.equal('a') 243 | end) 244 | 245 | test('directives', function() 246 | expect(function() parse('{...on a @ {}}') end).to.fail() 247 | 248 | inlineFragment = parse('{...{}}').definitions[1].selectionSet.selections[1] 249 | expect(inlineFragment.directives).to_not.exist() 250 | 251 | inlineFragment = parse('{...@skip{}}').definitions[1].selectionSet.selections[1] 252 | expect(inlineFragment.directives).to.exist() 253 | 254 | inlineFragment = parse('{...on a@skip {}}').definitions[1].selectionSet.selections[1] 255 | expect(inlineFragment.directives).to.exist() 256 | end) 257 | 258 | test('selectionSet', function() 259 | expect(function() parse('{... on a}') end).to.fail() 260 | 261 | inlineFragment = parse('{...{}}').definitions[1].selectionSet.selections[1] 262 | expect(inlineFragment.selectionSet).to.exist() 263 | 264 | inlineFragment = parse('{... on a{}}').definitions[1].selectionSet.selections[1] 265 | expect(inlineFragment.selectionSet).to.exist() 266 | end) 267 | end) 268 | 269 | test('arguments', function() 270 | local arguments 271 | 272 | expect(function() parse('{a()}') end).to.fail() 273 | 274 | arguments = parse('{a(b:1)}').definitions[1].selectionSet.selections[1].arguments 275 | expect(#arguments).to.equal(1) 276 | 277 | arguments = parse('{a(b:1 c:1)}').definitions[1].selectionSet.selections[1].arguments 278 | expect(#arguments).to.equal(2) 279 | end) 280 | 281 | test('argument', function() 282 | local argument 283 | 284 | expect(function() parse('{a(b)}') end).to.fail() 285 | expect(function() parse('{a(@b)}') end).to.fail() 286 | expect(function() parse('{a($b)}') end).to.fail() 287 | expect(function() parse('{a(b::)}') end).to.fail() 288 | expect(function() parse('{a(:1)}') end).to.fail() 289 | expect(function() parse('{a(b:)}') end).to.fail() 290 | expect(function() parse('{a(:)}') end).to.fail() 291 | expect(function() parse('{a(b c)}') end).to.fail() 292 | 293 | argument = parse('{a(b:1)}').definitions[1].selectionSet.selections[1].arguments[1] 294 | expect(argument.kind).to.equal('argument') 295 | expect(argument.name.value).to.equal('b') 296 | expect(argument.value.value).to.equal('1') 297 | end) 298 | 299 | test('directives', function() 300 | local directives 301 | 302 | expect(function() parse('{a@}') end).to.fail() 303 | expect(function() parse('{a@@}') end).to.fail() 304 | 305 | directives = parse('{a@b}').definitions[1].selectionSet.selections[1].directives 306 | expect(#directives).to.equal(1) 307 | 308 | directives = parse('{a@b(c:1)@d}').definitions[1].selectionSet.selections[1].directives 309 | expect(#directives).to.equal(2) 310 | end) 311 | 312 | test('directive', function() 313 | local directive 314 | 315 | expect(function() parse('{a@b()}') end).to.fail() 316 | 317 | directive = parse('{a@b}').definitions[1].selectionSet.selections[1].directives[1] 318 | expect(directive.kind).to.equal('directive') 319 | expect(directive.name.value).to.equal('b') 320 | expect(directive.arguments).to_not.exist() 321 | 322 | directive = parse('{a@b(c:1)}').definitions[1].selectionSet.selections[1].directives[1] 323 | expect(directive.arguments).to.exist() 324 | end) 325 | 326 | test('variableDefinitions', function() 327 | local variableDefinitions 328 | 329 | expect(function() parse('query(){}') end).to.fail() 330 | expect(function() parse('query(a){}') end).to.fail() 331 | expect(function() parse('query(@a){}') end).to.fail() 332 | expect(function() parse('query($a){}') end).to.fail() 333 | 334 | variableDefinitions = parse('query($a:Int){}').definitions[1].variableDefinitions 335 | expect(#variableDefinitions).to.equal(1) 336 | 337 | variableDefinitions = parse('query($a:Int $b:Int){}').definitions[1].variableDefinitions 338 | expect(#variableDefinitions).to.equal(2) 339 | end) 340 | 341 | describe('variableDefinition', function() 342 | local variableDefinition 343 | 344 | test('variable', function() 345 | variableDefinition = parse('query($a:Int){}').definitions[1].variableDefinitions[1] 346 | expect(variableDefinition.kind).to.equal('variableDefinition') 347 | expect(variableDefinition.variable.name.value).to.equal('a') 348 | end) 349 | 350 | test('type', function() 351 | expect(function() parse('query($a){}') end).to.fail() 352 | expect(function() parse('query($a:){}') end).to.fail() 353 | expect(function() parse('query($a Int){}') end).to.fail() 354 | 355 | variableDefinition = parse('query($a:Int){}').definitions[1].variableDefinitions[1] 356 | expect(variableDefinition.type.name.value).to.equal('Int') 357 | end) 358 | 359 | test('defaultValue', function() 360 | expect(function() parse('query($a:Int=){}') end).to.fail() 361 | 362 | variableDefinition = parse('query($a:Int){}').definitions[1].variableDefinitions[1] 363 | expect(variableDefinition.defaultValue).to_not.exist() 364 | 365 | variableDefinition = parse('query($a:Int=1){}').definitions[1].variableDefinitions[1] 366 | expect(variableDefinition.defaultValue).to.exist() 367 | end) 368 | end) 369 | 370 | describe('value', function() 371 | local value 372 | 373 | local function run(input, result, type) 374 | local value = parse('{x(y:' .. input .. ')}').definitions[1].selectionSet.selections[1].arguments[1].value 375 | if type then expect(value.kind).to.equal(type) end 376 | if result then expect(value.value).to.equal(result) end 377 | return value 378 | end 379 | 380 | test('variable', function() 381 | expect(function() parse('{x(y:$)}') end).to.fail() 382 | expect(function() parse('{x(y:$a$)}') end).to.fail() 383 | 384 | value = run('$a') 385 | expect(value.kind).to.equal('variable') 386 | expect(value.name.value).to.equal('a') 387 | end) 388 | 389 | test('int', function() 390 | expect(function() parse('{x(y:01)}') end).to.fail() 391 | expect(function() parse('{x(y:-01)}') end).to.fail() 392 | expect(function() parse('{x(y:--1)}') end).to.fail() 393 | expect(function() parse('{x(y:+0)}') end).to.fail() 394 | 395 | run('0', '0', 'int') 396 | run('-0', '-0', 'int') 397 | run('1234', '1234', 'int') 398 | run('-1234', '-1234', 'int') 399 | end) 400 | 401 | test('float', function() 402 | expect(function() parse('{x(y:.1)}') end).to.fail() 403 | expect(function() parse('{x(y:1.)}') end).to.fail() 404 | expect(function() parse('{x(y:1..)}') end).to.fail() 405 | expect(function() parse('{x(y:0e1.0)}') end).to.fail() 406 | 407 | run('0.0', '0.0', 'float') 408 | run('-0.0', '-0.0', 'float') 409 | run('12.34', '12.34', 'float') 410 | run('1e0', '1e0', 'float') 411 | run('1e3', '1e3', 'float') 412 | run('1.0e3', '1.0e3', 'float') 413 | run('1.0e+3', '1.0e+3', 'float') 414 | run('1.0e-3', '1.0e-3', 'float') 415 | run('1.00e-30', '1.00e-30', 'float') 416 | end) 417 | 418 | test('boolean', function() 419 | run('true', 'true', 'boolean') 420 | run('false', 'false', 'boolean') 421 | end) 422 | 423 | test('string', function() 424 | expect(function() parse('{x(y:")}') end).to.fail() 425 | expect(function() parse('{x(y:\'\')}') end).to.fail() 426 | expect(function() parse('{x(y:"\n")}') end).to.fail() 427 | 428 | run('"yarn"', 'yarn', 'string') 429 | run('"th\\"read"', 'th"read', 'string') 430 | end) 431 | 432 | test('enum', function() 433 | run('a', 'a', 'enum') 434 | end) 435 | 436 | test('list', function() 437 | expect(function() parse('{x(y:[)}') end).to.fail() 438 | 439 | value = run('[]') 440 | expect(value.values).to.equal({}) 441 | 442 | value = run('[a 1]') 443 | expect(value).to.equal({ 444 | kind = 'list', 445 | values = { 446 | { 447 | kind = 'enum', 448 | value = 'a' 449 | }, 450 | { 451 | kind = 'int', 452 | value = '1' 453 | } 454 | } 455 | }) 456 | 457 | value = run('[a [b] c]') 458 | expect(value).to.equal({ 459 | kind = 'list', 460 | values = { 461 | { 462 | kind = 'enum', 463 | value = 'a' 464 | }, 465 | { 466 | kind = 'list', 467 | values = { 468 | { 469 | kind = 'enum', 470 | value = 'b' 471 | } 472 | } 473 | }, 474 | { 475 | kind = 'enum', 476 | value = 'c' 477 | } 478 | } 479 | }) 480 | end) 481 | 482 | test('object', function() 483 | expect(function() parse('{x(y:{a})}') end).to.fail() 484 | expect(function() parse('{x(y:{a:})}') end).to.fail() 485 | expect(function() parse('{x(y:{a::})}') end).to.fail() 486 | expect(function() parse('{x(y:{1:1})}') end).to.fail() 487 | expect(function() parse('{x(y:{"foo":"bar"})}') end).to.fail() 488 | 489 | value = run('{}') 490 | expect(value.kind).to.equal('inputObject') 491 | expect(value.values).to.equal({}) 492 | 493 | value = run('{a:1}') 494 | expect(value.values).to.equal({ 495 | { 496 | name = 'a', 497 | value = { 498 | kind = 'int', 499 | value = '1' 500 | } 501 | } 502 | }) 503 | 504 | value = run('{a:1 b:2}') 505 | expect(#value.values).to.equal(2) 506 | end) 507 | end) 508 | 509 | test('namedType', function() 510 | expect(function() parse('query($a:$b){}') end).to.fail() 511 | 512 | local namedType = parse('query($a:b){}').definitions[1].variableDefinitions[1].type 513 | expect(namedType.kind).to.equal('namedType') 514 | expect(namedType.name.value).to.equal('b') 515 | end) 516 | 517 | test('listType', function() 518 | local listType 519 | 520 | expect(function() parse('query($a:[]){}') end).to.fail() 521 | 522 | listType = parse('query($a:[b]){}').definitions[1].variableDefinitions[1].type 523 | expect(listType.kind).to.equal('listType') 524 | expect(listType.type.kind).to.equal('namedType') 525 | expect(listType.type.name.value).to.equal('b') 526 | 527 | listType = parse('query($a:[[b]]){}').definitions[1].variableDefinitions[1].type 528 | expect(listType.kind).to.equal('listType') 529 | expect(listType.type.kind).to.equal('listType') 530 | end) 531 | 532 | test('nonNullType', function() 533 | local nonNullType 534 | 535 | expect(function() parse('query($a:!){}') end).to.fail() 536 | expect(function() parse('query($a:b!!){}') end).to.fail() 537 | 538 | nonNullType = parse('query($a:b!){}').definitions[1].variableDefinitions[1].type 539 | expect(nonNullType.kind).to.equal('nonNullType') 540 | expect(nonNullType.type.kind).to.equal('namedType') 541 | expect(nonNullType.type.name.value).to.equal('b') 542 | 543 | nonNullType = parse('query($a:[b]!){}').definitions[1].variableDefinitions[1].type 544 | expect(nonNullType.kind).to.equal('nonNullType') 545 | expect(nonNullType.type.kind).to.equal('listType') 546 | end) 547 | end) 548 | -------------------------------------------------------------------------------- /tests/rules.lua: -------------------------------------------------------------------------------- 1 | local parse = require 'graphql.parse' 2 | local validate = require 'graphql.validate' 3 | local schema = require 'tests/data/schema' 4 | 5 | local function expectError(message, document) 6 | if not message then 7 | expect(function() validate(schema, parse(document)) end).to_not.fail() 8 | else 9 | expect(function() validate(schema, parse(document)) end).to.fail.with(message) 10 | end 11 | end 12 | 13 | describe('rules', function() 14 | local document 15 | 16 | describe('uniqueOperationNames', function() 17 | local message = 'Multiple operations exist named' 18 | 19 | it('errors if two operations have the same name', function() 20 | expectError(message, [[ 21 | query foo { } 22 | query foo { } 23 | ]]) 24 | end) 25 | 26 | it('passes if all operations have different names', function() 27 | expectError(nil, [[ 28 | query foo { } 29 | query bar { } 30 | ]]) 31 | end) 32 | end) 33 | 34 | describe('loneAnonymousOperation', function() 35 | local message = 'Cannot have more than one operation when' 36 | 37 | it('fails if there is more than one operation and one of them is anonymous', function() 38 | expectError(message, [[ 39 | query { } 40 | query named { } 41 | ]]) 42 | 43 | expectError(message, [[ 44 | query named { } 45 | query { } 46 | ]]) 47 | 48 | expectError(message, [[ 49 | query { } 50 | query { } 51 | ]]) 52 | end) 53 | 54 | it('passes if there is one anonymous operation', function() 55 | expectError(nil, '{}') 56 | end) 57 | 58 | it('passes if there are two named operations', function() 59 | expectError(nil, [[ 60 | query one {} 61 | query two {} 62 | ]]) 63 | end) 64 | end) 65 | 66 | describe('fieldsDefinedOnType', function() 67 | local message = 'is not defined on type' 68 | 69 | it('fails if a field does not exist on an object type', function() 70 | expectError(message, '{ doggy { name } }') 71 | expectError(message, '{ dog { age } }') 72 | end) 73 | 74 | it('passes if all fields exist on object types', function() 75 | expectError(nil, '{ dog { name } }') 76 | end) 77 | 78 | it('understands aliases', function() 79 | expectError(nil, '{ doggy: dog { name } }') 80 | expectError(message, '{ dog: doggy { name } }') 81 | end) 82 | end) 83 | 84 | describe('argumentsDefinedOnType', function() 85 | local message = 'Non%-existent argument' 86 | 87 | it('passes if no arguments are supplied', function() 88 | expectError(nil, '{ dog { isHouseTrained } }') 89 | end) 90 | 91 | it('errors if an argument name does not match the schema', function() 92 | expectError(message, [[{ 93 | dog { 94 | doesKnowCommand(doggyCommand: SIT) 95 | } 96 | }]]) 97 | end) 98 | 99 | it('errors if an argument is supplied to a field that takes none', function() 100 | expectError(message, [[{ 101 | dog { 102 | name(truncateToLength: 32) 103 | } 104 | }]]) 105 | end) 106 | 107 | it('passes if all argument names match the schema', function() 108 | expectError(nil, [[{ 109 | dog { 110 | doesKnowCommand(dogCommand: SIT) 111 | } 112 | }]]) 113 | end) 114 | end) 115 | 116 | describe('scalarFieldsAreLeaves', function() 117 | local message = 'Scalar values cannot have subselections' 118 | 119 | it('fails if a scalar field has a subselection', function() 120 | expectError(message, '{ dog { name { firstLetter } } }') 121 | end) 122 | 123 | it('passes if all scalar fields are leaves', function() 124 | expectError(nil, '{ dog { name nickname } }') 125 | end) 126 | end) 127 | 128 | describe('compositeFieldsAreNotLeaves', function() 129 | local message = 'Composite types must have subselections' 130 | 131 | it('fails if an object is a leaf', function() 132 | expectError(message, '{ dog }') 133 | end) 134 | 135 | it('fails if an interface is a leaf', function() 136 | expectError(message, '{ pet }') 137 | end) 138 | 139 | it('fails if a union is a leaf', function() 140 | expectError(message, '{ catOrDog }') 141 | end) 142 | 143 | it('passes if all composite types have subselections', function() 144 | expectError(nil, '{ dog { name } pet { } }') 145 | end) 146 | end) 147 | 148 | describe('unambiguousSelections', function() 149 | it('fails if two fields with identical response keys have different types', function() 150 | expectError('Type name mismatch', [[{ 151 | dog { 152 | barkVolume 153 | barkVolume: name 154 | } 155 | }]]) 156 | end) 157 | 158 | it('fails if two fields have different argument sets', function() 159 | expectError('Argument mismatch', [[{ 160 | dog { 161 | doesKnowCommand(dogCommand: SIT) 162 | doesKnowCommand(dogCommand: DOWN) 163 | } 164 | }]]) 165 | end) 166 | 167 | it('passes if fields are identical', function() 168 | expectError(nil, [[{ 169 | dog { 170 | doesKnowCommand(dogCommand: SIT) 171 | doesKnowCommand: doesKnowCommand(dogCommand: SIT) 172 | } 173 | }]]) 174 | end) 175 | end) 176 | 177 | describe('uniqueArgumentNames', function() 178 | local message = 'Encountered multiple arguments named' 179 | 180 | it('fails if a field has two arguments with the same name', function() 181 | expectError(message, [[{ 182 | dog { 183 | doesKnowCommand(dogCommand: SIT, dogCommand: DOWN) 184 | } 185 | }]]) 186 | end) 187 | end) 188 | 189 | describe('argumentsOfCorrectType', function() 190 | it('fails if an argument has an incorrect type', function() 191 | expectError('Expected enum value', [[{ 192 | dog { 193 | doesKnowCommand(dogCommand: 4) 194 | } 195 | }]]) 196 | end) 197 | end) 198 | 199 | describe('requiredArgumentsPresent', function() 200 | local message = 'was not supplied' 201 | 202 | it('fails if a non-null argument is not present', function() 203 | expectError(message, [[{ 204 | dog { 205 | doesKnowCommand 206 | } 207 | }]]) 208 | end) 209 | end) 210 | 211 | describe('uniqueFragmentNames', function() 212 | local message = 'Encountered multiple fragments named' 213 | 214 | it('fails if there are two fragment definitions with the same name', function() 215 | expectError(message, [[ 216 | query { dog { ...nameFragment } } 217 | fragment nameFragment on Dog { name } 218 | fragment nameFragment on Dog { name } 219 | ]]) 220 | end) 221 | 222 | it('passes if all fragment definitions have different names', function() 223 | expectError(nil, [[ 224 | query { dog { ...one ...two } } 225 | fragment one on Dog { name } 226 | fragment two on Dog { name } 227 | ]]) 228 | end) 229 | end) 230 | 231 | describe('fragmentHasValidType', function() 232 | it('fails if a framgent refers to a non-composite type', function() 233 | expectError('Fragment type must be an Object, Interface, or Union', 'fragment f on DogCommand {}') 234 | end) 235 | 236 | it('fails if a fragment refers to a non-existent type', function() 237 | expectError('Fragment refers to non%-existent type', 'fragment f on Hyena {}') 238 | end) 239 | 240 | it('passes if a fragment refers to a composite type', function() 241 | expectError(nil, '{ dog { ...f } } fragment f on Dog {}') 242 | end) 243 | end) 244 | 245 | describe('noUnusedFragments', function() 246 | local message = 'was not used' 247 | 248 | it('fails if a fragment is not used', function() 249 | expectError(message, 'fragment f on Dog {}') 250 | end) 251 | end) 252 | 253 | describe('fragmentSpreadTargetDefined', function() 254 | local message = 'Fragment spread refers to non%-existent' 255 | 256 | it('fails if the fragment does not exist', function() 257 | expectError(message, '{ dog { ...f } }') 258 | end) 259 | end) 260 | 261 | describe('fragmentDefinitionHasNoCycles', function() 262 | local message = 'Fragment definition has cycles' 263 | 264 | it('fails if a fragment spread has cycles', function() 265 | expectError(message, [[ 266 | { dog { ...f } } 267 | fragment f on Dog { ...g } 268 | fragment g on Dog { ...h } 269 | fragment h on Dog { ...f } 270 | ]]) 271 | end) 272 | end) 273 | 274 | describe('fragmentSpreadIsPossible', function() 275 | local message = 'Fragment type condition is not possible' 276 | 277 | it('fails if a fragment type condition refers to a different object than the parent object', function() 278 | expectError(message, [[ 279 | { dog { ...f } } 280 | fragment f on Cat { } 281 | ]]) 282 | end) 283 | 284 | it('fails if a fragment type condition refers to an interface that the parent object does not implement', function() 285 | expectError(message, [[ 286 | { dog { ...f } } 287 | fragment f on Sentient { } 288 | ]]) 289 | end) 290 | 291 | it('fails if a fragment type condition refers to a union that the parent object does not belong to', function() 292 | expectError(message, [[ 293 | { dog { ...f } } 294 | fragment f on HumanOrAlien { } 295 | ]]) 296 | end) 297 | end) 298 | 299 | describe('uniqueInputObjectFields', function() 300 | local message = 'Multiple input object fields named' 301 | 302 | it('fails if an input object has two fields with the same name', function() 303 | expectError(message, [[ 304 | { 305 | dog { 306 | complicatedField(complicatedArgument: {x: "hi", x: "hi"}) 307 | } 308 | } 309 | ]]) 310 | end) 311 | 312 | it('passes if an input object has nested fields with the same name', function() 313 | expectError(nil, [[ 314 | { 315 | dog { 316 | complicatedField(complicatedArgument: {x: "hi", z: {x: "hi"}}) 317 | } 318 | } 319 | ]]) 320 | end) 321 | end) 322 | 323 | describe('directivesAreDefined', function() 324 | local message = 'Unknown directive' 325 | 326 | it('fails if a directive does not exist', function() 327 | expectError(message, 'query @someRandomDirective {}') 328 | end) 329 | 330 | it('passes if directives exists', function() 331 | expectError(nil, 'query @skip {}') 332 | end) 333 | end) 334 | end) 335 | -------------------------------------------------------------------------------- /tests/runner.lua: -------------------------------------------------------------------------------- 1 | lust = require 'tests/lust' 2 | 3 | for _, fn in pairs({'describe', 'it', 'test', 'expect', 'spy', 'before', 'after'}) do 4 | _G[fn] = lust[fn] 5 | end 6 | 7 | local files = { 8 | 'parse', 9 | 'rules' 10 | } 11 | 12 | for i, file in ipairs(files) do 13 | dofile('tests/' .. file .. '.lua') 14 | if next(files, i) then 15 | print() 16 | end 17 | end 18 | 19 | local red = string.char(27) .. '[31m' 20 | local green = string.char(27) .. '[32m' 21 | local normal = string.char(27) .. '[0m' 22 | 23 | if lust.errors > 0 then 24 | io.write(red .. lust.errors .. normal .. ' failed, ') 25 | end 26 | 27 | print(green .. lust.passes .. normal .. ' passed') 28 | 29 | if lust.errors > 0 then os.exit(1) end 30 | --------------------------------------------------------------------------------