From 914fc8d05a9497aaaa9d4d7cf0b34712bcecce6d Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Sat, 2 Oct 2021 10:52:15 +0300 Subject: [PATCH 1/7] Add coverage tooloing, update test tooling staff and improve coverage --- .github/workflows/test_on_push.yaml | 4 +- .gitignore | 6 +- .luacov | 6 ++ Makefile | 29 +++++++++ graphql/execute.lua | 8 +-- graphql/introspection.lua | 94 ++++++++++++++--------------- graphql/rules.lua | 2 +- graphql/types.lua | 26 ++++---- graphql/validate.lua | 26 ++++---- tmp/.keep | 0 10 files changed, 119 insertions(+), 82 deletions(-) create mode 100644 .luacov create mode 100644 Makefile create mode 100644 tmp/.keep diff --git a/.github/workflows/test_on_push.yaml b/.github/workflows/test_on_push.yaml index 2c423c8..1667c07 100644 --- a/.github/workflows/test_on_push.yaml +++ b/.github/workflows/test_on_push.yaml @@ -22,8 +22,8 @@ jobs: - name: Install dependencies run: | - tarantoolctl rocks install luatest 0.5.2 - tarantoolctl rocks install luacheck 0.25.0 + tarantoolctl rocks install luatest 0.5.5 + tarantoolctl rocks install luacheck 0.26.0 tarantoolctl rocks make - name: Run linter diff --git a/.gitignore b/.gitignore index 9d06d94..24155d7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ .doctrees __pycache__ /dev -/tmp +/tmp/* +!/tmp/.keep doc release release-doc @@ -27,4 +28,5 @@ luacov.*.out* /package-lock.json *.mo .history -.vscode +*.rock + diff --git a/.luacov b/.luacov new file mode 100644 index 0000000..32d8ee5 --- /dev/null +++ b/.luacov @@ -0,0 +1,6 @@ +statsfile = 'tmp/luacov.stats.out' +reportfile = 'tmp/luacov.report.out' +exclude = { + '/test/', + '/tmp/', +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..189751f --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +SHELL := /bin/bash + +.PHONY: .rocks +.rocks: graphql-scm-1.rockspec Makefile + tarantoolctl rocks make + tarantoolctl rocks install luatest 0.5.5 + tarantoolctl rocks install luacov 0.13.0 + tarantoolctl rocks install luacheck 0.26.0 + +.PHONY: lint +lint: + if [ ! -d ".rocks" ]; then make .rocks; fi + .rocks/bin/luacheck . + +.PHONY: test +test: lint + rm -f tmp/luacov* + .rocks/bin/luatest --verbose --coverage --shuffle group + .rocks/bin/luacov . && grep -A999 '^Summary' tmp/luacov.report.out + +.PHONY: clean +clean: + rm -rf .rocks + +.PHONY: build +build: + if [ ! -d ".rocks" ]; then make .rocks; fi + tarantoolctl rocks make + tarantoolctl rocks pack graphql scm-1 diff --git a/graphql/execute.lua b/graphql/execute.lua index 493b157..25bf09d 100644 --- a/graphql/execute.lua +++ b/graphql/execute.lua @@ -167,7 +167,7 @@ local function collectFields(objectType, selections, visitedFragments, result, c end local evaluateSelections -local serializemap = {__serialize='map'} +local serializemap = {__serialize='map',} local function completeValue(fieldType, result, subSelections, context, opts) local fieldName = opts and opts.fieldName or '???' @@ -284,7 +284,7 @@ local function getFieldEntry(objectType, object, fields, context) end end - arguments = setmetatable(arguments, {__index=positions}) + arguments = setmetatable(arguments, {__index=positions,}) local info = { context = context, @@ -307,7 +307,7 @@ local function getFieldEntry(objectType, object, fields, context) local subSelections = mergeSelectionSets(fields) return completeValue(fieldType.kind, resolvedObject, subSelections, context, - {fieldName = fieldName} + {fieldName = fieldName,} ), err end @@ -346,4 +346,4 @@ local function execute(schema, tree, rootValue, variables, operationName) end -return {execute=execute} +return {execute=execute,} diff --git a/graphql/introspection.lua b/graphql/introspection.lua index 69d0ea9..1931e9d 100644 --- a/graphql/introspection.lua +++ b/graphql/introspection.lua @@ -7,11 +7,11 @@ local __Schema, __Directive, __DirectiveLocation, __Type, __Field, __InputValue, local function resolveArgs(field) local function transformArg(arg, name) if arg.__type then - return { kind = arg, name = name } + return { kind = arg, name = name, } elseif arg.name then return arg else - local result = { name = name } + local result = { name = name, } for k, v in pairs(arg) do result[k] = v @@ -39,7 +39,7 @@ __Schema = types.object({ kind = types.nonNull(types.list(types.nonNull(__Type))), resolve = function(schema) return util.values(schema:getTypeMap()) - end + end, }, queryType = { @@ -47,7 +47,7 @@ __Schema = types.object({ kind = __Type.nonNull, resolve = function(schema) return schema:getQueryType() - end + end, }, mutationType = { @@ -55,7 +55,7 @@ __Schema = types.object({ kind = __Type, resolve = function(schema) return schema:getMutationType() - end + end, }, subscriptionType = { @@ -63,7 +63,7 @@ __Schema = types.object({ kind = __Type, resolve = function(_) return nil - end + end, }, @@ -72,7 +72,7 @@ __Schema = types.object({ kind = types.nonNull(types.list(types.nonNull(__Directive))), resolve = function(schema) return schema.directives - end + end, } } end @@ -111,12 +111,12 @@ __Directive = types.object({ if directive.onInlineFragment then table.insert(res, 'INLINE_FRAGMENT') end return res - end + end, }, args = { kind = types.nonNull(types.list(types.nonNull(__InputValue))), - resolve = resolveArgs + resolve = resolveArgs, } } end @@ -133,33 +133,33 @@ __DirectiveLocation = types.enum({ values = { QUERY = { value = 'QUERY', - description = 'Location adjacent to a query operation.' + description = 'Location adjacent to a query operation.', }, MUTATION = { value = 'MUTATION', - description = 'Location adjacent to a mutation operation.' + description = 'Location adjacent to a mutation operation.', }, FIELD = { value = 'FIELD', - description = 'Location adjacent to a field.' + description = 'Location adjacent to a field.', }, FRAGMENT_DEFINITION = { value = 'FRAGMENT_DEFINITION', - description = 'Location adjacent to a fragment definition.' + description = 'Location adjacent to a fragment definition.', }, FRAGMENT_SPREAD = { value = 'FRAGMENT_SPREAD', - description = 'Location adjacent to a fragment spread.' + description = 'Location adjacent to a fragment spread.', }, INLINE_FRAGMENT = { value = 'INLINE_FRAGMENT', - description = 'Location adjacent to an inline fragment.' - } + description = 'Location adjacent to an inline fragment.', + }, } }) @@ -205,7 +205,7 @@ __Type = types.object({ end error('Unknown type ' .. kind) - end + end, }, fields = { @@ -213,7 +213,7 @@ __Type = types.object({ arguments = { includeDeprecated = { kind = types.boolean, - defaultValue = false + defaultValue = false, } }, resolve = function(kind, arguments) @@ -233,7 +233,7 @@ __Type = types.object({ if kind.__type == 'Object' then return kind.interfaces or {} end - end + end, }, possibleTypes = { @@ -242,7 +242,7 @@ __Type = types.object({ if kind.__type == 'Interface' or kind.__type == 'Union' then return context.schema:getPossibleTypes(kind) end - end + end, }, enumValues = { @@ -256,7 +256,7 @@ __Type = types.object({ return arguments.includeDeprecated or not value.deprecationReason end) end - end + end, }, inputFields = { @@ -265,12 +265,12 @@ __Type = types.object({ if kind.__type == 'InputObject' then return util.values(kind.fields) end - end + end, }, ofType = { - kind = __Type - } + kind = __Type, + }, } end }) @@ -290,24 +290,24 @@ __Field = types.object({ args = { kind = types.nonNull(types.list(types.nonNull(__InputValue))), - resolve = resolveArgs + resolve = resolveArgs, }, type = { kind = __Type.nonNull, resolve = function(field) return field.kind - end + end, }, isDeprecated = { kind = types.boolean.nonNull, resolve = function(field) return field.deprecationReason ~= nil - end + end, }, - deprecationReason = types.string + deprecationReason = types.string, } end }) @@ -330,7 +330,7 @@ __InputValue = types.object({ kind = types.nonNull(__Type), resolve = function(field) return field.kind - end + end, }, defaultValue = { @@ -338,8 +338,8 @@ __InputValue = types.object({ description = 'A GraphQL-formatted string representing the default value for this input value.', resolve = function(inputVal) return inputVal.defaultValue and tostring(inputVal.defaultValue) -- TODO improve serialization a lot - end - } + end, + }, } end }) @@ -361,7 +361,7 @@ __EnumValue = types.object({ kind = types.boolean.nonNull, resolve = function(enumValue) return enumValue.deprecationReason ~= nil end }, - deprecationReason = types.string + deprecationReason = types.string, } end }) @@ -372,43 +372,43 @@ __TypeKind = types.enum({ values = { SCALAR = { value = 'SCALAR', - description = 'Indicates this type is a scalar.' + description = 'Indicates this type is a scalar.', }, OBJECT = { value = 'OBJECT', - description = 'Indicates this type is an object. `fields` and `interfaces` are valid fields.' + description = 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', }, INTERFACE = { value = 'INTERFACE', - description = 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.' + description = 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', }, UNION = { value = 'UNION', - description = 'Indicates this type is a union. `possibleTypes` is a valid field.' + description = 'Indicates this type is a union. `possibleTypes` is a valid field.', }, ENUM = { value = 'ENUM', - description = 'Indicates this type is an enum. `enumValues` is a valid field.' + description = 'Indicates this type is an enum. `enumValues` is a valid field.', }, INPUT_OBJECT = { value = 'INPUT_OBJECT', - description = 'Indicates this type is an input object. `inputFields` is a valid field.' + description = 'Indicates this type is an input object. `inputFields` is a valid field.', }, LIST = { value = 'LIST', - description = 'Indicates this type is a list. `ofType` is a valid field.' + description = 'Indicates this type is a list. `ofType` is a valid field.', }, NON_NULL = { value = 'NON_NULL', - description = 'Indicates this type is a non-null. `ofType` is a valid field.' - } + description = 'Indicates this type is a non-null. `ofType` is a valid field.', + }, } }) @@ -419,7 +419,7 @@ local Schema = { arguments = {}, resolve = function(_, _, info) return info.schema - end + end, } local Type = { @@ -427,11 +427,11 @@ local Type = { kind = __Type, description = 'Request the type information of a single type.', arguments = { - name = types.string.nonNull + name = types.string.nonNull, }, resolve = function(_, arguments, info) return info.schema:getType(arguments.name) - end + end, } local TypeName = { @@ -441,7 +441,7 @@ local TypeName = { arguments = {}, resolve = function(_, _, info) return info.parentType.name - end + end, } return { @@ -458,6 +458,6 @@ return { fieldMap = { __schema = Schema, __type = Type, - __typename = TypeName - } + __typename = TypeName, + }, } diff --git a/graphql/rules.lua b/graphql/rules.lua index 184a80d..dee1808 100644 --- a/graphql/rules.lua +++ b/graphql/rules.lua @@ -162,7 +162,7 @@ function rules.unambiguousSelections(node, context) local fieldEntry = { parent = parentType, field = selection, - definition = definition + definition = definition, } validateField(key, fieldEntry) diff --git a/graphql/types.lua b/graphql/types.lua index a9299ad..5aa7993 100644 --- a/graphql/types.lua +++ b/graphql/types.lua @@ -33,7 +33,7 @@ local function initFields(kind, fields) description = field.description, deprecationReason = field.deprecationReason, arguments = field.arguments or {}, - resolve = kind == 'Object' and field.resolve or nil + resolve = kind == 'Object' and field.resolve or nil, } end @@ -45,7 +45,7 @@ function types.nonNull(kind) return { __type = 'NonNull', - ofType = kind + ofType = kind, } end @@ -54,7 +54,7 @@ function types.list(kind) local instance = { __type = 'List', - ofType = kind + ofType = kind, } instance.nonNull = types.nonNull(instance) @@ -123,7 +123,7 @@ function types.object(config) description = config.description, isTypeOf = config.isTypeOf, fields = fields, - interfaces = config.interfaces + interfaces = config.interfaces, } instance.nonNull = types.nonNull(instance) @@ -152,7 +152,7 @@ function types.interface(config) name = config.name, description = config.description, fields = fields, - resolveType = config.resolveType + resolveType = config.resolveType, } instance.nonNull = types.nonNull(instance) @@ -176,7 +176,7 @@ function types.enum(config) name = name, description = entry.description, deprecationReason = entry.deprecationReason, - value = entry.value + value = entry.value, } end @@ -187,7 +187,7 @@ function types.enum(config) values = values, serialize = function(name) return instance.values[name] and instance.values[name].value or name - end + end, } instance.nonNull = types.nonNull(instance) @@ -204,7 +204,7 @@ function types.union(config) local instance = { __type = 'Union', name = config.name, - types = config.types + types = config.types, } instance.nonNull = types.nonNull(instance) @@ -222,7 +222,7 @@ function types.inputObject(config) field = field.__type and { kind = field } or field fields[fieldName] = { name = fieldName, - kind = field.kind + kind = field.kind, } end @@ -230,7 +230,7 @@ function types.inputObject(config) __type = 'InputObject', name = config.name, description = config.description, - fields = fields + fields = fields, } types.get_env(config.schema)[config.name] = instance @@ -418,7 +418,7 @@ function types.directive(config) onField = config.onField, onFragmentDefinition = config.onFragmentDefinition, onFragmentSpread = config.onFragmentSpread, - onInlineFragment = config.onInlineFragment + onInlineFragment = config.onInlineFragment, } return instance @@ -432,7 +432,7 @@ types.include = types.directive({ }, onField = true, onFragmentSpread = true, - onInlineFragment = true + onInlineFragment = true, }) types.skip = types.directive({ @@ -443,7 +443,7 @@ types.skip = types.directive({ }, onField = true, onFragmentSpread = true, - onInlineFragment = true + onInlineFragment = true, }) types.resolve = function(type_name_or_obj, schema) diff --git a/graphql/validate.lua b/graphql/validate.lua index 0bd2db4..c7f8aeb 100644 --- a/graphql/validate.lua +++ b/graphql/validate.lua @@ -48,7 +48,7 @@ local visitors = { end, children = function(node) - return { node.selectionSet } + return { node.selectionSet, } end, rules = { @@ -59,7 +59,7 @@ local visitors = { rules.variableDefaultValuesHaveCorrectType, exit = { rules.variablesAreUsed, - rules.variablesAreDefined + rules.variablesAreDefined, } } }, @@ -69,7 +69,7 @@ local visitors = { return node.selections end, - rules = { rules.unambiguousSelections } + rules = { rules.unambiguousSelections, } }, field = { @@ -120,7 +120,7 @@ local visitors = { rules.argumentsOfCorrectType, rules.requiredArgumentsPresent, rules.directivesAreDefined, - rules.variableUsageAllowed + rules.variableUsageAllowed, } }, @@ -141,14 +141,14 @@ local visitors = { children = function(node, _) if node.selectionSet then - return {node.selectionSet} + return {node.selectionSet,} end end, rules = { rules.fragmentHasValidType, rules.fragmentSpreadIsPossible, - rules.directivesAreDefined + rules.directivesAreDefined, } }, @@ -213,7 +213,7 @@ local visitors = { rules.fragmentSpreadTargetDefined, rules.fragmentSpreadIsPossible, rules.directivesAreDefined, - rules.variableUsageAllowed + rules.variableUsageAllowed, } }, @@ -240,7 +240,7 @@ local visitors = { rules = { rules.fragmentHasValidType, rules.fragmentDefinitionHasNoCycles, - rules.directivesAreDefined + rules.directivesAreDefined, } }, @@ -267,7 +267,7 @@ local visitors = { end) end, - rules = { rules.uniqueInputObjectFields } + rules = { rules.uniqueInputObjectFields, } }, inputObject = { @@ -277,7 +277,7 @@ local visitors = { end) end, - rules = { rules.uniqueInputObjectFields } + rules = { rules.uniqueInputObjectFields, } }, list = { @@ -289,13 +289,13 @@ local visitors = { variable = { enter = function(node, context) context.variableReferences[node.name.value] = true - end + end, }, directive = { children = function(node, _) return node.arguments - end + end, } } @@ -349,4 +349,4 @@ local function validate(schema, tree) return visit(tree) end -return {validate=validate} +return {validate=validate,} diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 From e555789ab473f874c1d777c2ec43353bec4b6980 Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Sat, 2 Oct 2021 14:43:42 +0300 Subject: [PATCH 2/7] Improve return data and errors --- graphql/execute.lua | 7 +++--- test/integration/graphql_test.lua | 39 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/graphql/execute.lua b/graphql/execute.lua index 25bf09d..f966228 100644 --- a/graphql/execute.lua +++ b/graphql/execute.lua @@ -313,7 +313,6 @@ end evaluateSelections = function(objectType, object, selections, context) local result = {} - local errors local err local fields = collectFields(objectType, selections, {}, {}, context) for _, field in ipairs(fields) do @@ -322,14 +321,14 @@ evaluateSelections = function(objectType, object, selections, context) result[field.name], err = getFieldEntry(objectType, object, {field.selection}, context) if err ~= nil then - errors = errors or {} - table.insert(errors, err) + context.errors = context.errors or {} + table.insert(context.errors, err) end if result[field.name] == nil then result[field.name] = box.NULL end end - return result, errors + return result, context.errors end local function execute(schema, tree, rootValue, variables, operationName) diff --git a/test/integration/graphql_test.lua b/test/integration/graphql_test.lua index a4d11e8..0a32613 100644 --- a/test/integration/graphql_test.lua +++ b/test/integration/graphql_test.lua @@ -1158,4 +1158,43 @@ function g.test_both_data_and_error_result() {message = 'Simple error A'}, {message = 'Simple error B'}, }) + + query = [[{ + prefix { + test_A: test(arg: "A") + test_B: test(arg: "B") + } + }]] + + query_schema = { + ['prefix'] = { + kind = { + __type = 'Object', + name = 'prefix', + fields = { + ['test'] = { + kind = types.string.nonNull, + arguments = { + arg = types.string.nonNull, + arg2 = types.string, + arg3 = types.int, + arg4 = types.long, + }, + resolve = callback, + } + }, + }, + arguments = {}, + resolve = function() + return {} + end, + } + } + + data, errors = check_request(query, query_schema) + t.assert_equals(data, {prefix = {test_A = "A", test_B = "B"}}) + t.assert_equals(errors, { + {message = 'Simple error A'}, + {message = 'Simple error B'}, + }) end From 892b627e7511fd7bdb5a593c82bfc81dccfb0ae7 Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Sat, 2 Oct 2021 22:29:40 +0300 Subject: [PATCH 3/7] Add custom directives --- graphql/execute.lua | 37 ++++- graphql/introspection.lua | 93 ++++++++++++- graphql/rules.lua | 5 +- graphql/schema.lua | 20 +++ graphql/types.lua | 17 ++- graphql/util.lua | 51 ++++--- graphql/validate.lua | 16 +-- test/integration/graphql_test.lua | 211 ++++++++++++++++++++++++----- test/integration/introspection.lua | 98 ++++++++++++++ test/unit/graphql_test.lua | 34 ++++- 10 files changed, 504 insertions(+), 78 deletions(-) create mode 100644 test/integration/introspection.lua diff --git a/graphql/execute.lua b/graphql/execute.lua index f966228..d35b1d7 100644 --- a/graphql/execute.lua +++ b/graphql/execute.lua @@ -278,7 +278,7 @@ local function getFieldEntry(objectType, object, fields, context) if argument and argument.value then positions[pos] = { name=argument.name.value, - value=arguments[argument.name.value] + value=arguments[argument.name.value], } pos = pos + 1 end @@ -286,6 +286,40 @@ local function getFieldEntry(objectType, object, fields, context) arguments = setmetatable(arguments, {__index=positions,}) + local directiveMap = {} + for _, directive in ipairs(firstField.directives or {}) do + directiveMap[directive.name.value] = directive + end + + local directives = {} + local directivesDefaultValues = {} + + if next(directiveMap) then + util.map_name(context.schema.directives or {}, function(directive, directive_name) + local supplied_directive = directiveMap[directive_name] + if supplied_directive ~= nil then + local directiveArgumentMap = {} + for _, argument in ipairs(supplied_directive.arguments or {}) do + directiveArgumentMap[argument.name.value] = argument + end + + directives[directive_name] = util.map(directive.arguments or {}, function(argument, name) + local supplied = directiveArgumentMap[name] and directiveArgumentMap[name].value + local defaultValue = argument.defaultValue + if argument.kind then argument = argument.kind end + directivesDefaultValues[directive_name] = directivesDefaultValues[directive_name] or {} + if defaultValue ~= nil then directivesDefaultValues[directive_name][name] = defaultValue end + local res = util.coerceValue(supplied, argument, context.variables, { + strict_non_null = true, + defaultValues = defaultValues, + }) + + return res + end) + end + end) + end + local info = { context = context, fieldName = fieldName, @@ -298,6 +332,7 @@ local function getFieldEntry(objectType, object, fields, context) operation = context.operation, variableValues = context.variables, defaultValues = context.defaultValues, + directives = directives, } local resolvedObject, err = (fieldType.resolve or defaultResolver)(object, arguments, info) diff --git a/graphql/introspection.lua b/graphql/introspection.lua index 1931e9d..d89eef4 100644 --- a/graphql/introspection.lua +++ b/graphql/introspection.lua @@ -109,6 +109,18 @@ __Directive = types.object({ if directive.onFragmentDefinition then table.insert(res, 'FRAGMENT_DEFINITION') end if directive.onFragmentSpread then table.insert(res, 'FRAGMENT_SPREAD') end if directive.onInlineFragment then table.insert(res, 'INLINE_FRAGMENT') end + if directive.onVariableDefinition then table.insert(res, 'VARIABLE_DEFINITION') end + if directive.onSchema then table.insert(res, 'SCHEMA') end + if directive.onScalar then table.insert(res, 'SCALAR') end + if directive.onObject then table.insert(res, 'OBJECT') end + if directive.onFieldDefinition then table.insert(res, 'FIELD_DEFINITION') end + if directive.onArgumentDefinition then table.insert(res, 'ARGUMENT_DEFINITION') end + if directive.onInterface then table.insert(res, 'INTERFACE') end + if directive.onUnion then table.insert(res, 'UNION') end + if directive.onEnum then table.insert(res, 'ENUM') end + if directive.onEnumValue then table.insert(res, 'ENUM_VALUE') end + if directive.onInputObject then table.insert(res, 'INPUT_OBJECT') end + if directive.onInputFieldDefinition then table.insert(res, 'INPUT_FIELD_DEFINITION') end return res end, @@ -117,7 +129,14 @@ __Directive = types.object({ args = { kind = types.nonNull(types.list(types.nonNull(__InputValue))), resolve = resolveArgs, - } + }, + + isRepeatable = { + kind = types.nonNull(types.boolean), + resolve = function(directive) + return directive.isRepeatable == true + end + }, } end }) @@ -160,6 +179,66 @@ __DirectiveLocation = types.enum({ value = 'INLINE_FRAGMENT', description = 'Location adjacent to an inline fragment.', }, + + VARIABLE_DEFINITION = { + value = 'VARIABLE_DEFINITION', + description = 'Location adjacent to a variable definition.', + }, + + SCHEMA = { + value = 'SCHEMA', + description = 'Location adjacent to schema.', + }, + + SCALAR = { + value = 'SCALAR', + description = 'Location adjacent to a scalar.', + }, + + OBJECT = { + value = 'OBJECT', + description = 'Location adjacent to an object.', + }, + + FIELD_DEFINITION = { + value = 'FIELD_DEFINITION', + description = 'Location adjacent to a field definition.', + }, + + ARGUMENT_DEFINITION = { + value = 'ARGUMENT_DEFINITION', + description = 'Location adjacent to an argument definition.', + }, + + INTERFACE = { + value = 'INTERFACE', + description = 'Location adjacent to an interface.', + }, + + UNION = { + value = 'UNION', + description = 'Location adjacent to an union.', + }, + + ENUM = { + value = 'ENUM', + description = 'Location adjacent to an enum.', + }, + + ENUM_VALUE = { + value = 'ENUM_VALUE', + description = 'Location adjacent to an enum value.', + }, + + INPUT_OBJECT = { + value = 'INPUT_OBJECT', + description = 'Location adjacent to an input object.', + }, + + INPUT_FIELD_DEFINITION = { + value = 'INPUT_FIELD_DEFINITION', + description = 'Location adjacent to an input field definition.', + }, } }) @@ -272,7 +351,7 @@ __Type = types.object({ kind = __Type, }, } - end + end, }) __Field = types.object({ @@ -309,7 +388,7 @@ __Field = types.object({ deprecationReason = types.string, } - end + end, }) __InputValue = types.object({ @@ -341,7 +420,7 @@ __InputValue = types.object({ end, }, } - end + end, }) __EnumValue = types.object({ @@ -359,11 +438,11 @@ __EnumValue = types.object({ description = types.string, isDeprecated = { kind = types.boolean.nonNull, - resolve = function(enumValue) return enumValue.deprecationReason ~= nil end + resolve = function(enumValue) return enumValue.deprecationReason ~= nil end, }, deprecationReason = types.string, } - end + end, }) __TypeKind = types.enum({ @@ -409,7 +488,7 @@ __TypeKind = types.enum({ value = 'NON_NULL', description = 'Indicates this type is a non-null. `ofType` is a valid field.', }, - } + }, }) local Schema = { diff --git a/graphql/rules.lua b/graphql/rules.lua index dee1808..089efdb 100644 --- a/graphql/rules.lua +++ b/graphql/rules.lua @@ -146,7 +146,7 @@ function rules.unambiguousSelections(node, context) table.insert(selectionMap[key], entry) else - selectionMap[key] = { entry } + selectionMap[key] = { entry, } end end @@ -314,7 +314,7 @@ function rules.fragmentSpreadIsPossible(node, context) local function getTypes(kind) if kind.__type == 'Object' then - return { [kind] = kind } + return { [kind] = kind, } elseif kind.__type == 'Interface' then return context.schema:getImplementors(kind.name) elseif kind.__type == 'Union' then @@ -332,7 +332,6 @@ function rules.fragmentSpreadIsPossible(node, context) local fragmentTypes = getTypes(fragmentType) local valid = util.find(parentTypes, function(kind) - local kind = kind -- Here is the check that type, mentioned in '... on some_type' -- conditional fragment expression is type of some field of parent object. -- In case of Union parent object and NonNull wrapped inner types diff --git a/graphql/schema.lua b/graphql/schema.lua index e9abb2d..9cffbf8 100644 --- a/graphql/schema.lua +++ b/graphql/schema.lua @@ -99,6 +99,26 @@ end function schema:generateDirectiveMap() for _, directive in ipairs(self.directives) do self.directiveMap[directive.name] = directive + if directive.arguments then + for name, argument in pairs(directive.arguments) do + + -- BEGIN_HACK: resolve type names to real types + if type(argument) == 'string' then + argument = types.resolve(argument, self.name) + directive.arguments[name] = argument + end + + if type(argument.kind) == 'string' then + argument.kind = types.resolve(argument.kind, self.name) + end + -- END_HACK: resolve type names to real types + + local argumentType = argument.__type and argument or argument.kind + assert(argumentType, 'Must supply type for argument "' .. name .. '" on "' .. directive.name .. '"') + argumentType.defaultValue = argument.defaultValue + self:generateTypeMap(argumentType) + end + end end end diff --git a/graphql/types.lua b/graphql/types.lua index 5aa7993..fb73a16 100644 --- a/graphql/types.lua +++ b/graphql/types.lua @@ -419,6 +419,19 @@ function types.directive(config) onFragmentDefinition = config.onFragmentDefinition, onFragmentSpread = config.onFragmentSpread, onInlineFragment = config.onInlineFragment, + onVariableDefinition = config.onVariableDefinition, + onSchema = config.onSchema, + onScalar = config.onScalar, + onObject = config.onObject, + onFieldDefinition = config.onFieldDefinition, + onArgumentDefinition = config.onArgumentDefinition, + onInterface = config.onInterface, + onUnion = config.onUnion, + onEnum = config.onEnum, + onEnumValue = config.onEnumValue, + onInputObject = config.onInputObject, + onInputFieldDefinition = config.onInputFieldDefinition, + isRepeatable = config.isRepeatable or false } return instance @@ -428,7 +441,7 @@ types.include = types.directive({ name = 'include', description = 'Directs the executor to include this field or fragment only when the `if` argument is true.', arguments = { - ['if'] = { kind = types.boolean.nonNull, description = 'Included when true.'} + ['if'] = { kind = types.boolean.nonNull, description = 'Included when true.', }, }, onField = true, onFragmentSpread = true, @@ -439,7 +452,7 @@ types.skip = types.directive({ name = 'skip', description = 'Directs the executor to skip this field or fragment when the `if` argument is true.', arguments = { - ['if'] = { kind = types.boolean.nonNull, description = 'Skipped when true.' } + ['if'] = { kind = types.boolean.nonNull, description = 'Skipped when true.', }, }, onField = true, onFragmentSpread = true, diff --git a/graphql/util.lua b/graphql/util.lua index 20a093b..01a7692 100644 --- a/graphql/util.lua +++ b/graphql/util.lua @@ -11,6 +11,16 @@ local function map(t, fn) return res end +local function map_name(t, fn) + local res = {} + for _, v in ipairs(t or {}) do + if v.name then + res[v.name] = fn(v, v.name) + end + end + return res +end + local function find(t, fn) for k, v in pairs(t) do if fn(v, k) then return v end @@ -169,17 +179,17 @@ local function coerceValue(node, schemaType, variables, opts) end end ---- Check whether passed value has one of listed types. ---- ---- @param obj value to check ---- ---- @tparam string obj_name name of the value to form an error ---- ---- @tparam string type_1 ---- @tparam[opt] string type_2 ---- @tparam[opt] string type_3 ---- ---- @return nothing +-- Check whether passed value has one of listed types. +-- +-- @param obj value to check +-- +-- @tparam string obj_name name of the value to form an error +-- +-- @tparam string type_1 +-- @tparam[opt] string type_2 +-- @tparam[opt] string type_3 +-- +-- @return nothing local function check(obj, obj_name, type_1, type_2, type_3) if type(obj) == type_1 or type(obj) == type_2 or type(obj) == type_3 then return @@ -196,15 +206,15 @@ local function check(obj, obj_name, type_1, type_2, type_3) end end ---- Check whether table is an array. ---- ---- Based on [that][1] implementation. ---- [1]: https://github.com/mpx/lua-cjson/blob/db122676/lua/cjson/util.lua ---- ---- @tparam table table to check ---- @return[1] `true` if passed table is an array (includes the empty table ---- case) ---- @return[2] `false` otherwise +-- Check whether table is an array. +-- +-- Based on [that][1] implementation. +-- [1]: https://github.com/mpx/lua-cjson/blob/db122676/lua/cjson/util.lua +-- +-- @tparam table table to check +-- @return[1] `true` if passed table is an array (includes the empty table +-- case) +-- @return[2] `false` otherwise local function is_array(table) if type(table) ~= 'table' then return false @@ -270,6 +280,7 @@ end return { map = map, + map_name = map_name, find = find, filter = filter, values = values, diff --git a/graphql/validate.lua b/graphql/validate.lua index c7f8aeb..3788755 100644 --- a/graphql/validate.lua +++ b/graphql/validate.lua @@ -31,7 +31,7 @@ local visitors = { return node.definitions end, - rules = { rules.uniqueFragmentNames, exit = { rules.noUnusedFragments } } + rules = { rules.uniqueFragmentNames, exit = { rules.noUnusedFragments, }, }, }, operation = { @@ -69,7 +69,7 @@ local visitors = { return node.selections end, - rules = { rules.unambiguousSelections, } + rules = { rules.unambiguousSelections, }, }, field = { @@ -121,7 +121,7 @@ local visitors = { rules.requiredArgumentsPresent, rules.directivesAreDefined, rules.variableUsageAllowed, - } + }, }, inlineFragment = { @@ -149,7 +149,7 @@ local visitors = { rules.fragmentHasValidType, rules.fragmentSpreadIsPossible, rules.directivesAreDefined, - } + }, }, fragmentSpread = { @@ -214,7 +214,7 @@ local visitors = { rules.fragmentSpreadIsPossible, rules.directivesAreDefined, rules.variableUsageAllowed, - } + }, }, fragmentDefinition = { @@ -241,7 +241,7 @@ local visitors = { rules.fragmentHasValidType, rules.fragmentDefinitionHasNoCycles, rules.directivesAreDefined, - } + }, }, argument = { @@ -267,7 +267,7 @@ local visitors = { end) end, - rules = { rules.uniqueInputObjectFields, } + rules = { rules.uniqueInputObjectFields, }, }, inputObject = { @@ -277,7 +277,7 @@ local visitors = { end) end, - rules = { rules.uniqueInputObjectFields, } + rules = { rules.uniqueInputObjectFields, }, }, list = { diff --git a/test/integration/graphql_test.lua b/test/integration/graphql_test.lua index 0a32613..4279e50 100644 --- a/test/integration/graphql_test.lua +++ b/test/integration/graphql_test.lua @@ -4,21 +4,23 @@ local schema = require('graphql.schema') local parse = require('graphql.parse') local validate = require('graphql.validate') local execute = require('graphql.execute') +local introspection = require('test.integration.introspection') local t = require('luatest') local g = t.group('integration') -local function check_request(query, query_schema, opts) +local function check_request(query, query_schema, mutation_schema, directives, opts) opts = opts or {} local root = { query = types.object({ name = 'Query', - fields = query_schema, + fields = query_schema or {}, }), mutation = types.object({ name = 'Mutation', - fields = {}, + fields = mutation_schema or {}, }), + directives = directives, } local compiled_schema = schema.create(root, 'default') @@ -111,7 +113,7 @@ function g.test_variables() } -- Positive test - t.assert_equals(check_request(query, query_schema, {variables = variables}), {test = 'B22'}) + t.assert_equals(check_request(query, query_schema, nil, nil, {variables = variables}), {test = 'B22'}) -- Negative tests local query = [[ @@ -121,7 +123,7 @@ function g.test_variables() t.assert_error_msg_equals( 'Variable "arg2" expected to be non-null', function() - check_request(query, query_schema, {variables = {}}) + check_request(query, query_schema, nil, nil, {variables = {}}) end ) @@ -134,7 +136,7 @@ function g.test_variables() ' the variable type "String" is not compatible' .. ' with the argument type "NonNull(String)"', function() - check_request(query, query_schema, {variables = {}}) + check_request(query, query_schema, nil, nil, {variables = {}}) end ) @@ -157,7 +159,7 @@ function g.test_variables() function() check_request([[ query { test(arg: "") } - ]], query_schema, { variables = {unknown_arg = ''}}) + ]], query_schema, nil, nil, { variables = {unknown_arg = ''}}) end ) @@ -336,7 +338,7 @@ function g.test_enum_input() query($arg: simple_input_object) { simple_enum(arg: $arg) } - ]], query_schema, {variables = {arg = {field = 'a'}}}), {simple_enum = 'a'}) + ]], query_schema, nil, nil, {variables = {arg = {field = 'a'}}}), {simple_enum = 'a'}) t.assert_error_msg_equals( 'Wrong variable "arg.field" for the Enum "simple_enum" with value "d"', @@ -345,7 +347,7 @@ function g.test_enum_input() query($arg: simple_input_object) { simple_enum(arg: $arg) } - ]], query_schema, {variables = {arg = {field = 'd'}}}) + ]], query_schema, nil, nil, {variables = {arg = {field = 'd'}}}) end ) end @@ -459,7 +461,7 @@ function g.test_nested_input() servers: [{ field: $field }] ) } - ]], query_schema, {variables = {field = 'echo'}}), {test_nested_InputObject = 'echo'}) + ]], query_schema, nil, nil, {variables = {field = 'echo'}}), {test_nested_InputObject = 'echo'}) t.assert_error_msg_equals( 'Unused variable "field"', @@ -470,7 +472,7 @@ function g.test_nested_input() servers: [{ field: "not-variable" }] ) } - ]], query_schema, {variables = {field = 'echo'}}) + ]], query_schema, nil, nil, {variables = {field = 'echo'}}) end ) @@ -480,7 +482,7 @@ function g.test_nested_input() servers: [$field] ) } - ]], query_schema, {variables = {field = 'echo'}}), {test_nested_list = 'echo'}) + ]], query_schema, nil, nil, {variables = {field = 'echo'}}), {test_nested_list = 'echo'}) t.assert_equals(check_request([[ query($field: String! $field2: String! $upvalue: String!) { @@ -492,7 +494,7 @@ function g.test_nested_input() } ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {field = 'echo', field2 = 'field', upvalue = 'upvalue'}, }), {test_nested_InputObject_complex = 'upvalue+field+echo'}) end @@ -599,7 +601,7 @@ function g.test_custom_type_scalar_variables() field: $field ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {field = '{"test": 123}'}, }), {test_json_type = '{"test":123}'}) @@ -609,7 +611,7 @@ function g.test_custom_type_scalar_variables() field: $field ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {field = box.NULL}, }), {test_json_type = 'null'}) @@ -619,7 +621,7 @@ function g.test_custom_type_scalar_variables() field: "null" ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {}, }), {test_json_type = 'null'}) @@ -629,7 +631,7 @@ function g.test_custom_type_scalar_variables() field: $field ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {field = 'echo'}, }), {test_custom_type_scalar = 'echo'}) @@ -644,7 +646,7 @@ function g.test_custom_type_scalar_variables() field: $field ) } - ]], query_schema, {variables = {field = 'echo'}}) + ]], query_schema, nil, nil, {variables = {field = 'echo'}}) end ) @@ -654,7 +656,7 @@ function g.test_custom_type_scalar_variables() fields: [$field] ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {field = 'echo'}, }), {test_custom_type_scalar_list = 'echo'}) @@ -683,7 +685,7 @@ function g.test_custom_type_scalar_variables() fields: [$field] ) } - ]], query_schema, {variables = {field = 'echo'}}) + ]], query_schema, nil, nil, {variables = {field = 'echo'}}) end ) @@ -693,7 +695,7 @@ function g.test_custom_type_scalar_variables() fields: $fields ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {fields = {'echo'}}, }), {test_custom_type_scalar_list = 'echo'}) @@ -708,7 +710,7 @@ function g.test_custom_type_scalar_variables() fields: $fields ) } - ]], query_schema, {variables = {fields = {'echo'}}}) + ]], query_schema, nil, nil, {variables = {fields = {'echo'}}}) end ) @@ -723,7 +725,7 @@ function g.test_custom_type_scalar_variables() fields: $fields ) } - ]], query_schema, {variables = {fields = {'echo'}}}) + ]], query_schema, nil, nil, {variables = {fields = {'echo'}}}) end ) @@ -733,7 +735,7 @@ function g.test_custom_type_scalar_variables() object: { nested_object: { field: $field } } ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {field = 'echo'}, }), {test_custom_type_scalar_inputObject = 'echo'}) @@ -748,7 +750,7 @@ function g.test_custom_type_scalar_variables() object: { nested_object: { field: $field } } ) } - ]], query_schema, {variables = {fields = {'echo'}}}) + ]], query_schema, nil, nil, {variables = {fields = {'echo'}}}) end ) end @@ -960,7 +962,7 @@ function g.test_default_values() query($arg: String = "default_value") { test_default_value(arg: $arg) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {}, }), {test_default_value = 'default_value'}) @@ -968,7 +970,7 @@ function g.test_default_values() query($arg: String = "default_value") { test_default_value(arg: $arg) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {arg = box.NULL}, }), {test_default_value = 'nil'}) @@ -976,7 +978,7 @@ function g.test_default_values() query($arg: [String] = ["default_value"]) { test_default_list(arg: $arg) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {}, }), {test_default_list = 'default_value'}) @@ -984,7 +986,7 @@ function g.test_default_values() query($arg: [String] = ["default_value"]) { test_default_list(arg: $arg) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {arg = box.NULL}, }), {test_default_list = 'nil'}) @@ -992,7 +994,7 @@ function g.test_default_values() query($arg: default_input_object = {field: "default_value"}) { test_default_object(arg: $arg) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {}, }), {test_default_object = 'default_value'}) @@ -1000,7 +1002,7 @@ function g.test_default_values() query($arg: default_input_object = {field: "default_value"}) { test_default_object(arg: $arg) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {arg = box.NULL}, }), {test_default_object = 'nil'}) @@ -1010,7 +1012,7 @@ function g.test_default_values() field: $field ) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {}, }), {test_json_type = '{"test":123}'}) @@ -1018,7 +1020,7 @@ function g.test_default_values() query($arg: String = null, $is_null: Boolean) { test_null(arg: $arg is_null: $is_null) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {arg = 'abc'}, }), {test_null = 'abc'}) @@ -1026,7 +1028,7 @@ function g.test_default_values() query($arg: String = null, $is_null: Boolean) { test_null(arg: $arg is_null: $is_null) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {arg = box.NULL, is_null = true}, }), {test_null = 'is_null'}) @@ -1034,7 +1036,7 @@ function g.test_default_values() query($arg: String = null, $is_null: Boolean) { test_null(arg: $arg is_null: $is_null) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {is_null = false}, }), {test_null = 'not is_null'}) end @@ -1071,7 +1073,7 @@ function g.test_null() query { test_null_nullable(arg: null) } - ]], query_schema, { + ]], query_schema, nil, nil, { variables = {}, }), {test_null_nullable = 'nil'}) @@ -1198,3 +1200,140 @@ function g.test_both_data_and_error_result() {message = 'Simple error B'}, }) end + +function g.test_introspection() + local function callback(_, _) + return nil + end + + local query_schema = { + ['test'] = { + kind = types.string.nonNull, + arguments = { + arg = types.string.nonNull, + arg2 = types.string, + arg3 = types.int, + arg4 = types.long, + }, + resolve = callback, + } + } + + local mutation_schema = { + ['test_mutation'] = { + kind = types.string.nonNull, + arguments = { + arg = types.string.nonNull, + arg2 = types.string, + arg3 = types.int, + arg4 = types.long, + }, + resolve = callback, + } + } + + local directives = { + types.directive({ + name = 'custom', + arguments = {}, + onQuery = true, + onMutation = true, + onField = true, + onFragmentDefinition = true, + onFragmentSpread = true, + onInlineFragment = true, + onVariableDefinition = true, + onSchema = true, + onScalar = true, + onObject = true, + onFieldDefinition = true, + onArgumentDefinition = true, + onInterface = true, + onUnion = true, + onEnum = true, + onEnumValue = true, + onInputObject = true, + onInputFieldDefinition = true, + isRepeatable = true, + }) + } + + local data, errors = check_request(introspection.query, query_schema, mutation_schema, directives) + t.assert_equals(type(data), 'table') + t.assert_equals(errors, nil) +end + +function g.test_custom_directives() + local function callback(_, _, info) + return require('json').encode(info.directives) + end + + local query = [[query TEST($arg: String){ + prefix { + test_A: test(arg: "A")@custom(arg: "a") + test_B: test(arg: "B")@custom(arg: $arg) + } + }]] + + local query_schema = { + ['prefix'] = { + kind = { + __type = 'Object', + name = 'prefix', + fields = { + ['test'] = { + kind = types.string, + arguments = { + arg = types.string.nonNull, + arg2 = types.string, + arg3 = types.int, + arg4 = types.long, + }, + resolve = callback, + } + }, + }, + arguments = {}, + resolve = function() + return {} + end, + } + } + + local directives = { + types.directive({ + name = 'custom', + arguments = { + arg = types.string.nonNull, + }, + onQuery = true, + onMutation = true, + onField = true, + onFragmentDefinition = true, + onFragmentSpread = true, + onInlineFragment = true, + onVariableDefinition = true, + onSchema = true, + onScalar = true, + onObject = true, + onFieldDefinition = true, + onArgumentDefinition = true, + onInterface = true, + onUnion = true, + onEnum = true, + onEnumValue = true, + onInputObject = true, + onInputFieldDefinition = true, + isRepeatable = true, + }) + } + + local data, errors = check_request(query, query_schema, nil, directives, + {variables = {arg = 'echo'}}) + t.assert_equals(data, {prefix = { + test_A = "{\"custom\":{\"arg\":\"a\"}}", + test_B = "{\"custom\":{\"arg\":\"echo\"}}" + } + }) + t.assert_equals(errors, nil) +end diff --git a/test/integration/introspection.lua b/test/integration/introspection.lua new file mode 100644 index 0000000..7829eaa --- /dev/null +++ b/test/integration/introspection.lua @@ -0,0 +1,98 @@ +return { + query = [[ + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + isRepeatable + locations + args { + ...InputValue + } + } + } + } + + fragment FullType on __Type { + kind + name + description + specifiedByUrl + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + } + ]], + variables = {} +} diff --git a/test/unit/graphql_test.lua b/test/unit/graphql_test.lua index 1e10a9a..cbe8708 100644 --- a/test/unit/graphql_test.lua +++ b/test/unit/graphql_test.lua @@ -1,10 +1,11 @@ local t = require('luatest') -local g = t.group() +local g = t.group('unit') local parse = require('graphql.parse').parse local types = require('graphql.types') local schema = require('graphql.schema') local validate = require('graphql.validate').validate +local util = require('graphql.util') function g.test_parse_comments() t.assert_error(parse('{a(b:"#")}').definitions, {}) @@ -1057,3 +1058,34 @@ function g.test_boolean_coerce() t.assert_error_msg_contains('Could not coerce value "value" with type "string" to type boolean', validate, test_schema, parse([[ { test_boolean(value: "value") } ]])) end + +function g.test_util_map_name() + local res = util.map_name(nil, nil) + t.assert_items_equals(res, {}) + res = util.map_name({ { name = 'a' }, { name = 'b' }, }, function(v) return v end) + t.assert_items_equals(res, {a = {name = 'a'}, b = {name = 'b'}}) +end + +function g.test_util_filter() + local res = util.filter({ { name = 'a' }, { name = 'b' }, }, function(v) return v.name == 'a' end) + t.assert_items_equals(res, {{name = 'a'}}) +end + +function g.test_util_values() + local res = util.values({ a = { name = 'a' }, b = { name = 'b' }, }) + t.assert_items_equals(res, {{name = "a"}, {name = "b"}}) +end + +function g.test_util_cmpdeeply() + t.assert_equals(util.cmpdeeply({{{ a = 1}}}, {{{ a = 1}}}), true) + t.assert_equals(util.cmpdeeply({ a = { a = 1 }}, { a = { a = 2 }}), false) + t.assert_equals(util.cmpdeeply({ a = 1 }, { a = 1, b = { b = 3 }}), false) + t.assert_equals(util.cmpdeeply({{{ a = tonumber('nan')}}}, {{{ a = tonumber('nan')}}}), true) + t.assert_equals(util.cmpdeeply('nan', {{{ a = tonumber('nan')}}}), false) +end + +function g.test_util_check() + t.assert_equals(select(2, pcall(util.check, 'a', 'a', nil)), 'a must be a nil, got string') + t.assert_equals(select(2, pcall(util.check, 'a', 'a', nil, 1)), 'a must be a nil or a 1, got string') + t.assert_equals(select(2, pcall(util.check, 'a', 'a', 3, 2, 1)), 'a must be a 3 or a 2r a 1, got string') +end From 93a031ffdb05588c15f522c1d6eda31bc4fa48da Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Sun, 3 Oct 2021 10:05:25 +0300 Subject: [PATCH 4/7] Add Scalar specifiedByUrl field and specifiedByUrl directive --- graphql/introspection.lua | 9 ++++++++ graphql/schema.lua | 3 ++- graphql/types.lua | 10 +++++++++ test/integration/graphql_test.lua | 37 +++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/graphql/introspection.lua b/graphql/introspection.lua index d89eef4..de04dca 100644 --- a/graphql/introspection.lua +++ b/graphql/introspection.lua @@ -347,6 +347,15 @@ __Type = types.object({ end, }, + specifiedByUrl = { + kind = types.string, + resolve = function(kind) + if kind.__type == 'Scalar' then + return kind.specifiedByUrl + end + end, + }, + ofType = { kind = __Type, }, diff --git a/graphql/schema.lua b/graphql/schema.lua index 9cffbf8..4c7de4c 100644 --- a/graphql/schema.lua +++ b/graphql/schema.lua @@ -21,7 +21,8 @@ function schema.create(config, name) self.directives = self.directives or { types.include, - types.skip + types.skip, + types.specifiedByUrl, } self.typeMap = {} diff --git a/graphql/types.lua b/graphql/types.lua index fb73a16..dae2b44 100644 --- a/graphql/types.lua +++ b/graphql/types.lua @@ -97,6 +97,7 @@ function types.scalar(config) parseValue = config.parseValue, parseLiteral = config.parseLiteral, isValueOfTheType = config.isValueOfTheType, + specifiedByUrl = config.specifiedByUrl, } instance.nonNull = types.nonNull(instance) @@ -459,6 +460,15 @@ types.skip = types.directive({ onInlineFragment = true, }) +types.specifiedByUrl = types.directive({ + name = 'specifiedByUrl', + description = 'Custom scalar specification URL.', + arguments = { + ['url'] = { kind = types.string.nonNull, description = 'Scalar specification URL.', } + }, + onScalar = true, +}) + types.resolve = function(type_name_or_obj, schema) if type(type_name_or_obj) == 'table' then return type_name_or_obj diff --git a/test/integration/graphql_test.lua b/test/integration/graphql_test.lua index 4279e50..b1f1dfb 100644 --- a/test/integration/graphql_test.lua +++ b/test/integration/graphql_test.lua @@ -4,6 +4,7 @@ local schema = require('graphql.schema') local parse = require('graphql.parse') local validate = require('graphql.validate') local execute = require('graphql.execute') +local util = require('graphql.util') local introspection = require('test.integration.introspection') local t = require('luatest') @@ -1337,3 +1338,39 @@ function g.test_custom_directives() }) t.assert_equals(errors, nil) end + +function g.test_specifiedByUrl() + local function callback(_, _) + return nil + end + + local custom_scalar = types.scalar({ + name = 'CustomInt', + description = "The `CustomInt` scalar type represents non-fractional signed whole numeric values. " .. + "Int can represent values from -(2^31) to 2^31 - 1, inclusive.", + serialize = function(value) + return value + end, + parseLiteral = function(node) + return node.value + end, + isValueOfTheType = function(_) + return true + end, + specifiedByUrl = 'http://localhost', + }) + + local query_schema = { + ['test'] = { + kind = types.string.nonNull, + arguments = { + arg = custom_scalar, + }, + resolve = callback, + } + } + + local data, errors = check_request(introspection.query, query_schema) + t.assert_equals(tostring(util.map_name(data.__schema.types, function(v) return v end)['CustomInt'].specifiedByUrl), 'http://localhost') + t.assert_equals(errors, nil) +end \ No newline at end of file From 55e5d41ea7e160368388af5ce5f806c1c1c2ce4a Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Sun, 3 Oct 2021 10:11:48 +0300 Subject: [PATCH 5/7] Fix lint warning --- test/integration/graphql_test.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration/graphql_test.lua b/test/integration/graphql_test.lua index b1f1dfb..fcb1986 100644 --- a/test/integration/graphql_test.lua +++ b/test/integration/graphql_test.lua @@ -1371,6 +1371,9 @@ function g.test_specifiedByUrl() } local data, errors = check_request(introspection.query, query_schema) - t.assert_equals(tostring(util.map_name(data.__schema.types, function(v) return v end)['CustomInt'].specifiedByUrl), 'http://localhost') + t.assert_equals( + tostring(util.map_name(data.__schema.types, function(v) return v end)['CustomInt'].specifiedByUrl), + 'http://localhost' + ) t.assert_equals(errors, nil) end \ No newline at end of file From 14ce5e5c6e71b7b4bb6f3e7246faaa29ad353104 Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Sun, 3 Oct 2021 12:02:48 +0300 Subject: [PATCH 6/7] Add allow descriptions everywhere --- graphql/execute.lua | 1 + graphql/rules.lua | 1 + graphql/types.lua | 1 + test/integration/graphql_test.lua | 84 ++++++++++++++++++++++++++++++- 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/graphql/execute.lua b/graphql/execute.lua index d35b1d7..a1decab 100644 --- a/graphql/execute.lua +++ b/graphql/execute.lua @@ -260,6 +260,7 @@ local function getFieldEntry(objectType, object, fields, context) local arguments = util.map(fieldType.arguments or {}, function(argument, name) local supplied = argumentMap[name] and argumentMap[name].value + if argument.kind then argument = argument.kind end return util.coerceValue(supplied, argument, context.variables, { strict_non_null = true, defaultValues = defaultValues, diff --git a/graphql/rules.lua b/graphql/rules.lua index 089efdb..2cd07e2 100644 --- a/graphql/rules.lua +++ b/graphql/rules.lua @@ -515,6 +515,7 @@ local function isVariableTypesValid(argument, argumentType, context, if hasDefault and variableType.__type ~= 'NonNull' then variableType = types.nonNull(variableType) end + if argumentType.kind then argumentType = argumentType.kind end if not isTypeSubTypeOf(variableType, argumentType, context) then return false, ('Variable "%s" type mismatch: the variable type "%s" ' .. diff --git a/graphql/types.lua b/graphql/types.lua index dae2b44..65f97af 100644 --- a/graphql/types.lua +++ b/graphql/types.lua @@ -224,6 +224,7 @@ function types.inputObject(config) fields[fieldName] = { name = fieldName, kind = field.kind, + description = field.description, } end diff --git a/test/integration/graphql_test.lua b/test/integration/graphql_test.lua index fcb1986..5541670 100644 --- a/test/integration/graphql_test.lua +++ b/test/integration/graphql_test.lua @@ -1376,4 +1376,86 @@ function g.test_specifiedByUrl() 'http://localhost' ) t.assert_equals(errors, nil) -end \ No newline at end of file +end + +function g.test_descriptions() + local function callback(_, _) + return nil + end + + local query_schema = { + ['test_query'] = { + kind = types.string.nonNull, + arguments = { + arg = types.string, + arg_described = { + kind = types.object({ + name = 'test_object', + fields = { + object_arg_described = { + kind = types.string, + description = 'object argument' + }, + object_arg = types.string, + }, + kind = types.string, + }), + description = 'described query argument', + } + }, + resolve = callback, + description = 'test query', + } + } + + local mutation_schema = { + ['test_mutation'] = { + kind = types.string.nonNull, + arguments = { + mutation_arg = types.string, + mutation_arg_described = { + kind = types.inputObject({ + name = 'test_input_object', + fields = { + input_object_arg_described = { + kind = types.string, + description = 'input object argument' + }, + input_object_arg = types.string, + }, + kind = types.string, + }), + description = 'described mutation argument', + }, + }, + resolve = callback, + description = 'test mutation', + } + } + + local data, errors = check_request(introspection.query, query_schema, mutation_schema) + t.assert_equals(errors, nil) + + local test_query = util.map_name(data.__schema.types, function(v) return v end)['Query'].fields + t.assert_equals(test_query[1].description, 'test query') + + local arg_described = util.map_name(test_query[1].args, function(v) return v end)['arg_described'] + t.assert_equals(arg_described.description, 'described query argument') + + local test_object = util.map_name(data.__schema.types, function(v) return v end)['test_object'] + local object_arg_described = + util.map_name(test_object.fields, function(v) return v end)['object_arg_described'] + t.assert_equals(object_arg_described.description, 'object argument') + + local test_mutation = util.map_name(data.__schema.types, function(v) return v end)['Mutation'].fields + t.assert_equals(test_mutation[1].description, 'test mutation') + + local mutation_arg_described = + util.map_name(test_mutation[1].args, function(v) return v end)['mutation_arg_described'] + t.assert_equals(mutation_arg_described.description, 'described mutation argument') + + local test_input_object = util.map_name(data.__schema.types, function(v) return v end)['test_input_object'] + local input_object_arg_described = + util.map_name(test_input_object.inputFields, function(v) return v end)['input_object_arg_described'] + t.assert_equals(input_object_arg_described.description, 'input object argument') +end From f3d5386b0fcedd52d2398bb14f5e999488a09e77 Mon Sep 17 00:00:00 2001 From: Yaroslav Shumakov Date: Wed, 17 Nov 2021 20:14:48 +0300 Subject: [PATCH 7/7] Fix arguments descriptions introspection propagation --- graphql/introspection.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/introspection.lua b/graphql/introspection.lua index de04dca..996592b 100644 --- a/graphql/introspection.lua +++ b/graphql/introspection.lua @@ -7,7 +7,7 @@ local __Schema, __Directive, __DirectiveLocation, __Type, __Field, __InputValue, local function resolveArgs(field) local function transformArg(arg, name) if arg.__type then - return { kind = arg, name = name, } + return { kind = arg, name = name, description = arg.description } elseif arg.name then return arg else