graphql_grpc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +145 -0
- data/Guardfile +75 -0
- data/LICENSE +21 -0
- data/README.md +6 -0
- data/Rakefile +9 -0
- data/bin/console +15 -0
- data/bin/example.rb +66 -0
- data/bin/setup +8 -0
- data/graphql_grpc.gemspec +47 -0
- data/lib/graphql_grpc/arrayify.rb +52 -0
- data/lib/graphql_grpc/function.rb +98 -0
- data/lib/graphql_grpc/graphql.rb +114 -0
- data/lib/graphql_grpc/proxy.rb +105 -0
- data/lib/graphql_grpc/schema.rb +84 -0
- data/lib/graphql_grpc/type_library.rb +232 -0
- data/lib/graphql_grpc/version.rb +3 -0
- data/lib/graphql_grpc.rb +14 -0
- metadata +220 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
# MIT License
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018, Zane Claes
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
require 'graphql'
|
24
|
+
module GraphqlGrpc
|
25
|
+
class Graphql
|
26
|
+
OP_FIELD = ::GraphQL::Language::Nodes::OperationDefinition
|
27
|
+
|
28
|
+
def initialize(proxy, error_presenter)
|
29
|
+
@error_presenter = error_presenter
|
30
|
+
@proxy = proxy
|
31
|
+
@output = { 'data' => {} }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Given a graphQL document, execute those OperationDefinitions which map to a RPC
|
35
|
+
# The document will be modified to not include those fields / selections which were executed.
|
36
|
+
def execute(document, variables = {}, metadata = {})
|
37
|
+
@variables = variables || {}
|
38
|
+
@metadata = metadata || {}
|
39
|
+
document.definitions.reject! do |d|
|
40
|
+
# Filter out fields handled by GRPC...
|
41
|
+
d.selections.reject! { |s| graphql(s) } if d.is_a?(OP_FIELD)
|
42
|
+
d.selections.empty? # Filter the empty operations.
|
43
|
+
end
|
44
|
+
@output
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Execute a GraphQL field as an RPC on the proxy.
|
50
|
+
# @param field [GraphQL::Language::Nodes::GraphqlSelection] the RPC field.
|
51
|
+
# @param block [Proc] the presenter for the error handler.
|
52
|
+
def graphql(field)
|
53
|
+
return false unless @proxy.respond_to?(field.name)
|
54
|
+
|
55
|
+
key = (field.alias || field.name).to_s
|
56
|
+
resp = @proxy.rpc(field.name, vars_from(field), @metadata)
|
57
|
+
@output['data'][key] = present(field, resp)
|
58
|
+
true
|
59
|
+
rescue StandardError => e
|
60
|
+
@output['errors'] ||= []
|
61
|
+
@output['errors'] << @error_presenter.call(e)
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Filter the response down to the selected fields.
|
66
|
+
# n.b., the GRPC server should not include unselected fields, but those fields will appear
|
67
|
+
# as blank/empty in the response unless we actually filter the keys.
|
68
|
+
def present(field, resp)
|
69
|
+
return resp unless field.selections
|
70
|
+
|
71
|
+
if resp.is_a?(Hash)
|
72
|
+
return Time.at(resp[:seconds]).to_datetime if resp.keys == %i[seconds nanos] # TODO: find better way to detect timestamps...
|
73
|
+
|
74
|
+
result = field.selections.each_with_object({}) do |s, out|
|
75
|
+
out[(s.alias || s.name).to_s] = present(s, resp[s.name.to_sym])
|
76
|
+
end
|
77
|
+
result
|
78
|
+
elsif resp.is_a?(Array)
|
79
|
+
resp.map { |r| present(field, r) }
|
80
|
+
else
|
81
|
+
resp
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Extract the variables from a field.
|
86
|
+
def vars_from(field)
|
87
|
+
vars = {}
|
88
|
+
vars[:selections] = graphql_selections_array(field)
|
89
|
+
field.arguments.each do |arg|
|
90
|
+
val = if arg.value.is_a?(::GraphQL::Language::Nodes::VariableIdentifier)
|
91
|
+
@variables[arg.value.name.to_s]
|
92
|
+
elsif arg.value.is_a?(::GraphQL::Language::Nodes::Enum)
|
93
|
+
arg.value.name
|
94
|
+
else
|
95
|
+
arg.value
|
96
|
+
end
|
97
|
+
vars[arg.name] = val
|
98
|
+
end
|
99
|
+
vars
|
100
|
+
end
|
101
|
+
|
102
|
+
# Turn a field into a selections object for the RPC.
|
103
|
+
def graphql_selections_array(field)
|
104
|
+
return nil unless field && field.selections.any?
|
105
|
+
|
106
|
+
field.selections.map do |sel|
|
107
|
+
{
|
108
|
+
name: sel.name,
|
109
|
+
selections: graphql_selections_array(sel)
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# MIT License
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018, Zane Claes
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
require 'active_support/core_ext/string'
|
24
|
+
module GraphqlGrpc
|
25
|
+
class GrpcGatewayError < StandardError; end
|
26
|
+
class HealthError < GrpcGatewayError; end
|
27
|
+
class ConfigurationError < GrpcGatewayError; end
|
28
|
+
class RpcNotFoundError < GrpcGatewayError; end
|
29
|
+
|
30
|
+
class Proxy
|
31
|
+
include GraphqlGrpc::Schema
|
32
|
+
attr_reader :services
|
33
|
+
|
34
|
+
# @param stub_services [Hash] mapping of a service_name to an instance of a stub service.
|
35
|
+
# @param error_presenter [Proc] a method that turns exceptions into a hash.
|
36
|
+
def initialize(stub_services = {}, &block)
|
37
|
+
@function_map = {} # func name => hash containing details
|
38
|
+
@services = {}
|
39
|
+
@error_presenter = block
|
40
|
+
map_functions(stub_services)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return a hash of all the healthchecks from all the services.
|
44
|
+
def healthcheck
|
45
|
+
Hash[@services.map do |service_name, stub|
|
46
|
+
hc = stub.send(:healthcheck, ::Google::Protobuf::Empty.new)
|
47
|
+
raise HealthError, "#{service_name} is not healthy." unless hc && hc.processID > 0
|
48
|
+
|
49
|
+
[service_name, hc]
|
50
|
+
end]
|
51
|
+
end
|
52
|
+
|
53
|
+
def function(function_name, noisy = true)
|
54
|
+
# function_name is a symbol; calling #to_s and #underscore calls #gsub! on it
|
55
|
+
# and it is frozen; so #dup first.
|
56
|
+
func = @function_map[::GRPC::GenericService.underscore(function_name.to_s.dup).to_sym]
|
57
|
+
raise RpcNotFoundError, "#{function_name} does not exist." if noisy && !func
|
58
|
+
|
59
|
+
func
|
60
|
+
end
|
61
|
+
|
62
|
+
def graphql
|
63
|
+
@graphql ||= ::GraphqlGrpc::Graphql.new(self, @error_presenter)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Execute a function with given params.
|
67
|
+
def rpc(function_name, params = {}, metadata = {})
|
68
|
+
function(function_name).call(params, metadata || {})
|
69
|
+
end
|
70
|
+
|
71
|
+
def respond_to_missing?(method, _include_private = false)
|
72
|
+
!!function(method, false)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Proxy methods through to the services, instead of calling rpc()
|
76
|
+
def method_missing(method, *args, &block)
|
77
|
+
return rpc(method, args.first, args[1]) if function(method)
|
78
|
+
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Add to the function_map by inspecting each service for the RPCs it provides.
|
85
|
+
def map_functions(stub_services)
|
86
|
+
return @function_map unless @function_map.empty?
|
87
|
+
|
88
|
+
stub_services.keys.each do |service_name|
|
89
|
+
stub = @services[service_name] = stub_services[service_name]
|
90
|
+
stub.class.to_s.gsub('::Stub', '::Service').constantize.rpc_descs.values.each do |d|
|
91
|
+
next if d.name.to_sym == :Healthcheck
|
92
|
+
|
93
|
+
grpc_func = ::GraphqlGrpc::Function.new(service_name, stub, d)
|
94
|
+
if @function_map.key?(grpc_func.name)
|
95
|
+
sn = @function_map[grpc_func.name].service_name
|
96
|
+
STDERR.puts "Skipping method #{grpc_func.name}; it was already defined on #{sn}"
|
97
|
+
# raise ConfigurationError, "#{grpc_func.name} was already defined on #{sn}."
|
98
|
+
end
|
99
|
+
@function_map[grpc_func.name] = grpc_func
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@function_map
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# MIT License
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018, Dane Avilla
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
module GraphqlGrpc
|
24
|
+
module Schema
|
25
|
+
def gql_mutations
|
26
|
+
# TODO: Find better way to detect mutations
|
27
|
+
@function_map.reject do |name_sym, _rpc_des|
|
28
|
+
name_sym.to_s.start_with?('get') ||
|
29
|
+
_rpc_des.rpc_desc.input == Google::Protobuf::Empty
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def gql_queries
|
34
|
+
# TODO: Find better way to detect queries
|
35
|
+
# Currently look for methods named 'get' or with no args
|
36
|
+
@function_map.select do |name_sym, _rpc_des|
|
37
|
+
name_sym.to_s.start_with?('get') ||
|
38
|
+
_rpc_des.rpc_desc.input == Google::Protobuf::Empty
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_schema_types
|
43
|
+
function_output_types = @function_map.values.map do |function|
|
44
|
+
function.rpc_desc.output
|
45
|
+
end.flatten.uniq
|
46
|
+
output_types = TypeLibrary.new(function_output_types)
|
47
|
+
function_input_types = @function_map.values.map do |function|
|
48
|
+
function.rpc_desc.input
|
49
|
+
end.flatten.uniq
|
50
|
+
input_types = InputTypeLibrary.new(function_input_types)
|
51
|
+
input_types.to_schema_types + "\nscalar Url\n" + output_types.to_schema_types
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_function_types(ggg_function_hash)
|
55
|
+
ggg_function_hash.values.sort_by(&:name).map(&:to_query_type).join("\n ")
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_schema_query
|
59
|
+
"type Query {
|
60
|
+
#{to_function_types(gql_queries)}
|
61
|
+
}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_schema_mutations
|
65
|
+
return '' if gql_mutations.empty?
|
66
|
+
|
67
|
+
"type Mutation {
|
68
|
+
#{to_function_types(gql_mutations)}
|
69
|
+
}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_gql_schema
|
73
|
+
<<EOF
|
74
|
+
#{to_schema_types}
|
75
|
+
#{to_schema_query}
|
76
|
+
#{to_schema_mutations}
|
77
|
+
schema {
|
78
|
+
query: Query
|
79
|
+
#{gql_mutations.empty? ? '' : 'mutation: Mutation'}
|
80
|
+
}
|
81
|
+
EOF
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# MIT License
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018, Dane Avilla
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
module GraphqlGrpc
|
24
|
+
module DescriptorExt
|
25
|
+
def <=>(b)
|
26
|
+
name <=> b.name
|
27
|
+
end
|
28
|
+
|
29
|
+
def types(prefix)
|
30
|
+
# Iterate through the Google::Protobuf::FieldDescriptor list
|
31
|
+
entries.sort.map { |fd| fd.to_gql_type(prefix) }
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Return an array of all (recursive) types known within this type
|
36
|
+
#
|
37
|
+
def sub_types
|
38
|
+
# Iterate through the Google::Protobuf::FieldDescriptor list
|
39
|
+
entries.map do |fd|
|
40
|
+
# fd.name = 'current_entity_to_update'
|
41
|
+
# fd.number = 1
|
42
|
+
# fd.label = :optional
|
43
|
+
# fd.submsg_name = "com.foo.bar.Baz"
|
44
|
+
# fd.subtype = #<Google::Protobuf::Descriptor:0x007fabb3947f08>
|
45
|
+
if fd.subtype.class == Google::Protobuf::Descriptor
|
46
|
+
# There is a subtype; recurse
|
47
|
+
[name, fd.submsg_name] + fd.subtype.sub_types
|
48
|
+
else
|
49
|
+
[name, fd.submsg_name]
|
50
|
+
end
|
51
|
+
end.flatten.compact
|
52
|
+
end
|
53
|
+
|
54
|
+
def type_name
|
55
|
+
name.split('::').last.split('.').last
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Decide whether this is a GraphQL 'type' or 'input'
|
60
|
+
#
|
61
|
+
def input_or_type(prefix)
|
62
|
+
return :input unless prefix.empty?
|
63
|
+
|
64
|
+
:type
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_gql_type(prefix = '')
|
68
|
+
<<EOF
|
69
|
+
#{input_or_type(prefix)} #{prefix}#{type_name} {
|
70
|
+
#{types(prefix).join("\n ")}
|
71
|
+
}
|
72
|
+
EOF
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module FieldDescriptorExt
|
77
|
+
def <=>(b)
|
78
|
+
name <=> b.name
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_gql_type_field(prefix)
|
82
|
+
t = case type
|
83
|
+
when :int64, :int32, :uint32, :uint64
|
84
|
+
'Int'
|
85
|
+
when :string
|
86
|
+
'String'
|
87
|
+
when :bool, :boolean
|
88
|
+
'Boolean'
|
89
|
+
when :double
|
90
|
+
'Float'
|
91
|
+
when :message
|
92
|
+
prefix + submsg_name.to_s.split('.').last
|
93
|
+
when :enum
|
94
|
+
# Enums are interesting; for Google::Protobuf::FieldDescriptor fd
|
95
|
+
# fd.type = :enum
|
96
|
+
# fd.subtype. = Google::Protobuf::EnumDescriptor
|
97
|
+
# fd.submsg_name = 'com.foo.bar.Baz
|
98
|
+
# ed = fd.subtype
|
99
|
+
# ed.entries. = [[:OUT, 0], [:IN, 1]]
|
100
|
+
#
|
101
|
+
prefix + submsg_name.to_s.split('.')[-2..-1].join('_')
|
102
|
+
else
|
103
|
+
type.to_s + '--Unknown'
|
104
|
+
end
|
105
|
+
return "[#{t}]" if repeated?
|
106
|
+
return "#{t}!" unless optional?
|
107
|
+
|
108
|
+
t
|
109
|
+
end
|
110
|
+
|
111
|
+
def optional?
|
112
|
+
label == :optional
|
113
|
+
end
|
114
|
+
|
115
|
+
def repeated?
|
116
|
+
label == :repeated
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_gql_type(prefix)
|
120
|
+
"#{name}: #{to_gql_type_field(prefix)}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
module EnumDescriptorExt
|
125
|
+
def type_name
|
126
|
+
# Take the last 2
|
127
|
+
name.split('.')[-2..-1].join('_')
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_gql_type(prefix)
|
131
|
+
"enum #{prefix}#{type_name} {
|
132
|
+
#{entries.map(&:first).join("\n ")}
|
133
|
+
}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
require 'google/protobuf'
|
139
|
+
Google::Protobuf::Descriptor.include(GraphqlGrpc::DescriptorExt)
|
140
|
+
Google::Protobuf::FieldDescriptor.include(GraphqlGrpc::FieldDescriptorExt)
|
141
|
+
Google::Protobuf::EnumDescriptor.include(GraphqlGrpc::EnumDescriptorExt)
|
142
|
+
|
143
|
+
module GraphqlGrpc
|
144
|
+
class TypeLibrary
|
145
|
+
def initialize(top_level_types)
|
146
|
+
build_descriptors(top_level_types)
|
147
|
+
end
|
148
|
+
|
149
|
+
def build_descriptors(some_types)
|
150
|
+
# Keep track of known types to avoid infinite loops when there
|
151
|
+
# are circular dependencies between gRPC types
|
152
|
+
@descriptors ||= {}
|
153
|
+
some_types.each do |java_class_name|
|
154
|
+
next unless @descriptors[java_class_name].nil?
|
155
|
+
|
156
|
+
# Store a reference to this type
|
157
|
+
descriptor = descriptor_for(java_class_name)
|
158
|
+
@descriptors[java_class_name] ||= descriptor
|
159
|
+
# Recurse
|
160
|
+
build_descriptors(descriptor.sub_types) if descriptor.respond_to?(:sub_types)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# generated_klass - a class created by the 'proto' compiler; maps
|
166
|
+
# to a Descriptor in the generated pool.
|
167
|
+
#
|
168
|
+
def self.descriptor_for(klass_str)
|
169
|
+
klass_str = klass_str.to_s
|
170
|
+
# If given a ruby class reference, convert to "java package" string
|
171
|
+
# Pull the Google::Protobuf::Descriptor out of the pool and return it
|
172
|
+
# with the name
|
173
|
+
Google::Protobuf::DescriptorPool.generated_pool.lookup(
|
174
|
+
ruby_class_to_underscore(klass_str)
|
175
|
+
) || Google::Protobuf::DescriptorPool.generated_pool.lookup(
|
176
|
+
ruby_class_to_dotted(klass_str)
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.ruby_class_to_underscore(klass_str)
|
181
|
+
if klass_str.to_s.include?('::')
|
182
|
+
java_name = klass_str.to_s.split('::')
|
183
|
+
camel_case = java_name.pop
|
184
|
+
java_package = java_name.map(&:underscore)
|
185
|
+
# Put the name back together
|
186
|
+
(java_package + [camel_case]).join('.')
|
187
|
+
else
|
188
|
+
klass_str
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.ruby_class_to_dotted(klass_str)
|
193
|
+
klass_str.gsub('::', '.')
|
194
|
+
end
|
195
|
+
|
196
|
+
def descriptor_for(klass_str)
|
197
|
+
TypeLibrary.descriptor_for(klass_str)
|
198
|
+
end
|
199
|
+
|
200
|
+
def types
|
201
|
+
@descriptors
|
202
|
+
end
|
203
|
+
|
204
|
+
def type_prefix
|
205
|
+
''
|
206
|
+
end
|
207
|
+
|
208
|
+
def to_schema_types
|
209
|
+
@descriptors.values.map do |t|
|
210
|
+
t.to_gql_type(type_prefix)
|
211
|
+
end.compact.sort.uniq.join("\n")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class InputTypeLibrary < TypeLibrary
|
216
|
+
PREFIX = 'i_'.freeze
|
217
|
+
def type_prefix
|
218
|
+
PREFIX
|
219
|
+
end
|
220
|
+
|
221
|
+
def build_descriptors(some_types)
|
222
|
+
super
|
223
|
+
# Edge case: remove any input types with empty sub_types, such
|
224
|
+
# as is the case when a google.protobuf.Empty object is declared
|
225
|
+
# as the argument for a gRPC call that is being mapped to a
|
226
|
+
# GraphQL query.
|
227
|
+
@descriptors.delete_if do |key, descriptor|
|
228
|
+
descriptor.respond_to?(:sub_types) and descriptor.sub_types.empty?
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
data/lib/graphql_grpc.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'grpc'
|
2
|
+
require 'graphql_grpc/version'
|
3
|
+
require 'graphql_grpc/arrayify'
|
4
|
+
require 'graphql_grpc/function'
|
5
|
+
require 'graphql_grpc/graphql'
|
6
|
+
require 'graphql_grpc/schema'
|
7
|
+
require 'graphql_grpc/proxy'
|
8
|
+
require 'graphql_grpc/type_library'
|
9
|
+
|
10
|
+
module GraphqlGrpc
|
11
|
+
# Your code goes here...
|
12
|
+
end
|
13
|
+
|
14
|
+
GraphqlGrpc::Function.include GraphqlGrpc::Arrayify
|