@@ -64,6 +64,8 @@ local function avro_type(avro_schema)
64
64
return ' record*'
65
65
elseif utils .is_array (avro_schema ) then
66
66
return ' union'
67
+ elseif utils .is_array (avro_schema .type ) then
68
+ return ' union'
67
69
elseif avro_schema .type == ' array' then
68
70
return ' array'
69
71
elseif avro_schema .type == ' array*' then
@@ -86,6 +88,8 @@ local function avro_type(avro_schema)
86
88
return ' string'
87
89
elseif avro_schema == ' string*' then
88
90
return ' string*'
91
+ elseif avro_schema == ' null' then
92
+ return ' null'
89
93
end
90
94
end
91
95
error (' unrecognized avro-schema type: ' .. json .encode (avro_schema ))
@@ -252,8 +256,10 @@ local function convert_record_fields_to_args(fields, opts)
252
256
if not skip_compound or (
253
257
avro_t ~= ' record' and avro_t ~= ' record*' and
254
258
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 )
259
+ avro_t ~= ' map' and avro_t ~= ' map*' and
260
+ avro_t ~= ' union' ) then
261
+
262
+ local gql_class = gql_argument_type (field .type , field .name )
257
263
args [field .name ] = nullable (gql_class )
258
264
end
259
265
end
@@ -275,10 +281,18 @@ local function convert_record_fields(state, fields)
275
281
(' field.name must be a string, got %s (schema %s)' )
276
282
:format (type (field .name ), json .encode (field )))
277
283
278
- res [field .name ] = {
279
- name = field .name ,
280
- kind = gql_type (state , field .type ),
281
- }
284
+ -- union case
285
+ if utils .is_array (field .type ) then
286
+ res [field .name ] = {
287
+ name = field .name ,
288
+ kind = gql_type (state , {name = field .name , type = field .type }),
289
+ }
290
+ else
291
+ res [field .name ] = {
292
+ name = field .name ,
293
+ kind = gql_type (state , field .type ),
294
+ }
295
+ end
282
296
end
283
297
return res
284
298
end
@@ -670,6 +684,150 @@ local convert_connection_to_field = function(state, connection, collection_name)
670
684
end
671
685
end
672
686
687
+ --- The function 'boxes' given GraphQL type into GraphQL Object 'box' type.
688
+ --- This serves two needs:
689
+ --- 1) GraphQL Union types may contain only GraphQL Objects. GraphQL Scalar
690
+ --- types maybe put inside Union only if they are 'boxed' inside an Object.
691
+ --- 2) GraphQL Union types must have specific form, described in @{create_gql_union}.
692
+ --- 'Boxes' is a part of this specific form.
693
+ ---
694
+ --- @tparam table gql_type GraphQL type to be boxed
695
+ --- @param avro_schema table or string ... necessary for right names for boxed
696
+ --- types. Right names are such names than ... something about validation
697
+ --- @treturn table GraphQL Object. Object name is type of initial GraphQL + '_box'.
698
+ --- Resulting Object has only one field. Field type is initial GraphQL type.
699
+ --- Field name is `avro_name`. Such name is necessary to make GraphQL response
700
+ --- valid
701
+ --- Consider the following example:
702
+ --- String
703
+ --- to
704
+ --- String_box {
705
+ --- string
706
+ --- }
707
+ local function box_type (gql_type , avro_name )
708
+ check (gql_type , ' gql_type' , ' table' )
709
+
710
+ local gql_true_type = nullable (gql_type )
711
+
712
+ local box_name = gql_true_type .name or gql_true_type .__type
713
+ box_name = box_name .. ' _box'
714
+
715
+ local box_fields = {[avro_name ] = {name = avro_name , kind = gql_type }}
716
+
717
+ return types .object ({
718
+ name = box_name ,
719
+ description = ' Box (wrapper) around union field' ,
720
+ fields = box_fields ,
721
+ })
722
+ end
723
+
724
+ --- The functions creates table of GraphQL types from avro-schema union type.
725
+ local function create_union_types (avro_schema , state )
726
+ check (avro_schema , ' avro_schema' , ' table' )
727
+ assert (utils .is_array (avro_schema ), ' union avro-schema must be an array ' ..
728
+ ' , got ' .. yaml .encode (avro_schema ))
729
+
730
+ local union_types = {}
731
+ local field_to_type = {}
732
+ local is_nullable = false
733
+
734
+ for _ , type in ipairs (avro_schema ) do
735
+ -- If there is a 'null' type among 'union' types (in avro-schema union)
736
+ -- then resulting GraphQL Union type will be nullable
737
+ if type == ' null' then
738
+ is_nullable = true
739
+ else
740
+ union_types [# union_types + 1 ] = box_type (gql_type (state , type ),
741
+ type .name or avro_type (type ))
742
+ local determinant = type .name or type .type or type
743
+ field_to_type [determinant ] = union_types [# union_types ]
744
+ end
745
+ end
746
+
747
+ return union_types , field_to_type , is_nullable
748
+ end
749
+
750
+ --- The function creates GraphQL Union type from given avro-schema union type.
751
+ --- GraphQL responses, received from tarantool graphql instance should be
752
+ --- successfully validated with initial avro-schema (which was used to configure
753
+ --- this tarantool graphql instance), so GraphQL Union types must have specific
754
+ --- formt to be avro-valid. See example in @treturn for details.
755
+ ---
756
+ --- @tparam table state
757
+ --- @tparam table avro_schema avro-schema union type
758
+ --- @tparam string union_name name for resulting GraphQL Union type
759
+ --- @treturn table GraphQL Union type. Consider the following example:
760
+ --- Avro-schema (inside a record):
761
+ --- ...
762
+ --- "name": "MyUnion", "type": [
763
+ --- "null",
764
+ --- "string",
765
+ --- { "type": "record", "name": "Foo", "fields":[
766
+ --- { "name": "foo1", "type": "string" },
767
+ --- { "name": "foo2", "type": "string" }
768
+ --- ]}
769
+ --- ]
770
+ --- ...
771
+ --- GraphQL Union type:
772
+ --- MyUnion {
773
+ --- ... on String_box {
774
+ --- string
775
+ --- }
776
+ ---
777
+ --- ... on Foo_box {
778
+ --- Foo {
779
+ --- foo1
780
+ --- foo2
781
+ --- }
782
+ --- }
783
+ local function create_gql_union (state , avro_schema , union_name )
784
+ check (avro_schema , ' avro_schema' , ' table' )
785
+ assert (utils .is_array (avro_schema ), ' union avro-schema must be an array ' ..
786
+ ' , got ' .. yaml .encode (avro_schema ))
787
+
788
+ -- check avro-schema constraints
789
+ for i , type in ipairs (avro_schema ) do
790
+ assert (avro_type (type ) ~= ' union' , ' unions must not immediately ' ..
791
+ ' contain other unions' )
792
+
793
+ if type .name == nil then
794
+ for j , another_type in ipairs (avro_schema ) do
795
+ if i ~= j then
796
+ assert (avro_type (type ) ~= avro_type (another_type ),
797
+ ' Unions may not contain more than one schema with ' ..
798
+ ' the same type except for the named types ' ..
799
+ ' record, fixed and enum' )
800
+ end
801
+ end
802
+ end
803
+ end
804
+
805
+ -- create GraphQL union
806
+ local union_types , field_to_type , is_nullable =
807
+ create_union_types (avro_schema , state )
808
+
809
+ local union_type = types .union ({
810
+ types = union_types ,
811
+ name = union_name ,
812
+ resolveType = function (result )
813
+ for determinant , type in pairs (field_to_type ) do
814
+ if result [determinant ] ~= nil then
815
+ return type
816
+ end
817
+ end
818
+ error ((' result objects "%s" has no determinant field matching ' ..
819
+ ' determinants for this union "%s"' ):format (yaml .encode (result ),
820
+ yaml .encode (field_to_type )))
821
+ end
822
+ })
823
+
824
+ if not is_nullable then
825
+ union_type = types .nonNull (union_type )
826
+ end
827
+
828
+ return union_type
829
+ end
830
+
673
831
--- The function converts passed avro-schema to a GraphQL type.
674
832
---
675
833
--- @tparam table state for read state.accessor and previously filled
@@ -763,6 +921,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
763
921
764
922
local gql_map = types_map
765
923
return avro_t == ' map' and types .nonNull (gql_map ) or gql_map
924
+ elseif avro_t == ' union' then
925
+ return create_gql_union (state , avro_schema .type , avro_schema .name )
766
926
else
767
927
local res = convert_scalar_type (avro_schema , {raise = false })
768
928
if res == nil then
0 commit comments