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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module GraphqlGrpc
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -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