@@ -86,6 +86,8 @@ local function avro_type(avro_schema)
86
86
return ' string'
87
87
elseif avro_schema == ' string*' then
88
88
return ' string*'
89
+ elseif avro_schema == ' null' then
90
+ return ' null'
89
91
end
90
92
end
91
93
error (' unrecognized avro-schema type: ' .. json .encode (avro_schema ))
@@ -252,8 +254,10 @@ local function convert_record_fields_to_args(fields, opts)
252
254
if not skip_compound or (
253
255
avro_t ~= ' record' and avro_t ~= ' record*' and
254
256
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 )
257
261
args [field .name ] = nullable (gql_class )
258
262
end
259
263
end
@@ -277,7 +281,7 @@ local function convert_record_fields(state, fields)
277
281
278
282
res [field .name ] = {
279
283
name = field .name ,
280
- kind = gql_type (state , field .type ),
284
+ kind = gql_type (state , field .type , nil , nil , field . name ),
281
285
}
282
286
end
283
287
return res
@@ -670,14 +674,224 @@ local convert_connection_to_field = function(state, connection, collection_name)
670
674
end
671
675
end
672
676
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\n result 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
+
673
884
--- The function converts passed avro-schema to a GraphQL type.
674
885
---
675
886
--- @tparam table state for read state.accessor and previously filled
676
887
--- state.nullable_collection_types (those are gql types)
677
888
--- @tparam table avro_schema input avro-schema
678
889
--- @tparam [opt] table collection table with schema_name, connections fields
679
890
--- 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
681
895
--- If collection is passed, two things are changed within this function:
682
896
---
683
897
--- 1. Connections from the collection will be taken into account to
688
902
--- XXX As it is not clear now what to do with complex types inside arrays
689
903
--- (just pass to results or allow to use filters), only scalar arrays
690
904
--- 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 )
692
906
assert (type (state ) == ' table' ,
693
907
' state must be a table, got ' .. type (state ))
694
908
assert (avro_schema ~= nil ,
@@ -763,6 +977,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
763
977
764
978
local gql_map = types_map
765
979
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 )
766
982
else
767
983
local res = convert_scalar_type (avro_schema , {raise = false })
768
984
if res == nil then
0 commit comments