Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 777a607

Browse files
committed
Add support for avro-schema unions
(including unions with arrays, maps and records), closes #109
1 parent 6677785 commit 777a607

File tree

6 files changed

+414
-5
lines changed

6 files changed

+414
-5
lines changed

graphql/core/schema.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ function schema:generateTypeMap(node)
5050
node.fields = type(node.fields) == 'function' and node.fields() or node.fields
5151
self.typeMap[node.name] = node
5252

53+
if node.__type == 'Union' then
54+
for _, type in ipairs(node.types) do
55+
self:generateTypeMap(type)
56+
end
57+
end
58+
5359
if node.__type == 'Object' and node.interfaces then
5460
for _, interface in ipairs(node.interfaces) do
5561
self:generateTypeMap(interface)

graphql/tarantool_graphql.lua

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ local function avro_type(avro_schema)
8686
return 'string'
8787
elseif avro_schema == 'string*' then
8888
return 'string*'
89+
elseif avro_schema == 'null' then
90+
return 'null'
8991
end
9092
end
9193
error('unrecognized avro-schema type: ' .. json.encode(avro_schema))
@@ -252,8 +254,10 @@ local function convert_record_fields_to_args(fields, opts)
252254
if not skip_compound or (
253255
avro_t ~= 'record' and avro_t ~= 'record*' and
254256
avro_t ~= 'array' and avro_t ~= 'array*' and
255-
avro_t ~= 'map' and avro_t ~= 'map*') then
256-
local gql_class = gql_argument_type(field.type)
257+
avro_t ~= 'map' and avro_t ~= 'map*' and
258+
avro_t ~= 'union') then
259+
260+
local gql_class = gql_argument_type(field.type, field.name)
257261
args[field.name] = nullable(gql_class)
258262
end
259263
end
@@ -277,7 +281,7 @@ local function convert_record_fields(state, fields)
277281

278282
res[field.name] = {
279283
name = field.name,
280-
kind = gql_type(state, field.type),
284+
kind = gql_type(state, field.type, nil, nil, field.name),
281285
}
282286
end
283287
return res
@@ -670,6 +674,150 @@ local convert_connection_to_field = function(state, connection, collection_name)
670674
end
671675
end
672676

