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

Commit 1ba583b

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

File tree

6 files changed

+431
-6
lines changed

6 files changed

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

764928
local gql_map = types_map
765929
return avro_t == 'map' and types.nonNull(gql_map) or gql_map
930+
elseif avro_t == 'union' then
931+
return create_gql_union(state, avro_schema, field_name)
766932
else
767933
local res = convert_scalar_type(avro_schema, {raise = false})
768934
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)