grpc-web 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|