Skip to content

Commit 226ff41

Browse files
no1semanDifferentialOrange
authored andcommitted
Support scalar specification URL
When defining a custom scalar, GraphQL services should provide a scalar specification URL via the @specifiedBy directive or the specifiedByURL introspection field (spec. #3.5 [1]). This patch introduces support of these tools. 1. https://spec.graphql.org/draft/#sec-Scalars.Custom-Scalars Based on PR #21 by @no1seman
1 parent cb448ae commit 226ff41

File tree

6 files changed

+131
-0
lines changed

6 files changed

+131
-0
lines changed

graphql/introspection.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,15 @@ __Type = types.object({
347347
end,
348348
},
349349

350+
specifiedByUrl = {
351+
kind = types.string,
352+
resolve = function(kind)
353+
if kind.__type == 'Scalar' then
354+
return kind.specifiedByUrl
355+
end
356+
end,
357+
},
358+
350359
ofType = {
351360
kind = __Type,
352361
},

graphql/schema.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function schema.create(config, name)
2222
self.directives = self.directives or {
2323
types.include,
2424
types.skip,
25+
types.specifiedBy,
2526
}
2627

2728
self.typeMap = {}

graphql/types.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function types.scalar(config)
9797
parseValue = config.parseValue,
9898
parseLiteral = config.parseLiteral,
9999
isValueOfTheType = config.isValueOfTheType,
100+
specifiedByUrl = config.specifiedByUrl,
100101
}
101102

102103
instance.nonNull = types.nonNull(instance)
@@ -459,6 +460,15 @@ types.skip = types.directive({
459460
onInlineFragment = true,
460461
})
461462

463+
types.specifiedBy = types.directive({
464+
name = 'specifiedBy',
465+
description = 'Custom scalar specification.',
466+
arguments = {
467+
['url'] = { kind = types.string.nonNull, description = 'Scalar specification URL.', }
468+
},
469+
onScalar = true,
470+
})
471+
462472
types.resolve = function(type_name_or_obj, schema)
463473
if type(type_name_or_obj) == 'table' then
464474
return type_name_or_obj

graphql/util.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ local function find(t, fn)
2727
end
2828
end
2929

30+
local function find_by_name(t, name)
31+
for _, v in pairs(t or {}) do
32+
if v.name == name then return v end
33+
end
34+
return nil
35+
end
36+
3037
local function filter(t, fn)
3138
local res = {}
3239
for _,v in pairs(t) do
@@ -282,6 +289,7 @@ return {
282289
map = map,
283290
map_name = map_name,
284291
find = find,
292+
find_by_name = find_by_name,
285293
filter = filter,
286294
values = values,
287295
compose = compose,

test/integration/graphql_test.lua

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local schema = require('graphql.schema')
44
local parse = require('graphql.parse')
55
local validate = require('graphql.validate')
66
local execute = require('graphql.execute')
7+
local util = require('graphql.util')
78
local introspection = require('test.integration.introspection')
89

910
local t = require('luatest')
@@ -1425,3 +1426,86 @@ function g.test_custom_directives()
14251426
t.assert_equals(data, { test_G = '{"num":5,"str":"news"}' })
14261427
t.assert_equals(errors, nil)
14271428
end
1429+
1430+
function g.test_specifiedByUrl_scalar_field()
1431+
local function callback(_, _)
1432+
return nil
1433+
end
1434+
1435+
local custom_scalar = types.scalar({
1436+
name = 'CustomInt',
1437+
description = "The `CustomInt` scalar type represents non-fractional signed whole numeric values. " ..
1438+
"Int can represent values from -(2^31) to 2^31 - 1, inclusive.",
1439+
serialize = function(value)
1440+
return value
1441+
end,
1442+
parseLiteral = function(node)
1443+
return node.value
1444+
end,
1445+
isValueOfTheType = function(_)
1446+
return true
1447+
end,
1448+
specifiedByUrl = 'http://localhost',
1449+
})
1450+
1451+
local query_schema = {
1452+
['test'] = {
1453+
kind = types.string.nonNull,
1454+
arguments = {
1455+
arg = custom_scalar,
1456+
},
1457+
resolve = callback,
1458+
}
1459+
}
1460+
1461+
local data, errors = check_request(introspection.query, query_schema)
1462+
local CustomInt_schema = util.find_by_name(data.__schema.types, 'CustomInt')
1463+
t.assert_type(CustomInt_schema, 'table', 'CustomInt schema found on introspection')
1464+
t.assert_equals(CustomInt_schema.specifiedByUrl, 'http://localhost')
1465+
t.assert_equals(errors, nil)
1466+
end
1467+
1468+
function g.test_specifiedBy_directive()
1469+
local function callback(_, args, info)
1470+
local v = args[1].value
1471+
local dir = info.directives
1472+
if dir ~= nil and dir.specifiedBy ~= nil then
1473+
return { value = v, url = dir.specifiedBy.url }
1474+
end
1475+
1476+
return { value = v }
1477+
end
1478+
1479+
local custom_scalar = types.scalar({
1480+
name = 'CustomInt',
1481+
description = "The `CustomInt` scalar type represents non-fractional signed whole numeric values. " ..
1482+
"Int can represent values from -(2^31) to 2^31 - 1, inclusive.",
1483+
serialize = function(value)
1484+
return value
1485+
end,
1486+
parseLiteral = function(node)
1487+
return node.value
1488+
end,
1489+
isValueOfTheType = function(_)
1490+
return true
1491+
end,
1492+
})
1493+
1494+
local query_schema = {
1495+
['test'] = {
1496+
kind = custom_scalar,
1497+
arguments = {
1498+
arg = custom_scalar,
1499+
},
1500+
resolve = callback,
1501+
}
1502+
}
1503+
1504+
local query = [[query {
1505+
test_A: test(arg: 1)@specifiedBy(url: "http://localhost")
1506+
}]]
1507+
1508+
local data, errors = check_request(query, query_schema)
1509+
t.assert_equals(data, { test_A = { url = "http://localhost", value = "1" } })
1510+
t.assert_equals(errors, nil)
1511+
end

test/unit/graphql_test.lua

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,3 +1069,22 @@ function g.test_util_map_name()
10691069
res = util.map_name({ entry_a = { name = 'a' }, entry_b = { name = 'b' }, }, function(v) return v end)
10701070
t.assert_equals(res, {a = {name = 'a'}, b = {name = 'b'}})
10711071
end
1072+
1073+
function g.test_util_find_by_name()
1074+
local res = util.find_by_name({}, 'var')
1075+
t.assert_equals(res, nil)
1076+
1077+
res = util.find_by_name({ { name = 'avr' } }, 'var')
1078+
t.assert_equals(res, nil)
1079+
1080+
res = util.find_by_name({ { name = 'avr', value = 1 }, { name = 'var', value = 2 } }, 'var')
1081+
t.assert_equals(res, { name = 'var', value = 2 })
1082+
1083+
res = util.find_by_name(
1084+
{
1085+
entry1 = { name = 'avr', value = 1 },
1086+
entry2 = { name = 'var', value = 2 }
1087+
},
1088+
'var')
1089+
t.assert_equals(res, { name = 'var', value = 2 })
1090+
end

0 commit comments

Comments
 (0)