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

Commit e08cb5c

Browse files
authored
Merge pull request #115 from tarantool/sb/avro-union
Add support for avro-unions
2 parents 6677785 + 46116a2 commit e08cb5c

File tree

6 files changed

+481
-6
lines changed

6 files changed

+481
-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: 221 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,224 @@ 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+
---
679+
--- @tparam table gql_type GraphQL type to be boxed
680+
--- @tparam string avro_name type (or name, in record case) of avro-schema which
681+
--- was used to create `gql_type`. `avro_name` is used to provide avro-valid names
682+
--- for fields of boxed types
683+
--- @treturn table GraphQL Object
684+
local function box_type(gql_type, avro_name)
685+
check(gql_type, 'gql_type', 'table')
686+
687+
local gql_true_type = nullable(gql_type)
688+
689+
local box_name = gql_true_type.name or gql_true_type.__type
690+
box_name = box_name .. '_box'
691+
692+
local box_fields = {[avro_name] = {name = avro_name, kind = gql_type}}
693+
694+
return types.object({
695+
name = box_name,
696+
description = 'Box (wrapper) around union variant',
697+
fields = box_fields,
698+
})
699+
end
700+
701+
--- The functions creates table of GraphQL types from avro-schema union type.
702+
local function create_union_types(avro_schema, state)
703+
check(avro_schema, 'avro_schema', 'table')
704+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array ' ..
705+
', got\n' .. yaml.encode(avro_schema))
706+
707+
local union_types = {}
708+
local determinant_to_type = {}
709+
local is_nullable = false
710+
711+
for _, type in ipairs(avro_schema) do
712+
-- If there is a 'null' type among 'union' types (in avro-schema union)
713+
-- then resulting GraphQL Union type will be nullable
714+
if type == 'null' then
715+
is_nullable = true
716+
else
717+
local variant_type = gql_type(state, type)
718+
local box_field_name = type.name or avro_type(type)
719+
union_types[#union_types + 1] = box_type(variant_type, box_field_name)
720+
local determinant = type.name or type.type or type
721+
determinant_to_type[determinant] = union_types[#union_types]
722+
end
723+
end
724+
725+
return union_types, determinant_to_type, is_nullable
726+
end
727+
728+
--- The function creates GraphQL Union type from given avro-schema union type.
729+
--- There are two problems with GraphQL Union types, which we solve with specific
730+
--- format of generated Unions. These problems are:
731+
--- 1) GraphQL Unions represent an object that could be one of a list of
732+
--- GraphQL Object types. So Scalars and Lists can not be one of Union types.
733+
--- 2) GraphQL responses, received from tarantool graphql, must be avro-valid.
734+
--- On every incoming GraphQL query a corresponding avro-schema can be generated.
735+
--- Response to this query is 'avro-valid' if it can be successfully validated with
736+
--- this generated (from incoming query) avro-schema.
737+
---
738+
--- Specific format of generated Unions include the following:
739+
---
740+
--- Avro scalar types (e.g. int, string) are converted into GraphQL Object types.
741+
--- Avro scalar converted to GraphQL Scalar (string -> String) and then name of
742+
--- GraphQL type is concatenated with '_box' ('String_box'). Resulting name is a name
743+
--- of created GraphQL Object. This object has only one field with GraphQL type
744+
--- corresponding to avro scalar type (String type in our example). Avro type's
745+
--- name is taken as a name for this single field.
746+
--- [..., "string", ...]
747+
--- turned into
748+
--- MyUnion {
749+
--- ...
750+
--- ... on String_box {
751+
--- string
752+
--- ...
753+
--- }
754+
---
755+
--- Avro arrays and maps are converted into GraphQL Object types. The name of
756+
--- the resulting GraphQL Object is 'List_box' or 'Map_box' respectively. This
757+
--- object has only one field with GraphQL type corresponding to 'items'/'values'
758+
--- avro type. 'array' or 'map' (respectively) is taken as a name of this
759+
--- single field.
760+
--- [..., {"type": "array", "items": "int"}, ...]
761+
--- turned into
762+
--- MyUnion {
763+
--- ...
764+
--- ... on List_box {
765+
--- array
766+
--- ...
767+
--- }
768+
---
769+
--- Avro records are converted into GraphQL Object types. The name of the resulting
770+
--- GraphQL Object is concatenation of record's name and '_box'. This Object
771+
--- has only one field. The name of this field is record's name. The type of this
772+
--- field is GraphQL Object generated from avro record schema in a usual way
773+
--- (see @{gql_type})
774+
---
775+
--- { "type": "record", "name": "Foo", "fields":[
776+
--- { "name": "foo1", "type": "string" },
777+
--- { "name": "foo2", "type": "string" }
778+
--- ]}
779+
--- turned into
780+
--- MyUnion {
781+
--- ...
782+
--- ... on Foo_box {
783+
--- Foo {
784+
--- foo1
785+
--- foo2
786+
--- }
787+
--- ...
788+
--- }
789+
---
790+
--- Please consider full example below.
791+
---
792+
--- @tparam table state
793+
--- @tparam table avro_schema avro-schema union type
794+
--- @tparam string union_name name for resulting GraphQL Union type
795+
--- @treturn table GraphQL Union type. Consider the following example:
796+
--- Avro-schema (inside a record):
797+
--- ...
798+
--- "name": "MyUnion", "type": [
799+
--- "null",
800+
--- "string",
801+
--- { "type": "array", "items": "int" },
802+
--- { "type": "record", "name": "Foo", "fields":[
803+
--- { "name": "foo1", "type": "string" },
804+
--- { "name": "foo2", "type": "string" }
805+
--- ]}
806+
--- ]
807+
--- ...
808+
--- GraphQL Union type (It will be nullable as avro-schema has 'null' variant):
809+
--- MyUnion {
810+
--- ... on String_box {
811+
--- string
812+
--- }
813+
---
814+
--- ... on List_box {
815+
--- array
816+
--- }
817+
---
818+
--- ... on Foo_box {
819+
--- Foo {
820+
--- foo1
821+
--- foo2
822+
--- }
823+
--- }
824+
local function create_gql_union(state, avro_schema, union_name)
825+
check(avro_schema, 'avro_schema', 'table')
826+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array, ' ..
827+
' got ' .. yaml.encode(avro_schema))
828+
829+
-- check avro-schema constraints
830+
for i, type in ipairs(avro_schema) do
831+
assert(avro_type(type) ~= 'union', 'unions must not immediately ' ..
832+
'contain other unions')
833+
834+
if type.name ~= nil then
835+
for j, another_type in ipairs(avro_schema) do
836+
if i ~= j then
837+
if another_type.name ~= nil then
838+
assert(type.name:gsub('%*$', '') ~=
839+
another_type.name:gsub('%*$', ''),
840+
'Unions may not contain more than one schema with ' ..
841+
'the same name')
842+
end
843+
end
844+
end
845+
else
846+
for j, another_type in ipairs(avro_schema) do
847+
if i ~= j then
848+
assert(avro_type(type) ~= avro_type(another_type),
849+
'Unions may not contain more than one schema with ' ..
850+
'the same type except for the named types: ' ..
851+
'record, fixed and enum')
852+
end
853+
end
854+
end
855+
end
856+
857+
-- create GraphQL union
858+
local union_types, determinant_to_type, is_nullable =
859+
create_union_types(avro_schema, state)
860+
861+
local union_type = types.union({
862+
types = union_types,
863+
name = union_name,
864+
resolveType = function(result)
865+
for determinant, type in pairs(determinant_to_type) do
866+
if result[determinant] ~= nil then
867+
return type
868+
end
869+
end
870+
error(('result object has no determinant field matching ' ..
871+
'determinants for this union\nresult object:\n%sdeterminants:\n%s')
872+
:format(yaml.encode(result),
873+
yaml.encode(determinant_to_type)))
874+
end
875+
})
876+
877+
if not is_nullable then
878+
union_type = types.nonNull(union_type)
879+
end
880+
881+
return union_type
882+
end
883+
673884
--- The function converts passed avro-schema to a GraphQL type.
674885
---
675886
--- @tparam table state for read state.accessor and previously filled
676887
--- state.nullable_collection_types (those are gql types)
677888
--- @tparam table avro_schema input avro-schema
678889
--- @tparam[opt] table collection table with schema_name, connections fields
679890
--- described a collection (e.g. tarantool's spaces)
680-
---
891+
--- @tparam[opt] string collection_name name of `collection`
892+
--- @tparam[opt] string field_name it is only for an union generation,
893+
--- because avro-schema union has no name in it and specific name is necessary
894+
--- for GraphQL union
681895
--- If collection is passed, two things are changed within this function:
682896
---
683897
--- 1. Connections from the collection will be taken into account to
@@ -688,7 +902,7 @@ end
688902
--- XXX As it is not clear now what to do with complex types inside arrays
689903
--- (just pass to results or allow to use filters), only scalar arrays
690904
--- is allowed for now. Note: map is considered scalar.
691-
gql_type = function(state, avro_schema, collection, collection_name)
905+
gql_type = function(state, avro_schema, collection, collection_name, field_name)
692906
assert(type(state) == 'table',
693907
'state must be a table, got ' .. type(state))
694908
assert(avro_schema ~= nil,
@@ -763,6 +977,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
763977

764978
local gql_map = types_map
765979
return avro_t == 'map' and types.nonNull(gql_map) or gql_map
980+
elseif avro_t == 'union' then
981+
return create_gql_union(state, avro_schema, field_name)
766982
else
767983
local res = convert_scalar_type(avro_schema, {raise = false})
768984
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)