677+
--- The function 'boxes' given GraphQL type into GraphQL Object 'box' type.
678+
--- This serves two needs:
679+
--- 1) GraphQL Union types may contain only GraphQL Objects. GraphQL Scalar
680+
--- types maybe put inside Union only if they are 'boxed' inside an Object.
681+
--- 2) GraphQL Union types must have specific form, described in @{create_gql_union}.
682+
--- 'Boxes' is a part of this specific form.
683+
---
684+
--- @tparam table gql_type GraphQL type to be boxed
685+
--- @param avro_schema table or string ... necessary for right names for boxed
686+
--- types. Right names are such names than ... something about validation
687+
--- @treturn table GraphQL Object. Object name is type of initial GraphQL + '_box'.
688+
--- Resulting Object has only one field. Field type is initial GraphQL type.
689+
--- Field name is `avro_name`. Such name is necessary to make GraphQL response
690+
--- valid
691+
--- Consider the following example:
692+
--- String
693+
--- to
694+
--- String_box {
695+
--- string
696+
--- }
697+
local function box_type(gql_type, avro_name)
698+
check(gql_type, 'gql_type', 'table')
699+
700+
local gql_true_type = nullable(gql_type)
701+
702+
local box_name = gql_true_type.name or gql_true_type.__type
703+
box_name = box_name .. '_box'
704+
705+
local box_fields = {[avro_name] = {name = avro_name, kind = gql_type}}
706+
707+
return types.object({
708+
name = box_name,
709+
description = 'Box (wrapper) around union field',
710+
fields = box_fields,
711+
})
712+
end
713+
714+
--- The functions creates table of GraphQL types from avro-schema union type.
715+
local function create_union_types (avro_schema, state)
716+
check(avro_schema, 'avro_schema', 'table')
717+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array ' ..
718+
', got ' .. yaml.encode(avro_schema))
719+
720+
local union_types = {}
721+
local field_to_type = {}
722+
local is_nullable = false
723+
724+
for _, type in ipairs(avro_schema) do
725+
-- If there is a 'null' type among 'union' types (in avro-schema union)
726+
-- then resulting GraphQL Union type will be nullable
727+
if type == 'null' then
728+
is_nullable = true
729+
else
730+
union_types[#union_types + 1] = box_type(gql_type(state, type),
731+
type.name or avro_type(type))
732+
local determinant = type.name or type.type or type
733+
field_to_type[determinant] = union_types[#union_types]
734+
end
735+
end
736+
737+
return union_types, field_to_type, is_nullable
738+
end
739+
740+
--- The function creates GraphQL Union type from given avro-schema union type.
741+
--- GraphQL responses, received from tarantool graphql instance should be
742+
--- successfully validated with initial avro-schema (which was used to configure
743+
--- this tarantool graphql instance), so GraphQL Union types must have specific
744+
--- formt to be avro-valid. See example in @treturn for details.
745+
---
746+
--- @tparam table state
747+
--- @tparam table avro_schema avro-schema union type
748+
--- @tparam string union_name name for resulting GraphQL Union type
749+
--- @treturn table GraphQL Union type. Consider the following example:
750+
--- Avro-schema (inside a record):
751+
--- ...
752+
--- "name": "MyUnion", "type": [
753+
--- "null",
754+
--- "string",
755+
--- { "type": "record", "name": "Foo", "fields":[
756+
--- { "name": "foo1", "type": "string" },
757+
--- { "name": "foo2", "type": "string" }
758+
--- ]}
759+
--- ]
760+
--- ...
761+
--- GraphQL Union type:
762+
--- MyUnion {
763+
--- ... on String_box {
764+
--- string
765+
--- }
766+
---
767+
--- ... on Foo_box {
768+
--- Foo {
769+
--- foo1
770+
--- foo2
771+
--- }
772+
--- }
773+
local function create_gql_union(state, avro_schema, union_name)
774+
check(avro_schema, 'avro_schema', 'table')
775+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array ' ..
776+
', got ' .. yaml.encode(avro_schema))
777+
778+
-- check avro-schema constraints
779+
for i, type in ipairs(avro_schema) do
780+
assert(avro_type(type) ~= 'union', 'unions must not immediately ' ..
781+
'contain other unions')
782+
783+
if type.name == nil then
784+
for j, another_type in ipairs(avro_schema) do
785+
if i ~= j then
786+
assert(avro_type(type) ~= avro_type(another_type),
787+
'Unions may not contain more than one schema with ' ..
788+
'the same type except for the named types ' ..
789+
'record, fixed and enum')
790+
end
791+
end
792+
end
793+
end
794+
795+
-- create GraphQL union
796+
local union_types, field_to_type, is_nullable =
797+
create_union_types(avro_schema, state)
798+
799+
local union_type = types.union({
800+
types = union_types,
801+
name = union_name,
802+
resolveType = function(result)
803+
for determinant, type in pairs(field_to_type) do
804+
if result[determinant] ~= nil then
805+
return type
806+
end
807+
end
808+
error(('result objects "%s" has no determinant field matching ' ..
809+
'determinants for this union "%s"'):format(yaml.encode(result),
810+
yaml.encode(field_to_type)))
811+
end
812+
})
813+
814+
if not is_nullable then
815+
union_type = types.nonNull(union_type)
816+
end
817+
818+
return union_type
819+
end
820+
673821
--- The function converts passed avro-schema to a GraphQL type.
674822
---
675823
--- @tparam table state for read state.accessor and previously filled
@@ -688,7 +836,7 @@ end
688836
--- XXX As it is not clear now what to do with complex types inside arrays
689837
--- (just pass to results or allow to use filters), only scalar arrays
690838
--- is allowed for now. Note: map is considered scalar.
691-
gql_type = function(state, avro_schema, collection, collection_name)
839+
gql_type = function(state, avro_schema, collection, collection_name, field_name)
692840
assert(type(state) == 'table',
693841
'state must be a table, got ' .. type(state))
694842
assert(avro_schema ~= nil,
@@ -763,6 +911,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
763911

764912
local gql_map = types_map
765913
return avro_t == 'map' and types.nonNull(gql_map) or gql_map
914+
elseif avro_t == 'union' then
915+
return create_gql_union(state, avro_schema, field_name)
766916
else
767917
local res = convert_scalar_type(avro_schema, {raise = false})
768918
if res == nil then

graphql/utils.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,13 @@ function utils.table_size(t)
204204
return count
205205
end
206206

207+
function utils.is_in_array(key, array)
208+
for _, v in ipairs(array) do
209+
if key == v then
210+
return true
211+
end
212+
end
213+
return false
214+
end
215+
207216
return utils

test/local/avro_union.result

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
RESULT
2+
---
3+
user_collection:
4+
- user_id: user_id_0
5+
name: Nobody
6+
- user_id: user_id_1
7+
name: Zlata
8+
stuff:
9+
string: Some string
10+
- user_id: user_id_2
11+
name: Ivan
12+
stuff:
13+
int: 123
14+
- user_id: user_id_3
15+
name: Jane
16+
stuff:
17+
map: {'salary': 333, 'deposit': 444}
18+
- user_id: user_id_4
19+
name: Dan
20+
stuff:
21+
Foo:
22+
foo1: foo1 string
23+
foo2: foo2 string
24+
- user_id: user_id_5
25+
name: Max
26+
stuff:
27+
array:
28+
- {'salary': 'salary string', 'deposit': 'deposit string'}
29+
- {'salary': 'string salary', 'deposit': 'string deposit'}
30+
...
31+
32+
Validating results with initial avro-schema
33+
false
34+
Field stuff missing
35+
---
36+
user_id: user_id_0
37+
name: Nobody
38+
...
39+
40+
true
41+
---
42+
user_id: user_id_1
43+
name: Zlata
44+
stuff:
45+
string: Some string
46+
...
47+
48+
true
49+
---
50+
user_id: user_id_2
51+
name: Ivan
52+
stuff:
53+
int: 123
54+
...
55+
56+
true
57+
---
58+
user_id: user_id_3
59+
name: Jane
60+
stuff:
61+
map: {'salary': 333, 'deposit': 444}
62+
...
63+
64+
true
65+
---
66+
user_id: user_id_4
67+
name: Dan
68+
stuff:
69+
Foo:
70+
foo1: foo1 string
71+
foo2: foo2 string
72+
...
73+
74+
true
75+
---
76+
user_id: user_id_5
77+
name: Max
78+
stuff:
79+
array:
80+
- {'salary': 'salary string', 'deposit': 'deposit string'}
81+
- {'salary': 'string salary', 'deposit': 'string deposit'}
82+
...
83+

0 commit comments

Comments
 (0)