grpc-web 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []