grpc-web 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e933d5fb31c8a64203e667339b03fe89a5b1bcfb8ddecd66c0271bef8d60f0e
4
+ data.tar.gz: af6dca14b9a5ba0ada4c91f1c8cf534f408cf8be77180b2553d5418491b38908
5
+ SHA512:
6
+ metadata.gz: 1e06359b9f0097781bf63e28b87e0c12746ab6c3e1a80b1d02a971420931f2f62d5d49b78b1629b70ae204e7ed9c5502346176661c4eff5fabcaec724665c602
7
+ data.tar.gz: 18cd177d9f793d9006ee23f1a28be8879971db60a1e970d7d77e90bb193ed324279d8de3f1bdd7c58a13e31359e9b95596a54a94f57fe7a2ca18b82930ae3141
data/lib/grpc-web.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc_web'
data/lib/grpc_web.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc_web/version'
4
+ require 'grpc_web/rack_app'
5
+
6
+ module GRPCWeb
7
+ class << self
8
+ def rack_app
9
+ @rack_app ||= ::GRPCWeb::RackApp.new
10
+ end
11
+
12
+ def handle(service_or_class, &lazy_init_block)
13
+ rack_app.handle(service_or_class, &lazy_init_block)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+
6
+ require 'grpc/errors'
7
+ require 'grpc_web/content_types'
8
+ require 'grpc_web/message_framing'
9
+
10
+ module GRPCWeb::ClientExecutor
11
+ class << self
12
+ include ::GRPCWeb::ContentTypes
13
+
14
+ GRPC_STATUS_HEADER = 'grpc-status'
15
+ GRPC_MESSAGE_HEADER = 'grpc-message'
16
+
17
+ def request(uri, rpc_desc, params = {})
18
+ req_proto = rpc_desc.input.new(params)
19
+ request_body = ::GRPCWeb::MessageFraming.frame_content(req_proto.to_proto)
20
+
21
+ resp = post_request(uri, request_body)
22
+ resp_body = handle_response(resp)
23
+ rpc_desc.output.decode(resp_body)
24
+ end
25
+
26
+ private
27
+
28
+ def request_headers
29
+ {
30
+ 'Accept' => PROTO_CONTENT_TYPE,
31
+ 'Content-Type' => PROTO_CONTENT_TYPE,
32
+ }
33
+ end
34
+
35
+ def post_request(uri, request_body)
36
+ request = Net::HTTP::Post.new(uri, request_headers)
37
+ request.body = request_body
38
+ if uri.userinfo
39
+ request.basic_auth uri.user, uri.password
40
+ end
41
+
42
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
43
+ http.use_ssl = (uri.scheme == 'https')
44
+ http.request(request)
45
+ end
46
+ end
47
+
48
+ def handle_response(resp)
49
+ unless resp.is_a?(Net::HTTPSuccess)
50
+ raise "Received #{resp.code} #{resp.message} response: #{resp.body}"
51
+ end
52
+
53
+ frames = ::GRPCWeb::MessageFraming.unframe_content(resp.body)
54
+ header_frame = frames.find(&:header?)
55
+ headers = parse_headers(header_frame.body) if header_frame
56
+ raise_on_error(headers)
57
+ frames.find(&:payload?).body
58
+ end
59
+
60
+ def parse_headers(header_str)
61
+ headers = {}
62
+ lines = header_str.split(/\r?\n/)
63
+ lines.each do |line|
64
+ key, value = line.split(':', 2)
65
+ headers[key] = value
66
+ end
67
+ headers
68
+ end
69
+
70
+ def raise_on_error(headers)
71
+ return unless headers
72
+
73
+ status_str = headers[GRPC_STATUS_HEADER]
74
+ status_code = status_str.to_i if status_str && status_str == status_str.to_i.to_s
75
+
76
+ if status_code && status_code != 0
77
+ raise ::GRPC::BadStatus.new_status_exception(status_code, headers[GRPC_MESSAGE_HEADER])
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRPCWeb::ContentTypes
4
+ DEFAULT_CONTENT_TYPE = 'application/grpc-web'
5
+ PROTO_CONTENT_TYPE = 'application/grpc-web+proto'
6
+ JSON_CONTENT_TYPE = 'application/grpc-web+json'
7
+ TEXT_CONTENT_TYPE = 'application/grpc-web-text'
8
+ TEXT_PROTO_CONTENT_TYPE = 'application/grpc-web-text+proto'
9
+
10
+ BASE64_CONTENT_TYPES = [TEXT_CONTENT_TYPE, TEXT_PROTO_CONTENT_TYPE].freeze
11
+ ALL_CONTENT_TYPES = [
12
+ DEFAULT_CONTENT_TYPE,
13
+ PROTO_CONTENT_TYPE,
14
+ JSON_CONTENT_TYPE,
15
+ TEXT_CONTENT_TYPE,
16
+ TEXT_PROTO_CONTENT_TYPE,
17
+ ].freeze
18
+ ANY_CONTENT_TYPES = ['*/*', ''].freeze
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRPCWeb
4
+ class << self
5
+ NOOP_ON_ERROR = proc { |ex, service, service_method| }
6
+
7
+ def on_error(&block)
8
+ if block_given?
9
+
10
+ unless block.parameters.length == 3
11
+ raise ArgumentError, 'callback must accept (exception, service, service_method)'
12
+ end
13
+
14
+ self.on_error_callback = block
15
+ else
16
+ on_error_callback || NOOP_ON_ERROR
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ attr_accessor :on_error_callback
23
+ end
24
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc_web/content_types'
4
+ require 'grpc_web/error_callback'
5
+ require 'grpc_web/grpc_web_response'
6
+ require 'grpc_web/message_framing'
7
+ require 'grpc_web/message_serialization'
8
+ require 'grpc_web/text_coder'
9
+
10
+ module GRPCWeb::GRPCRequestProcessor
11
+ class << self
12
+ include ::GRPCWeb::ContentTypes
13
+
14
+ def process(grpc_web_request)
15
+ text_coder = ::GRPCWeb::TextCoder
16
+ framing = ::GRPCWeb::MessageFraming
17
+ serialization = ::GRPCWeb::MessageSerialization
18
+
19
+ grpc_web_request = text_coder.decode_request(grpc_web_request)
20
+ grpc_web_request = framing.unframe_request(grpc_web_request)
21
+ grpc_web_request = serialization.deserialize_request(grpc_web_request)
22
+ grpc_web_response = execute_request(grpc_web_request)
23
+ grpc_web_response = serialization.serialize_response(grpc_web_response)
24
+ grpc_web_response = framing.frame_response(grpc_web_response)
25
+ text_coder.encode_response(grpc_web_response)
26
+ end
27
+
28
+ private
29
+
30
+ def execute_request(request)
31
+ service_method = ::GRPC::GenericService.underscore(request.service_method.to_s)
32
+
33
+ begin
34
+ response = request.service.send(service_method, request.body)
35
+ rescue StandardError => e
36
+ ::GRPCWeb.on_error.call(e, request.service, request.service_method)
37
+ response = e # Return exception as body if one is raised
38
+ end
39
+ ::GRPCWeb::GRPCWebResponse.new(response_content_type(request), response)
40
+ end
41
+
42
+ # Use Accept header value if specified, otherwise use request content type
43
+ def response_content_type(request)
44
+ if request.accept.nil? || ANY_CONTENT_TYPES.include?(request.accept)
45
+ request.content_type
46
+ else
47
+ request.accept
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRPCWeb
4
+ GRPCWebRequest = Struct.new(
5
+ :service,
6
+ :service_method,
7
+ :content_type,
8
+ :accept,
9
+ :body,
10
+ )
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRPCWeb
4
+ GRPCWebResponse = Struct.new(:content_type, :body)
5
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRPCWeb
4
+ class MessageFrame
5
+ PAYLOAD_FRAME_TYPE = 0 # String: "\x00"
6
+ HEADER_FRAME_TYPE = 128 # String: "\x80"
7
+
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
15
+
16
+ attr_accessor :frame_type, :body
17
+
18
+ def initialize(frame_type, body)
19
+ self.frame_type = frame_type
20
+ self.body = body
21
+ end
22
+
23
+ def payload?
24
+ frame_type == PAYLOAD_FRAME_TYPE
25
+ end
26
+
27
+ def header?
28
+ frame_type == HEADER_FRAME_TYPE
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
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_frame'
6
+
7
+ module GRPCWeb
8
+ # GRPC Web uses a simple 5 byte framing scheme. The first byte represents
9
+ # flags indicating what type of frame this is. The next 4 bytes indicate the
10
+ # byte length of the frame body.
11
+ module MessageFraming
12
+ class << self
13
+ def unframe_request(request)
14
+ frames = unframe_content(request.body)
15
+ ::GRPCWeb::GRPCWebRequest.new(
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
31
+
32
+ def unframe_content(content)
33
+ frames = []
34
+ remaining_content = content
35
+ until remaining_content.empty?
36
+ msg_length = remaining_content[1..4].unpack1('N')
37
+ raise 'Invalid message length' if msg_length <= 0
38
+
39
+ frame_end = 5 + msg_length
40
+ frames << ::GRPCWeb::MessageFrame.new(
41
+ remaining_content[0].bytes[0],
42
+ remaining_content[5...frame_end],
43
+ )
44
+ remaining_content = remaining_content[frame_end..-1]
45
+ end
46
+ frames
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc/core/status_codes'
4
+ require 'grpc/errors'
5
+ require 'grpc_web/content_types'
6
+ require 'grpc_web/grpc_web_response'
7
+ require 'grpc_web/grpc_web_request'
8
+ require 'grpc_web/message_frame'
9
+
10
+ module GRPCWeb::MessageSerialization
11
+ class << self
12
+ include ::GRPCWeb::ContentTypes
13
+
14
+ def deserialize_request(request)
15
+ service_class = request.service.class
16
+ request_proto_class = service_class.rpc_descs[request.service_method.to_sym].input
17
+ payload_frame = request.body.find(&:payload?)
18
+
19
+ if request.content_type == JSON_CONTENT_TYPE
20
+ request_proto = request_proto_class.decode_json(payload_frame.body)
21
+ else
22
+ request_proto = request_proto_class.decode(payload_frame.body)
23
+ end
24
+
25
+ ::GRPCWeb::GRPCWebRequest.new(
26
+ request.service,
27
+ request.service_method,
28
+ request.content_type,
29
+ request.accept,
30
+ request_proto,
31
+ )
32
+ end
33
+
34
+ def serialize_response(response)
35
+ if response.body.is_a?(Exception)
36
+ serialize_error_response(response)
37
+ else
38
+ serialize_success_response(response)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def serialize_success_response(response)
45
+ if response.content_type == JSON_CONTENT_TYPE
46
+ payload = response.body.to_json
47
+ else
48
+ payload = response.body.to_proto
49
+ end
50
+
51
+ header_str = generate_headers(::GRPC::Core::StatusCodes::OK, 'OK')
52
+ payload_frame = ::GRPCWeb::MessageFrame.payload_frame(payload)
53
+ header_frame = ::GRPCWeb::MessageFrame.header_frame(header_str)
54
+
55
+ ::GRPCWeb::GRPCWebResponse.new(response.content_type, [payload_frame, header_frame])
56
+ end
57
+
58
+ def serialize_error_response(response)
59
+ ex = response.body
60
+ if ex.is_a?(::GRPC::BadStatus)
61
+ header_str = generate_headers(ex.code, ex.details)
62
+ else
63
+ header_str = generate_headers(
64
+ ::GRPC::Core::StatusCodes::UNKNOWN,
65
+ "#{ex.class}: #{ex.message}",
66
+ )
67
+ end
68
+ header_frame = ::GRPCWeb::MessageFrame.header_frame(header_str)
69
+ ::GRPCWeb::GRPCWebResponse.new(response.content_type, [header_frame])
70
+ end
71
+
72
+ # If needed, trailers can be appended to the response as a 2nd
73
+ # base64 encoded string with independent framing.
74
+ def generate_headers(status, message)
75
+ header_str = [
76
+ "grpc-status:#{status}",
77
+ "grpc-message:#{message}",
78
+ 'x-grpc-web:1',
79
+ nil, # for trailing newline
80
+ ].join("\r\n")
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,77 @@
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
@@ -0,0 +1,83 @@
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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'grpc_web/content_types'
5
+ require 'grpc_web/grpc_web_response'
6
+ require 'grpc_web/grpc_web_request'
7
+
8
+ module GRPCWeb::TextCoder
9
+ class << self
10
+ include ::GRPCWeb::ContentTypes
11
+
12
+ def decode_request(request)
13
+ return request unless BASE64_CONTENT_TYPES.include?(request.content_type)
14
+
15
+ # Body can be several base64 "chunks" concatenated together
16
+ base64_chunks = request.body.scan(%r{[a-zA-Z0-9+/]+={0,2}})
17
+ decoded = base64_chunks.map { |chunk| Base64.decode64(chunk) }.join
18
+ ::GRPCWeb::GRPCWebRequest.new(
19
+ request.service, request.service_method, request.content_type, request.accept, decoded,
20
+ )
21
+ end
22
+
23
+ def encode_response(response)
24
+ return response unless BASE64_CONTENT_TYPES.include?(response.content_type)
25
+
26
+ encoded = Base64.strict_encode64(response.body)
27
+ ::GRPCWeb::GRPCWebResponse.new(response.content_type, encoded)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRPCWeb
4
+ VERSION = '1.0.0'
5
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: grpc-web
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - James Shkolnik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: grpc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.0
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.6.0
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: apparition
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: pry-byebug
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rack-cors
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.3'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.3'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.79.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.79.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: rubocop-rspec
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: simplecov
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ description: Host gRPC-Web endpoints for Ruby gRPC services in a Rack or Rails app(over
160
+ HTTP/1.1). Client included.
161
+ email:
162
+ - js@gusto.com
163
+ executables: []
164
+ extensions: []
165
+ extra_rdoc_files: []
166
+ files:
167
+ - lib/grpc-web.rb
168
+ - lib/grpc_web.rb
169
+ - lib/grpc_web/client.rb
170
+ - lib/grpc_web/client_executor.rb
171
+ - lib/grpc_web/content_types.rb
172
+ - lib/grpc_web/error_callback.rb
173
+ - lib/grpc_web/grpc_request_processor.rb
174
+ - lib/grpc_web/grpc_web_request.rb
175
+ - lib/grpc_web/grpc_web_response.rb
176
+ - lib/grpc_web/message_frame.rb
177
+ - lib/grpc_web/message_framing.rb
178
+ - lib/grpc_web/message_serialization.rb
179
+ - lib/grpc_web/rack_app.rb
180
+ - lib/grpc_web/rack_handler.rb
181
+ - lib/grpc_web/text_coder.rb
182
+ - lib/grpc_web/version.rb
183
+ homepage: https://github.com/gusto/grpc-web-ruby
184
+ licenses:
185
+ - MIT
186
+ metadata: {}
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubygems_version: 3.0.3
203
+ signing_key:
204
+ specification_version: 4
205
+ summary: Host gRPC-Web endpoints for Ruby gRPC services in a Rack or Rails app(over
206
+ HTTP/1.1). Client included.
207
+ test_files: []