grpc-web 1.0.0 → 1.0.1
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 +4 -4
- data/lib/grpc_web.rb +1 -1
- data/lib/grpc_web/client/client.rb +38 -0
- data/lib/grpc_web/{client_executor.rb → client/client_executor.rb} +8 -8
- data/lib/grpc_web/content_types.rb +1 -1
- data/lib/grpc_web/message_frame.rb +24 -21
- data/lib/grpc_web/message_framing.rb +22 -40
- data/lib/grpc_web/{error_callback.rb → server/error_callback.rb} +1 -0
- data/lib/grpc_web/{grpc_request_processor.rb → server/grpc_request_processor.rb} +7 -6
- data/lib/grpc_web/{message_serialization.rb → server/message_serialization.rb} +2 -1
- data/lib/grpc_web/server/rack_app.rb +69 -0
- data/lib/grpc_web/server/rack_handler.rb +79 -0
- data/lib/grpc_web/server/request_framing.rb +28 -0
- data/lib/grpc_web/server/service_class_validator.rb +15 -0
- data/lib/grpc_web/{text_coder.rb → server/text_coder.rb} +1 -0
- data/lib/grpc_web/version.rb +1 -1
- metadata +26 -10
- data/lib/grpc_web/client.rb +0 -35
- data/lib/grpc_web/rack_app.rb +0 -77
- data/lib/grpc_web/rack_handler.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 758ff05af92607617e668f99e32a0fb1e39e8795dd99a4d617e7de4d18a593df
|
4
|
+
data.tar.gz: 0fc70d8bf94ddb52d1a747590682f56750b2c57f2add9f7206cd4e20138bb998
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ccebf90c39a63a496dfb7d5f518f5a7edb31c62b13b62e0fd01d6162a9804c81ee1d7032c56fbc822e1b51988339904471c0017966cbc65ef2ff8aefeeccf59
|
7
|
+
data.tar.gz: ffce3e4c465840373a5c17ca6eff72056ddd86301e3d5743b126f664e7db51ed185fcfe3468d66cf9bd2e0a26697cab4d7a3fa26c1c926f0b7a905c4b5638e21
|
data/lib/grpc_web.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'grpc_web/client/client_executor'
|
5
|
+
|
6
|
+
# GRPC Client implementation
|
7
|
+
# Example usage:
|
8
|
+
#
|
9
|
+
# client = GRPCWeb::Client.new("http://localhost:3000/grpc", HelloService::Service)
|
10
|
+
# client.say_hello(name: 'James')
|
11
|
+
class GRPCWeb::Client
|
12
|
+
attr_reader :base_url, :service_interface
|
13
|
+
|
14
|
+
def initialize(base_url, service_interface)
|
15
|
+
self.base_url = base_url
|
16
|
+
self.service_interface = service_interface
|
17
|
+
|
18
|
+
service_interface.rpc_descs.each do |rpc_method, rpc_desc|
|
19
|
+
define_rpc_method(rpc_method, rpc_desc)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_writer :base_url, :service_interface
|
26
|
+
|
27
|
+
def define_rpc_method(rpc_method, rpc_desc)
|
28
|
+
ruby_method = ::GRPC::GenericService.underscore(rpc_method.to_s).to_sym
|
29
|
+
define_singleton_method(ruby_method) do |params = {}|
|
30
|
+
uri = endpoint_uri(rpc_desc)
|
31
|
+
::GRPCWeb::ClientExecutor.request(uri, rpc_desc, params)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def endpoint_uri(rpc_desc)
|
36
|
+
URI(File.join(base_url, service_interface.service_name, rpc_desc.name.to_s))
|
37
|
+
end
|
38
|
+
end
|
@@ -7,6 +7,7 @@ require 'grpc/errors'
|
|
7
7
|
require 'grpc_web/content_types'
|
8
8
|
require 'grpc_web/message_framing'
|
9
9
|
|
10
|
+
# Client execution concerns
|
10
11
|
module GRPCWeb::ClientExecutor
|
11
12
|
class << self
|
12
13
|
include ::GRPCWeb::ContentTypes
|
@@ -16,11 +17,13 @@ module GRPCWeb::ClientExecutor
|
|
16
17
|
|
17
18
|
def request(uri, rpc_desc, params = {})
|
18
19
|
req_proto = rpc_desc.input.new(params)
|
19
|
-
|
20
|
+
marshalled_proto = rpc_desc.marshal_proc.call(req_proto)
|
21
|
+
frame = ::GRPCWeb::MessageFrame.payload_frame(marshalled_proto)
|
22
|
+
request_body = ::GRPCWeb::MessageFraming.pack_frames([frame])
|
20
23
|
|
21
24
|
resp = post_request(uri, request_body)
|
22
25
|
resp_body = handle_response(resp)
|
23
|
-
rpc_desc.output.
|
26
|
+
rpc_desc.unmarshal_proc(:output).call(resp_body)
|
24
27
|
end
|
25
28
|
|
26
29
|
private
|
@@ -35,12 +38,9 @@ module GRPCWeb::ClientExecutor
|
|
35
38
|
def post_request(uri, request_body)
|
36
39
|
request = Net::HTTP::Post.new(uri, request_headers)
|
37
40
|
request.body = request_body
|
38
|
-
if uri.userinfo
|
39
|
-
request.basic_auth uri.user, uri.password
|
40
|
-
end
|
41
|
+
request.basic_auth uri.user, uri.password if uri.userinfo
|
41
42
|
|
42
|
-
Net::HTTP.start(uri.hostname, uri.port) do |http|
|
43
|
-
http.use_ssl = (uri.scheme == 'https')
|
43
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
44
44
|
http.request(request)
|
45
45
|
end
|
46
46
|
end
|
@@ -50,7 +50,7 @@ module GRPCWeb::ClientExecutor
|
|
50
50
|
raise "Received #{resp.code} #{resp.message} response: #{resp.body}"
|
51
51
|
end
|
52
52
|
|
53
|
-
frames = ::GRPCWeb::MessageFraming.
|
53
|
+
frames = ::GRPCWeb::MessageFraming.unpack_frames(resp.body)
|
54
54
|
header_frame = frames.find(&:header?)
|
55
55
|
headers = parse_headers(header_frame.body) if header_frame
|
56
56
|
raise_on_error(headers)
|
@@ -1,31 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
# Placeholder
|
4
|
+
class GRPCWeb::MessageFrame
|
5
|
+
PAYLOAD_FRAME_TYPE = 0 # String: "\x00"
|
6
|
+
HEADER_FRAME_TYPE = 128 # String: "\x80"
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
def self.payload_frame(body)
|
9
|
+
new(PAYLOAD_FRAME_TYPE, body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.header_frame(body)
|
13
|
+
new(HEADER_FRAME_TYPE, body)
|
14
|
+
end
|
11
15
|
|
12
|
-
|
13
|
-
new(HEADER_FRAME_TYPE, body)
|
14
|
-
end
|
16
|
+
attr_accessor :frame_type, :body
|
15
17
|
|
16
|
-
|
18
|
+
def initialize(frame_type, body)
|
19
|
+
self.frame_type = frame_type
|
20
|
+
self.body = body.b # treat body as a byte string
|
21
|
+
end
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
23
|
+
def payload?
|
24
|
+
frame_type == PAYLOAD_FRAME_TYPE
|
25
|
+
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
27
|
+
def header?
|
28
|
+
frame_type == HEADER_FRAME_TYPE
|
29
|
+
end
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
end
|
31
|
+
def ==(other)
|
32
|
+
frame_type == other.frame_type && body == other.body
|
30
33
|
end
|
31
34
|
end
|
@@ -1,50 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grpc_web/grpc_web_response'
|
4
|
-
require 'grpc_web/grpc_web_request'
|
5
3
|
require 'grpc_web/message_frame'
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
request.service, request.service_method, request.content_type, request.accept, frames,
|
17
|
-
)
|
18
|
-
end
|
19
|
-
|
20
|
-
def frame_response(response)
|
21
|
-
framed = response.body.map do |frame|
|
22
|
-
frame_content(frame.body, frame.frame_type)
|
23
|
-
end.join
|
24
|
-
::GRPCWeb::GRPCWebResponse.new(response.content_type, framed)
|
25
|
-
end
|
26
|
-
|
27
|
-
def frame_content(content, frame_type = ::GRPCWeb::MessageFrame::PAYLOAD_FRAME_TYPE)
|
28
|
-
length_bytes = [content.bytesize].pack('N')
|
29
|
-
"#{frame_type.chr}#{length_bytes}#{content}"
|
30
|
-
end
|
5
|
+
# Placeholder
|
6
|
+
module GRPCWeb::MessageFraming
|
7
|
+
class << self
|
8
|
+
def pack_frames(frames)
|
9
|
+
frames.map do |frame|
|
10
|
+
length_bytes = [frame.body.bytesize].pack('N')
|
11
|
+
"#{frame.frame_type.chr}#{length_bytes}#{frame.body}"
|
12
|
+
end.join
|
13
|
+
end
|
31
14
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
15
|
+
def unpack_frames(content)
|
16
|
+
frames = []
|
17
|
+
remaining_content = content
|
18
|
+
until remaining_content.empty?
|
19
|
+
msg_length = remaining_content[1..4].unpack1('N')
|
20
|
+
raise 'Invalid message length' if msg_length <= 0
|
38
21
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
frames
|
22
|
+
frame_end = 5 + msg_length
|
23
|
+
frames << ::GRPCWeb::MessageFrame.new(
|
24
|
+
remaining_content[0].bytes[0],
|
25
|
+
remaining_content[5...frame_end],
|
26
|
+
)
|
27
|
+
remaining_content = remaining_content[frame_end..-1]
|
47
28
|
end
|
29
|
+
frames
|
48
30
|
end
|
49
31
|
end
|
50
32
|
end
|
@@ -1,19 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'grpc_web/content_types'
|
4
|
-
require 'grpc_web/error_callback'
|
5
4
|
require 'grpc_web/grpc_web_response'
|
6
|
-
require 'grpc_web/
|
7
|
-
require 'grpc_web/
|
8
|
-
require 'grpc_web/
|
5
|
+
require 'grpc_web/server/request_framing'
|
6
|
+
require 'grpc_web/server/error_callback'
|
7
|
+
require 'grpc_web/server/message_serialization'
|
8
|
+
require 'grpc_web/server/text_coder'
|
9
9
|
|
10
|
+
# Placeholder
|
10
11
|
module GRPCWeb::GRPCRequestProcessor
|
11
12
|
class << self
|
12
13
|
include ::GRPCWeb::ContentTypes
|
13
14
|
|
14
15
|
def process(grpc_web_request)
|
15
16
|
text_coder = ::GRPCWeb::TextCoder
|
16
|
-
framing = ::GRPCWeb::
|
17
|
+
framing = ::GRPCWeb::RequestFraming
|
17
18
|
serialization = ::GRPCWeb::MessageSerialization
|
18
19
|
|
19
20
|
grpc_web_request = text_coder.decode_request(grpc_web_request)
|
@@ -41,7 +42,7 @@ module GRPCWeb::GRPCRequestProcessor
|
|
41
42
|
|
42
43
|
# Use Accept header value if specified, otherwise use request content type
|
43
44
|
def response_content_type(request)
|
44
|
-
if
|
45
|
+
if UNSPECIFIED_CONTENT_TYPES.include?(request.accept)
|
45
46
|
request.content_type
|
46
47
|
else
|
47
48
|
request.accept
|
@@ -7,6 +7,7 @@ require 'grpc_web/grpc_web_response'
|
|
7
7
|
require 'grpc_web/grpc_web_request'
|
8
8
|
require 'grpc_web/message_frame'
|
9
9
|
|
10
|
+
# Placeholder
|
10
11
|
module GRPCWeb::MessageSerialization
|
11
12
|
class << self
|
12
13
|
include ::GRPCWeb::ContentTypes
|
@@ -72,7 +73,7 @@ module GRPCWeb::MessageSerialization
|
|
72
73
|
# If needed, trailers can be appended to the response as a 2nd
|
73
74
|
# base64 encoded string with independent framing.
|
74
75
|
def generate_headers(status, message)
|
75
|
-
|
76
|
+
[
|
76
77
|
"grpc-status:#{status}",
|
77
78
|
"grpc-message:#{message}",
|
78
79
|
'x-grpc-web:1',
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/builder'
|
4
|
+
require 'grpc_web/server/rack_handler'
|
5
|
+
require 'grpc_web/server/service_class_validator'
|
6
|
+
|
7
|
+
# Can be given a service class, an instance of a service class, or a
|
8
|
+
# service interface class with a block to lazily initialize the service.
|
9
|
+
#
|
10
|
+
# Example 1:
|
11
|
+
# app.handle(TestHelloService)
|
12
|
+
#
|
13
|
+
# Example 2:
|
14
|
+
# app.handle(TestHelloService.new)
|
15
|
+
#
|
16
|
+
# Example 3:
|
17
|
+
# app.handle(HelloService::Service) do
|
18
|
+
# require 'test_hello_service'
|
19
|
+
# TestHelloService.new
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# TODO: in-mem cache the result of lazy initialization
|
23
|
+
class GRPCWeb::RackApp < ::Rack::Builder
|
24
|
+
def handle(service_or_class, &lazy_init_block)
|
25
|
+
service_class = service_or_class.is_a?(Class) ? service_or_class : service_or_class.class
|
26
|
+
GRPCWeb::ServiceClassValidator.validate(service_class)
|
27
|
+
service_config = lazy_init_block || service_or_class
|
28
|
+
|
29
|
+
service_class.rpc_descs.keys.each do |service_method|
|
30
|
+
add_service_method_to_app(service_class.service_name, service_config, service_method)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Map a path with Rack::Builder corresponding to the service method
|
37
|
+
def add_service_method_to_app(service_name, service_config, service_method)
|
38
|
+
map("/#{service_name}/#{service_method}") do
|
39
|
+
run(RouteHandler.new(service_config, service_method))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Handles a single route
|
44
|
+
class RouteHandler
|
45
|
+
def initialize(service_config, service_method)
|
46
|
+
self.service_config = service_config
|
47
|
+
self.service_method = service_method
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(env)
|
51
|
+
::GRPCWeb::RackHandler.call(service, service_method, env)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_accessor :service_config, :service_method
|
57
|
+
|
58
|
+
def service
|
59
|
+
case service_config
|
60
|
+
when Proc
|
61
|
+
service_config.call
|
62
|
+
when Class
|
63
|
+
service_config.new
|
64
|
+
else
|
65
|
+
service_config
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'google/protobuf'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/request'
|
6
|
+
require 'grpc_web/content_types'
|
7
|
+
require 'grpc_web/grpc_web_request'
|
8
|
+
require 'grpc_web/server/error_callback'
|
9
|
+
require 'grpc_web/server/grpc_request_processor'
|
10
|
+
|
11
|
+
# Placeholder
|
12
|
+
module GRPCWeb::RackHandler
|
13
|
+
NOT_FOUND = 404
|
14
|
+
UNSUPPORTED_MEDIA_TYPE = 415
|
15
|
+
INTERNAL_SERVER_ERROR = 500
|
16
|
+
ACCEPT_HEADER = 'HTTP_ACCEPT'
|
17
|
+
|
18
|
+
class << self
|
19
|
+
include ::GRPCWeb::ContentTypes
|
20
|
+
|
21
|
+
def call(service, service_method, env)
|
22
|
+
rack_request = Rack::Request.new(env)
|
23
|
+
return not_found_response(rack_request.path) unless rack_request.post?
|
24
|
+
return unsupported_media_type_response unless valid_content_types?(rack_request)
|
25
|
+
|
26
|
+
content_type = rack_request.content_type
|
27
|
+
accept = rack_request.get_header(ACCEPT_HEADER)
|
28
|
+
body = rack_request.body.read
|
29
|
+
request = GRPCWeb::GRPCWebRequest.new(service, service_method, content_type, accept, body)
|
30
|
+
response = GRPCWeb::GRPCRequestProcessor.process(request)
|
31
|
+
|
32
|
+
[200, { 'Content-Type' => response.content_type }, [response.body]]
|
33
|
+
rescue Google::Protobuf::ParseError => e
|
34
|
+
invalid_response(e.message)
|
35
|
+
rescue StandardError => e
|
36
|
+
::GRPCWeb.on_error.call(e, service, service_method)
|
37
|
+
error_response
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def valid_content_types?(rack_request)
|
43
|
+
return false unless ALL_CONTENT_TYPES.include?(rack_request.content_type)
|
44
|
+
|
45
|
+
accept = rack_request.get_header(ACCEPT_HEADER)
|
46
|
+
return true if UNSPECIFIED_CONTENT_TYPES.include?(accept)
|
47
|
+
|
48
|
+
ALL_CONTENT_TYPES.include?(accept)
|
49
|
+
end
|
50
|
+
|
51
|
+
def not_found_response(path)
|
52
|
+
[
|
53
|
+
NOT_FOUND,
|
54
|
+
{ 'Content-Type' => 'text/plain', 'X-Cascade' => 'pass' },
|
55
|
+
["Not Found: #{path}"],
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
def unsupported_media_type_response
|
60
|
+
[
|
61
|
+
UNSUPPORTED_MEDIA_TYPE,
|
62
|
+
{ 'Content-Type' => 'text/plain' },
|
63
|
+
['Unsupported Media Type: Invalid Content-Type or Accept header'],
|
64
|
+
]
|
65
|
+
end
|
66
|
+
|
67
|
+
def invalid_response(message)
|
68
|
+
[422, { 'Content-Type' => 'text/plain' }, ["Invalid request format: #{message}"]]
|
69
|
+
end
|
70
|
+
|
71
|
+
def error_response
|
72
|
+
[
|
73
|
+
INTERNAL_SERVER_ERROR,
|
74
|
+
{ 'Content-Type' => 'text/plain' },
|
75
|
+
['Request failed with an unexpected error.'],
|
76
|
+
]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grpc_web/grpc_web_response'
|
4
|
+
require 'grpc_web/grpc_web_request'
|
5
|
+
require 'grpc_web/message_framing'
|
6
|
+
|
7
|
+
# Framing concerns for handling a request on the server
|
8
|
+
module GRPCWeb::RequestFraming
|
9
|
+
class << self
|
10
|
+
def unframe_request(request)
|
11
|
+
frames = message_framing.unpack_frames(request.body)
|
12
|
+
::GRPCWeb::GRPCWebRequest.new(
|
13
|
+
request.service, request.service_method, request.content_type, request.accept, frames,
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def frame_response(response)
|
18
|
+
frames = message_framing.pack_frames(response.body)
|
19
|
+
::GRPCWeb::GRPCWebResponse.new(response.content_type, frames)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def message_framing
|
25
|
+
::GRPCWeb::MessageFraming
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Validates that clazz is a grpc service class
|
4
|
+
module GRPCWeb::ServiceClassValidator
|
5
|
+
def self.validate(clazz)
|
6
|
+
unless clazz.include?(::GRPC::GenericService)
|
7
|
+
raise(ArgumentError, "#{clazz} must 'include GenericService'")
|
8
|
+
end
|
9
|
+
if clazz.rpc_descs.size.zero?
|
10
|
+
raise(ArgumentError, "#{clazz} should specify some rpc descriptions")
|
11
|
+
end
|
12
|
+
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
data/lib/grpc_web/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grpc-web
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Shkolnik
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-02-
|
11
|
+
date: 2020-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: grpc
|
@@ -156,6 +156,20 @@ dependencies:
|
|
156
156
|
- - ">="
|
157
157
|
- !ruby/object:Gem::Version
|
158
158
|
version: '0'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: webmock
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
159
173
|
description: Host gRPC-Web endpoints for Ruby gRPC services in a Rack or Rails app(over
|
160
174
|
HTTP/1.1). Client included.
|
161
175
|
email:
|
@@ -166,19 +180,21 @@ extra_rdoc_files: []
|
|
166
180
|
files:
|
167
181
|
- lib/grpc-web.rb
|
168
182
|
- lib/grpc_web.rb
|
169
|
-
- lib/grpc_web/client.rb
|
170
|
-
- lib/grpc_web/client_executor.rb
|
183
|
+
- lib/grpc_web/client/client.rb
|
184
|
+
- lib/grpc_web/client/client_executor.rb
|
171
185
|
- lib/grpc_web/content_types.rb
|
172
|
-
- lib/grpc_web/error_callback.rb
|
173
|
-
- lib/grpc_web/grpc_request_processor.rb
|
174
186
|
- lib/grpc_web/grpc_web_request.rb
|
175
187
|
- lib/grpc_web/grpc_web_response.rb
|
176
188
|
- lib/grpc_web/message_frame.rb
|
177
189
|
- lib/grpc_web/message_framing.rb
|
178
|
-
- lib/grpc_web/
|
179
|
-
- lib/grpc_web/
|
180
|
-
- lib/grpc_web/
|
181
|
-
- lib/grpc_web/
|
190
|
+
- lib/grpc_web/server/error_callback.rb
|
191
|
+
- lib/grpc_web/server/grpc_request_processor.rb
|
192
|
+
- lib/grpc_web/server/message_serialization.rb
|
193
|
+
- lib/grpc_web/server/rack_app.rb
|
194
|
+
- lib/grpc_web/server/rack_handler.rb
|
195
|
+
- lib/grpc_web/server/request_framing.rb
|
196
|
+
- lib/grpc_web/server/service_class_validator.rb
|
197
|
+
- lib/grpc_web/server/text_coder.rb
|
182
198
|
- lib/grpc_web/version.rb
|
183
199
|
homepage: https://github.com/gusto/grpc-web-ruby
|
184
200
|
licenses:
|
data/lib/grpc_web/client.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'uri'
|
4
|
-
require 'grpc_web/client_executor'
|
5
|
-
|
6
|
-
module GRPCWeb
|
7
|
-
class Client
|
8
|
-
attr_reader :base_url, :service_interface
|
9
|
-
|
10
|
-
def initialize(base_url, service_interface)
|
11
|
-
self.base_url = base_url
|
12
|
-
self.service_interface = service_interface
|
13
|
-
|
14
|
-
service_interface.rpc_descs.each do |rpc_method, rpc_desc|
|
15
|
-
define_rpc_method(rpc_method, rpc_desc)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
attr_writer :base_url, :service_interface
|
22
|
-
|
23
|
-
def define_rpc_method(rpc_method, rpc_desc)
|
24
|
-
ruby_method = ::GRPC::GenericService.underscore(rpc_method.to_s).to_sym
|
25
|
-
define_singleton_method(ruby_method) do |params = {}|
|
26
|
-
uri = endpoint_uri(rpc_desc)
|
27
|
-
::GRPCWeb::ClientExecutor.request(uri, rpc_desc, params)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def endpoint_uri(rpc_desc)
|
32
|
-
URI(File.join(base_url, service_interface.service_name, rpc_desc.name.to_s))
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
data/lib/grpc_web/rack_app.rb
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack/builder'
|
4
|
-
require 'grpc_web/rack_handler'
|
5
|
-
|
6
|
-
module GRPCWeb
|
7
|
-
class RackApp < ::Rack::Builder
|
8
|
-
# Can be given a service class, an instance of a service class, or a
|
9
|
-
# service interface class with a block to lazily initialize the service.
|
10
|
-
#
|
11
|
-
# Example 1:
|
12
|
-
# app.handle(TestHelloService)
|
13
|
-
#
|
14
|
-
# Example 2:
|
15
|
-
# app.handle(TestHelloService.new)
|
16
|
-
#
|
17
|
-
# Example 3:
|
18
|
-
# app.handle(HelloService::Service) do
|
19
|
-
# require 'test_hello_service'
|
20
|
-
# TestHelloService.new
|
21
|
-
# end
|
22
|
-
#
|
23
|
-
def handle(service_or_class, &lazy_init_block)
|
24
|
-
service_class = service_or_class.is_a?(Class) ? service_or_class : service_or_class.class
|
25
|
-
validate_service_class(service_class)
|
26
|
-
service_config = lazy_init_block || service_or_class
|
27
|
-
|
28
|
-
service_class.rpc_descs.keys.each do |service_method|
|
29
|
-
add_service_method_to_app(service_class.service_name, service_config, service_method)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def validate_service_class(clazz)
|
36
|
-
unless clazz.include?(::GRPC::GenericService)
|
37
|
-
raise(ArgumentError, "#{clazz} must 'include GenericService'")
|
38
|
-
end
|
39
|
-
if clazz.rpc_descs.size.zero?
|
40
|
-
raise(ArgumentError, "#{clazz} should specify some rpc descriptions")
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# Map a path with Rack::Builder corresponding to the service method
|
45
|
-
def add_service_method_to_app(service_name, service_config, service_method)
|
46
|
-
map("/#{service_name}/#{service_method}") do
|
47
|
-
run(RouteHandler.new(service_config, service_method))
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class RouteHandler
|
52
|
-
def initialize(service_config, service_method)
|
53
|
-
self.service_config = service_config
|
54
|
-
self.service_method = service_method
|
55
|
-
end
|
56
|
-
|
57
|
-
def call(env)
|
58
|
-
::GRPCWeb::RackHandler.call(service, service_method, env)
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
attr_accessor :service_config, :service_method
|
64
|
-
|
65
|
-
def service
|
66
|
-
case service_config
|
67
|
-
when Proc
|
68
|
-
service_config.call
|
69
|
-
when Class
|
70
|
-
service_config.new
|
71
|
-
else
|
72
|
-
service_config
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,83 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack/request'
|
4
|
-
require 'grpc_web/content_types'
|
5
|
-
require 'grpc_web/error_callback'
|
6
|
-
require 'grpc_web/grpc_request_processor'
|
7
|
-
require 'grpc_web/grpc_web_request'
|
8
|
-
|
9
|
-
module GRPCWeb
|
10
|
-
module RackHandler
|
11
|
-
NOT_FOUND = 404
|
12
|
-
UNSUPPORTED_MEDIA_TYPE = 415
|
13
|
-
INTERNAL_SERVER_ERROR = 500
|
14
|
-
POST = 'POST'
|
15
|
-
ACCEPT_HEADER = 'HTTP_ACCEPT'
|
16
|
-
|
17
|
-
class << self
|
18
|
-
include ::GRPCWeb::ContentTypes
|
19
|
-
|
20
|
-
def call(service, service_method, env)
|
21
|
-
rack_request = Rack::Request.new(env)
|
22
|
-
return not_found_response(rack_request.path) unless post?(rack_request)
|
23
|
-
return unsupported_media_type_response unless valid_content_types?(rack_request)
|
24
|
-
|
25
|
-
request_format = rack_request.content_type
|
26
|
-
accept = rack_request.get_header(ACCEPT_HEADER)
|
27
|
-
body = rack_request.body.read
|
28
|
-
request = GRPCWebRequest.new(service, service_method, request_format, accept, body)
|
29
|
-
response = GRPCRequestProcessor.process(request)
|
30
|
-
|
31
|
-
[200, { 'Content-Type' => response.content_type }, [response.body]]
|
32
|
-
rescue Google::Protobuf::ParseError => e
|
33
|
-
invalid_response(e.message)
|
34
|
-
rescue StandardError => e
|
35
|
-
::GRPCWeb.on_error.call(e, service, service_method)
|
36
|
-
error_response
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def post?(rack_request)
|
42
|
-
rack_request.request_method == POST
|
43
|
-
end
|
44
|
-
|
45
|
-
def valid_content_types?(rack_request)
|
46
|
-
return false unless ALL_CONTENT_TYPES.include?(rack_request.content_type)
|
47
|
-
|
48
|
-
accept = rack_request.get_header(ACCEPT_HEADER)
|
49
|
-
return true if ANY_CONTENT_TYPES.include?(accept)
|
50
|
-
|
51
|
-
ALL_CONTENT_TYPES.include?(accept)
|
52
|
-
end
|
53
|
-
|
54
|
-
def not_found_response(path)
|
55
|
-
[
|
56
|
-
NOT_FOUND,
|
57
|
-
{ 'Content-Type' => 'text/plain', 'X-Cascade' => 'pass' },
|
58
|
-
["Not Found: #{path}"],
|
59
|
-
]
|
60
|
-
end
|
61
|
-
|
62
|
-
def unsupported_media_type_response
|
63
|
-
[
|
64
|
-
UNSUPPORTED_MEDIA_TYPE,
|
65
|
-
{ 'Content-Type' => 'text/plain' },
|
66
|
-
['Unsupported Media Type: Invalid Content-Type or Accept header'],
|
67
|
-
]
|
68
|
-
end
|
69
|
-
|
70
|
-
def invalid_response(message)
|
71
|
-
[422, { 'Content-Type' => 'text/plain' }, ["Invalid request format: #{message}"]]
|
72
|
-
end
|
73
|
-
|
74
|
-
def error_response
|
75
|
-
[
|
76
|
-
INTERNAL_SERVER_ERROR,
|
77
|
-
{ 'Content-Type' => 'text/plain' },
|
78
|
-
['Request failed with an unexpected error.'],
|
79
|
-
]
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|