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

Commit bf5f2a5

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

File tree

6 files changed

+432
-6
lines changed

6 files changed

+432
-6
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ make test
107107
* python 2.7,
108108
* virtualenv,
109109
* luacheck,
110-
* >=tarantool/shard-1.1-92-gec1a27e (but < 2.0).
110+
* >=tarantool/shard-1.1-92-gec1a27e (but < 2.0),
111+
* >=tarantool/avro-schema-2.2.2-4-g1145e3e.
111112
* For building apidoc (additionally to 'for use'):
112113
* ldoc.
113114

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: 172 additions & 5 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,14 +674,175 @@ 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 may be 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+
--- @tparam string avro_name type (or name, in record case) of avro-schema which
686+
--- was used to create `gql_type`. `avro_name` is used to provide avro-valid names
687+
--- for fields of boxed types
688+
--- @treturn table GraphQL Object. Object name is type of initial GraphQL + '_box'.
689+
--- Resulting Object has only one field. Field type is initial GraphQL type.
690+
--- Field name is `avro_name`. Such name is necessary to make GraphQL response
691+
--- avro-valid. 'Avro-valid' means the following: on every incoming GraphQL query
692+
--- avro-schema is created (a subset of initial avro-schema, used to configurate
693+
--- tarantool graphql). An 'Avro-valid' is about being valid from point of view of
694+
--- this subset avro-schema, generated for incoming query.
695+
--- Consider the following example of resulting GraphQL Object:
696+
--- String
697+
--- to
698+
--- String_box {
699+
--- string
700+
--- }
701+
local function box_type(gql_type, avro_name)
702+
check(gql_type, 'gql_type', 'table')
703+
704+
local gql_true_type = nullable(gql_type)
705+
706+
local box_name = gql_true_type.name or gql_true_type.__type
707+
box_name = box_name .. '_box'
708+
709+
local box_fields = {[avro_name] = {name = avro_name, kind = gql_type}}
710+
711+
return types.object({
712+
name = box_name,
713+
description = 'Box (wrapper) around union variant',
714+
fields = box_fields,
715+
})
716+
end
717+
718+
--- The functions creates table of GraphQL types from avro-schema union type.
719+
local function create_union_types(avro_schema, state)
720+
check(avro_schema, 'avro_schema', 'table')
721+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array ' ..
722+
', got\n' .. yaml.encode(avro_schema))
723+
724+
local union_types = {}
725+
local determinant_to_type = {}
726+
local is_nullable = false
727+
728+
for _, type in ipairs(avro_schema) do
729+
-- If there is a 'null' type among 'union' types (in avro-schema union)
730+
-- then resulting GraphQL Union type will be nullable
731+
if type == 'null' then
732+
is_nullable = true
733+
else
734+
local variant_type = gql_type(state, type)
735+
local box_field_name = type.name or avro_type(type)
736+
union_types[#union_types + 1] = box_type(variant_type, box_field_name)
737+
local determinant = type.name or type.type or type
738+
determinant_to_type[determinant] = union_types[#union_types]
739+
end
740+
end
741+
742+
return union_types, determinant_to_type, is_nullable
743+
end
744+
745+
--- The function creates GraphQL Union type from given avro-schema union type.
746+
--- GraphQL responses, received from tarantool graphql instance should be
747+
--- successfully validated with initial avro-schema (which was used to configure
748+
--- this tarantool graphql instance), so GraphQL Union types must have specific
749+
--- format to be avro-valid. See example in @treturn for details.
750+
---
751+
--- @tparam table state
752+
--- @tparam table avro_schema avro-schema union type
753+
--- @tparam string union_name name for resulting GraphQL Union type
754+
--- @treturn table GraphQL Union type. Consider the following example:
755+
--- Avro-schema (inside a record):
756+
--- ...
757+
--- "name": "MyUnion", "type": [
758+
--- "null",
759+
--- "string",
760+
--- { "type": "record", "name": "Foo", "fields":[
761+
--- { "name": "foo1", "type": "string" },
762+
--- { "name": "foo2", "type": "string" }
763+
--- ]}
764+
--- ]
765+
--- ...
766+
--- GraphQL Union type (It will be nullable as avro-schema has 'null' variant):
767+
--- MyUnion {
768+
--- ... on String_box {
769+
--- string
770+
--- }
771+
---
772+
--- ... on Foo_box {
773+
--- Foo {
774+
--- foo1
775+
--- foo2
776+
--- }
777+
--- }
778+
local function create_gql_union(state, avro_schema, union_name)
779+
check(avro_schema, 'avro_schema', 'table')
780+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array, ' ..
781+
' got ' .. yaml.encode(avro_schema))
782+
783+
-- check avro-schema constraints
784+
for i, type in ipairs(avro_schema) do
785+
assert(avro_type(type) ~= 'union', 'unions must not immediately ' ..
786+
'contain other unions')
787+
788+
if type.name ~= nil then
789+
for j, another_type in ipairs(avro_schema) do
790+
if i ~= j then
791+
assert(type.name ~= another_type.name,
792+
'Unions may not contain more than one schema with ' ..
793+
'the same name')
794+
end
795+
end
796+
else
797+
for j, another_type in ipairs(avro_schema) do
798+
if i ~= j then
799+
assert(avro_type(type) ~= avro_type(another_type),
800+
'Unions may not contain more than one schema with ' ..
801+
'the same type except for the named types: ' ..
802+
'record, fixed and enum')
803+
end
804+
end
805+
end
806+
end
807+
808+
-- create GraphQL union
809+
local union_types, determinant_to_type, is_nullable =
810+
create_union_types(avro_schema, state)
811+
812+
local union_type = types.union({
813+
types = union_types,
814+
name = union_name,
815+
resolveType = function(result)
816+
for determinant, type in pairs(determinant_to_type) do
817+
if result[determinant] ~= nil then
818+
return type
819+
end
820+
end
821+
error(('result object has no determinant field matching ' ..
822+
'determinants for this union\nresult object:\n%sdeterminants:\n%s')
823+
:format(yaml.encode(result),
824+
yaml.encode(determinant_to_type)))
825+
end
826+
})
827+
828+
if not is_nullable then
829+
union_type = types.nonNull(union_type)
830+
end
831+
832+
return union_type
833+
end
834+
673835
--- The function converts passed avro-schema to a GraphQL type.
674836
---
675837
--- @tparam table state for read state.accessor and previously filled
676838
--- state.nullable_collection_types (those are gql types)
677839
--- @tparam table avro_schema input avro-schema
678840
--- @tparam[opt] table collection table with schema_name, connections fields
679841
--- described a collection (e.g. tarantool's spaces)
680-
---
842+
--- @tparam[opt] string collection_name name of `collection`
843+
--- @tparam[opt] string field_name it is only for an union generation,
844+
--- because avro-schema union has no name in it and specific name is necessary
845+
--- for GraphQL union
681846
--- If collection is passed, two things are changed within this function:
682847
---
683848
--- 1. Connections from the collection will be taken into account to
@@ -688,7 +853,7 @@ end
688853
--- XXX As it is not clear now what to do with complex types inside arrays
689854
--- (just pass to results or allow to use filters), only scalar arrays
690855
--- is allowed for now. Note: map is considered scalar.
691-
gql_type = function(state, avro_schema, collection, collection_name)
856+
gql_type = function(state, avro_schema, collection, collection_name, field_name)
692857
assert(type(state) == 'table',
693858
'state must be a table, got ' .. type(state))
694859
assert(avro_schema ~= nil,
@@ -763,6 +928,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
763928

764929
local gql_map = types_map
765930
return avro_t == 'map' and types.nonNull(gql_map) or gql_map
931+
elseif avro_t == 'union' then
932+
return create_gql_union(state, avro_schema, field_name)
766933
else
767934
local res = convert_scalar_type(avro_schema, {raise = false})
768935
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.value_in(value, array)
208+
for _, v in ipairs(array) do
209+
if value == 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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
true
34+
---
35+
user_id: user_id_0
36+
name: Nobody
37+
...
38+
39+
true
40+
---
41+
user_id: user_id_1
42+
name: Zlata
43+
stuff:
44+
string: Some string
45+
...
46+
47+
true
48+
---
49+
user_id: user_id_2
50+
name: Ivan
51+
stuff:
52+
int: 123
53+
...
54+
55+
true
56+
---
57+
user_id: user_id_3
58+
name: Jane
59+
stuff:
60+
map: {'salary': 333, 'deposit': 444}
61+
...
62+
63+
true
64+
---
65+
user_id: user_id_4
66+
name: Dan
67+
stuff:
68+
Foo:
69+
foo1: foo1 string
70+
foo2: foo2 string
71+
...
72+
73+
true
74+
---
75+
user_id: user_id_5
76+
name: Max
77+
stuff:
78+
array:
79+
- {'salary': 'salary string', 'deposit': 'deposit string'}
80+
- {'salary': 'string salary', 'deposit': 'string deposit'}
81+
...
82+

0 commit comments

Comments
 (0)