protocol-grpc 0.11.0 → 0.13.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +7 -9
- data/context/index.yaml +2 -0
- data/design.md +21 -28
- data/lib/protocol/grpc/body/readable.rb +29 -24
- data/lib/protocol/grpc/body/writable.rb +15 -14
- data/lib/protocol/grpc/call.rb +21 -2
- data/lib/protocol/grpc/header/timeout.rb +40 -6
- data/lib/protocol/grpc/interface.rb +8 -2
- data/lib/protocol/grpc/metadata.rb +58 -33
- data/lib/protocol/grpc/methods.rb +26 -68
- data/lib/protocol/grpc/middleware.rb +1 -1
- data/lib/protocol/grpc/route.rb +48 -0
- data/lib/protocol/grpc/version.rb +1 -1
- data/lib/protocol/grpc.rb +2 -1
- data/readme.md +29 -3
- data/releases.md +10 -0
- data.tar.gz.sig +0 -0
- metadata +6 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a991dcf85eaa66f7a1cbd8d31c919e82b37e0b2034ad594da58d342391b00590
|
|
4
|
+
data.tar.gz: 0fe139aeffb04fbaee3700bcfde98588625dc37706195598fa99dd632786396a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbd273eb5cb2481235b14404bdc2742f95b3dee7a53383e1a69926e5b93508e2701bbba165d26023953b73cd9cc1438872d3fffa9dd1e77f2c5f0116c2ec95ce
|
|
7
|
+
data.tar.gz: 7e532651a9629d38558c1078eb98fb97b13c4d3c999d13ef22e01d8f6e31822bfdd10c11ce7e9640eb42db393f3441420e804fc7c6d6b74ddcdc49b3ceef560e
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/getting-started.md
CHANGED
|
@@ -62,11 +62,10 @@ rpc :SayHelloAgain, request_class: Hello::HelloRequest, response_class: Hello::H
|
|
|
62
62
|
|
|
63
63
|
### Building a Request
|
|
64
64
|
|
|
65
|
-
Build gRPC requests using `Protocol::GRPC::
|
|
65
|
+
Build gRPC requests using `Protocol::GRPC::Metadata`, `Protocol::GRPC::Route`, and `Protocol::GRPC::Body::Writable`:
|
|
66
66
|
|
|
67
67
|
``` ruby
|
|
68
68
|
require "protocol/grpc"
|
|
69
|
-
require "protocol/grpc/methods"
|
|
70
69
|
require "protocol/grpc/body/writable"
|
|
71
70
|
|
|
72
71
|
# Build request body
|
|
@@ -75,8 +74,8 @@ body.write(Hello::HelloRequest.new(name: "World"))
|
|
|
75
74
|
body.close_write
|
|
76
75
|
|
|
77
76
|
# Build headers
|
|
78
|
-
headers = Protocol::GRPC::
|
|
79
|
-
path = Protocol::GRPC::
|
|
77
|
+
headers = Protocol::GRPC::Metadata.build(timeout: 5.0)
|
|
78
|
+
path = Protocol::GRPC::Route.build("hello.Greeter", "SayHello")
|
|
80
79
|
|
|
81
80
|
# Create HTTP request
|
|
82
81
|
request = Protocol::HTTP::Request["POST", path, headers, body]
|
|
@@ -117,11 +116,11 @@ class MyMiddleware < Protocol::GRPC::Middleware
|
|
|
117
116
|
protected
|
|
118
117
|
|
|
119
118
|
def dispatch(request)
|
|
120
|
-
|
|
121
|
-
service_name, method_name = Protocol::GRPC::
|
|
119
|
+
# Parse the service and method from the path:
|
|
120
|
+
service_name, method_name = Protocol::GRPC::Route.parse(request.path)
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
# Handle the request using service_name and method_name.
|
|
123
|
+
# ...
|
|
125
124
|
end
|
|
126
125
|
end
|
|
127
126
|
```
|
|
@@ -144,4 +143,3 @@ call.deadline.exceeded? # => false
|
|
|
144
143
|
# Access peer information
|
|
145
144
|
call.peer # => Protocol::HTTP::Address
|
|
146
145
|
```
|
|
147
|
-
|
data/context/index.yaml
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
---
|
|
4
4
|
description: Protocol abstractions for gRPC, built on top of protocol-http.
|
|
5
5
|
metadata:
|
|
6
|
+
bug_tracker_uri: https://github.com/socketry/protocol-grpc/issues
|
|
7
|
+
changelog_uri: https://github.com/socketry/protocol-grpc/blob/main/releases.md
|
|
6
8
|
documentation_uri: https://socketry.github.io/protocol-grpc/
|
|
7
9
|
source_code_uri: https://github.com/socketry/protocol-grpc.git
|
|
8
10
|
files:
|
data/design.md
CHANGED
|
@@ -32,8 +32,8 @@ It does NOT include:
|
|
|
32
32
|
The protocol layer provides these core abstractions:
|
|
33
33
|
|
|
34
34
|
1. **Message Interface** - `Protocol::GRPC::Message` and `MessageHelpers`
|
|
35
|
-
2. **Path Handling** - `Protocol::GRPC::
|
|
36
|
-
3. **Metadata** - `Protocol::GRPC::Metadata` (extract
|
|
35
|
+
2. **Path Handling** - `Protocol::GRPC::Route` (build and parse request paths)
|
|
36
|
+
3. **Metadata** - `Protocol::GRPC::Metadata` (build request headers and extract or assign metadata)
|
|
37
37
|
4. **Body Framing** - `Protocol::GRPC::Body::Readable` and `Body::Writable`
|
|
38
38
|
5. **Status Codes** - `Protocol::GRPC::Status` constants
|
|
39
39
|
6. **Errors** - `Protocol::GRPC::Error` hierarchy
|
|
@@ -122,35 +122,29 @@ end
|
|
|
122
122
|
|
|
123
123
|
**Path of Least Resistance**: Google's `protobuf` gem already generates classes with `.decode(binary)` and `#to_proto` methods, so they work out of the box with no wrapper needed.
|
|
124
124
|
|
|
125
|
-
#### 2. `Protocol::GRPC::
|
|
125
|
+
#### 2. `Protocol::GRPC::Route` and `Protocol::GRPC::Metadata`
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
`Route` represents the service and method encoded in a gRPC request path. `Metadata` builds request headers and extracts application metadata:
|
|
128
128
|
|
|
129
129
|
```ruby
|
|
130
130
|
module Protocol
|
|
131
131
|
module GRPC
|
|
132
|
-
module
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# @parameter method [String] e.g., "SayHello"
|
|
136
|
-
# @returns [String] e.g., "/my_service.Greeter/SayHello"
|
|
137
|
-
def self.build_path(service, method)
|
|
138
|
-
"/#{service}/#{method}"
|
|
132
|
+
module Route
|
|
133
|
+
def self.parse(path)
|
|
134
|
+
# Return the service and method names.
|
|
139
135
|
end
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# @returns [Tuple(String, String)] of service and method.
|
|
144
|
-
def self.parse_path(path)
|
|
145
|
-
parts = path.split("/")
|
|
146
|
-
[parts[1], parts[2]]
|
|
137
|
+
def self.build(service_name, method_name)
|
|
138
|
+
# Return the gRPC request path.
|
|
147
139
|
end
|
|
148
|
-
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
module Metadata
|
|
149
143
|
# Build gRPC request headers
|
|
150
144
|
# @parameter metadata [Hash] Custom metadata key-value pairs
|
|
151
145
|
# @parameter timeout [Numeric] Optional timeout in seconds
|
|
152
146
|
# @returns [Protocol::HTTP::Headers]
|
|
153
|
-
def self.
|
|
147
|
+
def self.build(metadata: {}, timeout: nil, content_type: "application/grpc+proto")
|
|
154
148
|
headers = Protocol::HTTP::Headers.new
|
|
155
149
|
headers["content-type"] = content_type
|
|
156
150
|
headers["te"] = "trailers"
|
|
@@ -171,7 +165,7 @@ module Protocol
|
|
|
171
165
|
# Extract metadata from gRPC headers
|
|
172
166
|
# @parameter headers [Protocol::HTTP::Headers]
|
|
173
167
|
# @returns [Hash] Metadata key-value pairs
|
|
174
|
-
def self.
|
|
168
|
+
def self.extract(headers)
|
|
175
169
|
metadata = {}
|
|
176
170
|
|
|
177
171
|
headers.each do |key, value|
|
|
@@ -553,7 +547,7 @@ module Protocol
|
|
|
553
547
|
# Extract metadata from request headers
|
|
554
548
|
# @returns [Hash] Custom metadata
|
|
555
549
|
def metadata
|
|
556
|
-
@metadata ||=
|
|
550
|
+
@metadata ||= Metadata.extract(@request.headers)
|
|
557
551
|
end
|
|
558
552
|
|
|
559
553
|
# Check if the deadline has expired
|
|
@@ -719,7 +713,7 @@ module Protocol
|
|
|
719
713
|
end
|
|
720
714
|
|
|
721
715
|
# Parse service and method from path
|
|
722
|
-
service_name, method_name =
|
|
716
|
+
service_name, method_name = Route.parse(request.path)
|
|
723
717
|
|
|
724
718
|
# Find handler
|
|
725
719
|
handler = @services[service_name]
|
|
@@ -836,13 +830,13 @@ body.write(MyService::HelloRequest.new(name: "World"))
|
|
|
836
830
|
body.close_write
|
|
837
831
|
|
|
838
832
|
# Build gRPC headers
|
|
839
|
-
headers = Protocol::GRPC::
|
|
833
|
+
headers = Protocol::GRPC::Metadata.build(
|
|
840
834
|
metadata: {"authorization" => "Bearer token123"},
|
|
841
835
|
timeout: 5.0
|
|
842
836
|
)
|
|
843
837
|
|
|
844
838
|
# Create HTTP request with gRPC path
|
|
845
|
-
path = Protocol::GRPC::
|
|
839
|
+
path = Protocol::GRPC::Route.build("my_service.Greeter", "SayHello")
|
|
846
840
|
|
|
847
841
|
request = Protocol::HTTP::Request[
|
|
848
842
|
"POST", path,
|
|
@@ -895,7 +889,7 @@ require "protocol/grpc"
|
|
|
895
889
|
# This would be inside a Rack/HTTP middleware/handler
|
|
896
890
|
def handle_grpc_request(http_request)
|
|
897
891
|
# Parse gRPC path
|
|
898
|
-
service, method = Protocol::GRPC::
|
|
892
|
+
service, method = Protocol::GRPC::Route.parse(http_request.path)
|
|
899
893
|
|
|
900
894
|
# Read input messages
|
|
901
895
|
input = Protocol::GRPC::Body::Readable.new(
|
|
@@ -1436,8 +1430,8 @@ This keeps dependencies minimal while providing great developer experience!
|
|
|
1436
1430
|
- Binary message support (no message_class = raw binary) (✅ Designed)
|
|
1437
1431
|
|
|
1438
1432
|
### Phase 2: Protocol Helpers
|
|
1439
|
-
- `Protocol::GRPC::
|
|
1440
|
-
- `Protocol::GRPC::Header`
|
|
1433
|
+
- `Protocol::GRPC::Route` (path parsing and building) (✅ Designed)
|
|
1434
|
+
- `Protocol::GRPC::Header` values (Status, Message, Timeout, Encoding) (✅ Designed)
|
|
1441
1435
|
- `Protocol::GRPC::HEADER_POLICY` for trailer support (✅ Designed)
|
|
1442
1436
|
- `Protocol::GRPC::Metadata` (status extraction, trailer helpers) (✅ Designed)
|
|
1443
1437
|
- `Protocol::GRPC::Call` context object (✅ Designed)
|
|
@@ -1644,4 +1638,3 @@ These map naturally to `Protocol::HTTP::Body::Writable` and `Readable`.
|
|
|
1644
1638
|
- [gRPC Protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md)
|
|
1645
1639
|
- [Protocol::HTTP Design](https://socketry.github.io/protocol-http/guides/design-overview/)
|
|
1646
1640
|
- [gRPC over HTTP/2](https://grpc.io/docs/what-is-grpc/core-concepts/)
|
|
1647
|
-
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "protocol/http"
|
|
7
7
|
require "protocol/http/body/wrapper"
|
|
8
8
|
require "zlib"
|
|
9
9
|
|
|
10
|
+
require_relative "../error"
|
|
11
|
+
require_relative "../status"
|
|
12
|
+
|
|
10
13
|
module Protocol
|
|
11
14
|
module GRPC
|
|
12
15
|
# @namespace
|
|
@@ -47,8 +50,6 @@ module Protocol
|
|
|
47
50
|
# Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages.
|
|
48
51
|
# @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended
|
|
49
52
|
def read
|
|
50
|
-
return nil if @body.nil? || @body.empty?
|
|
51
|
-
|
|
52
53
|
# Read 5-byte prefix: 1 byte compression flag + 4 bytes length
|
|
53
54
|
prefix = read_exactly(5)
|
|
54
55
|
return nil unless prefix
|
|
@@ -58,7 +59,9 @@ module Protocol
|
|
|
58
59
|
|
|
59
60
|
# Read the message body:
|
|
60
61
|
data = read_exactly(length)
|
|
61
|
-
|
|
62
|
+
unless data
|
|
63
|
+
raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{length} bytes, received 0")
|
|
64
|
+
end
|
|
62
65
|
|
|
63
66
|
# Decompress if needed:
|
|
64
67
|
data = decompress(data) if compressed
|
|
@@ -76,19 +79,25 @@ module Protocol
|
|
|
76
79
|
private
|
|
77
80
|
|
|
78
81
|
# Read exactly n bytes from the underlying body.
|
|
79
|
-
# @parameter n [Integer] The number of bytes to read
|
|
80
|
-
# @returns [String | Nil] The data read, or `Nil` if the stream ended
|
|
82
|
+
# @parameter n [Integer] The number of bytes to read.
|
|
83
|
+
# @returns [String | Nil] The data read, or `Nil` if the stream ended before reading any bytes.
|
|
84
|
+
# @raises [Error] If the stream ends after reading a partial value.
|
|
81
85
|
def read_exactly(n)
|
|
82
86
|
# Fill buffer until we have enough data:
|
|
83
87
|
while @buffer.bytesize < n
|
|
84
|
-
|
|
88
|
+
if @body.nil? || @body.empty?
|
|
89
|
+
return nil if @buffer.empty?
|
|
90
|
+
|
|
91
|
+
raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{n} bytes, received #{@buffer.bytesize}")
|
|
92
|
+
end
|
|
85
93
|
|
|
86
94
|
# Read chunk from underlying body:
|
|
87
95
|
chunk = @body.read
|
|
88
96
|
|
|
89
97
|
if chunk.nil?
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
return nil if @buffer.empty?
|
|
99
|
+
|
|
100
|
+
raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{n} bytes, received #{@buffer.bytesize}")
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
# Append to buffer:
|
|
@@ -108,24 +117,20 @@ module Protocol
|
|
|
108
117
|
def decompress(data)
|
|
109
118
|
case @encoding
|
|
110
119
|
when "gzip"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
result
|
|
120
|
+
begin
|
|
121
|
+
Zlib.gunzip(data)
|
|
122
|
+
rescue => error
|
|
123
|
+
raise Error.new(Status::INTERNAL, "Failed to decompress message: #{error.message}")
|
|
124
|
+
end
|
|
117
125
|
when "deflate"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
result
|
|
126
|
+
begin
|
|
127
|
+
Zlib::Inflate.inflate(data)
|
|
128
|
+
rescue => error
|
|
129
|
+
raise Error.new(Status::INTERNAL, "Failed to decompress message: #{error.message}")
|
|
130
|
+
end
|
|
124
131
|
else
|
|
125
|
-
|
|
132
|
+
raise Error.new(Status::UNIMPLEMENTED, "Unsupported compression encoding: #{@encoding.inspect}")
|
|
126
133
|
end
|
|
127
|
-
rescue StandardError => error
|
|
128
|
-
raise Error.new(Status::INTERNAL, "Failed to decompress message: #{error.message}")
|
|
129
134
|
end
|
|
130
135
|
end
|
|
131
136
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "protocol/http"
|
|
7
7
|
require "protocol/http/body/writable"
|
|
8
8
|
require "zlib"
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
require_relative "../error"
|
|
11
|
+
require_relative "../status"
|
|
10
12
|
|
|
11
13
|
module Protocol
|
|
12
14
|
module GRPC
|
|
@@ -86,21 +88,20 @@ module Protocol
|
|
|
86
88
|
def compress(data)
|
|
87
89
|
case @encoding
|
|
88
90
|
when "gzip"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
io.string
|
|
91
|
+
begin
|
|
92
|
+
Zlib.gzip(data, level: @level)
|
|
93
|
+
rescue => error
|
|
94
|
+
raise Error.new(Status::INTERNAL, "Failed to compress message: #{error.message}")
|
|
95
|
+
end
|
|
95
96
|
when "deflate"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
begin
|
|
98
|
+
Zlib::Deflate.deflate(data, @level)
|
|
99
|
+
rescue => error
|
|
100
|
+
raise Error.new(Status::INTERNAL, "Failed to compress message: #{error.message}")
|
|
101
|
+
end
|
|
99
102
|
else
|
|
100
|
-
|
|
103
|
+
raise Error.new(Status::INTERNAL, "Unsupported compression encoding: #{@encoding.inspect}")
|
|
101
104
|
end
|
|
102
|
-
rescue StandardError => error
|
|
103
|
-
raise Error.new(Status::INTERNAL, "Failed to compress message: #{error.message}")
|
|
104
105
|
end
|
|
105
106
|
end
|
|
106
107
|
end
|
data/lib/protocol/grpc/call.rb
CHANGED
|
@@ -4,12 +4,25 @@
|
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "async/deadline"
|
|
7
|
-
require_relative "
|
|
7
|
+
require_relative "metadata"
|
|
8
8
|
|
|
9
9
|
module Protocol
|
|
10
10
|
module GRPC
|
|
11
11
|
# Represents context for a single RPC call.
|
|
12
12
|
class Call
|
|
13
|
+
# Create a new RPC call context for the given request and response.
|
|
14
|
+
# Automatically computes a deadline from the `grpc-timeout` request header, if present.
|
|
15
|
+
# @parameter request [Protocol::HTTP::Request] The HTTP request
|
|
16
|
+
# @parameter response [Protocol::HTTP::Response | Nil] The HTTP response
|
|
17
|
+
# @returns [Call] The new call context.
|
|
18
|
+
def self.for(request, response = nil)
|
|
19
|
+
if timeout = request.headers["grpc-timeout"]
|
|
20
|
+
deadline = Async::Deadline.start(timeout.to_seconds)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return new(request, response, deadline: deadline)
|
|
24
|
+
end
|
|
25
|
+
|
|
13
26
|
# Initialize a new RPC call context.
|
|
14
27
|
# @parameter request [Protocol::HTTP::Request] The HTTP request
|
|
15
28
|
# @parameter response [Protocol::HTTP::Response | Nil] The HTTP response (for setting metadata and trailers)
|
|
@@ -33,7 +46,13 @@ module Protocol
|
|
|
33
46
|
# Extract metadata from request headers.
|
|
34
47
|
# @returns [Hash] Custom metadata key-value pairs
|
|
35
48
|
def metadata
|
|
36
|
-
@metadata ||=
|
|
49
|
+
@metadata ||= Metadata.extract(@request.headers)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the timeout requested by the client.
|
|
53
|
+
# @returns [Numeric | Nil] The original timeout in seconds, or `nil` if no timeout was specified.
|
|
54
|
+
def timeout
|
|
55
|
+
@request.headers["grpc-timeout"]&.to_seconds
|
|
37
56
|
end
|
|
38
57
|
|
|
39
58
|
# Check if the deadline has expired.
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative "../methods"
|
|
7
|
-
|
|
8
6
|
module Protocol
|
|
9
7
|
module GRPC
|
|
10
8
|
module Header
|
|
@@ -14,6 +12,28 @@ module Protocol
|
|
|
14
12
|
# The format is: value + unit (H=hours, M=minutes, S=seconds, m=milliseconds, u=microseconds, n=nanoseconds).
|
|
15
13
|
# This header appears only in request headers, not in trailers.
|
|
16
14
|
class Timeout < String
|
|
15
|
+
# The wire format for a gRPC timeout value.
|
|
16
|
+
FORMAT = /\A(?<amount>[1-9]\d{0,7})(?<unit>[HMSmun])\z/
|
|
17
|
+
|
|
18
|
+
# Format a timeout duration for the `grpc-timeout` header.
|
|
19
|
+
# @parameter timeout [Numeric] The timeout duration in seconds.
|
|
20
|
+
# @returns [String] The formatted timeout.
|
|
21
|
+
def self.format(timeout)
|
|
22
|
+
if timeout >= 3600
|
|
23
|
+
"#{(timeout / 3600).to_i}H"
|
|
24
|
+
elsif timeout >= 60
|
|
25
|
+
"#{(timeout / 60).to_i}M"
|
|
26
|
+
elsif timeout >= 1
|
|
27
|
+
"#{timeout.to_i}S"
|
|
28
|
+
elsif timeout >= 0.001
|
|
29
|
+
"#{(timeout * 1000).to_i}m"
|
|
30
|
+
elsif timeout >= 0.000001
|
|
31
|
+
"#{(timeout * 1_000_000).to_i}u"
|
|
32
|
+
else
|
|
33
|
+
"#{(timeout * 1_000_000_000).to_i}n"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
17
37
|
# Parse a timeout from a header value.
|
|
18
38
|
#
|
|
19
39
|
# @parameter value [String] The header value to parse (e.g., "5S", "1000m").
|
|
@@ -24,13 +44,13 @@ module Protocol
|
|
|
24
44
|
|
|
25
45
|
# Coerce a value to a Timeout instance.
|
|
26
46
|
#
|
|
27
|
-
# If a Numeric is provided, it will be formatted as a gRPC timeout string using {
|
|
47
|
+
# If a Numeric is provided, it will be formatted as a gRPC timeout string using {format}.
|
|
28
48
|
#
|
|
29
49
|
# @parameter value [String | Numeric] The value to coerce.
|
|
30
50
|
# @returns [Timeout] A new Timeout instance.
|
|
31
51
|
def self.coerce(value)
|
|
32
52
|
if value.is_a?(Numeric)
|
|
33
|
-
return new(
|
|
53
|
+
return new(format(value))
|
|
34
54
|
else
|
|
35
55
|
return new(value.to_s)
|
|
36
56
|
end
|
|
@@ -45,9 +65,23 @@ module Protocol
|
|
|
45
65
|
|
|
46
66
|
# Parse the timeout value to seconds.
|
|
47
67
|
#
|
|
48
|
-
# @returns [Numeric
|
|
68
|
+
# @returns [Numeric] Timeout in seconds.
|
|
69
|
+
# @raises [ArgumentError] If the timeout value is invalid.
|
|
49
70
|
def to_seconds
|
|
50
|
-
|
|
71
|
+
unless match = FORMAT.match(self)
|
|
72
|
+
raise ArgumentError, "Invalid grpc-timeout: #{self.inspect}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
amount = match[:amount].to_i
|
|
76
|
+
|
|
77
|
+
case match[:unit]
|
|
78
|
+
when "H" then amount * 3600
|
|
79
|
+
when "M" then amount * 60
|
|
80
|
+
when "S" then amount
|
|
81
|
+
when "m" then amount / 1000.0
|
|
82
|
+
when "u" then amount / 1_000_000.0
|
|
83
|
+
when "n" then amount / 1_000_000_000.0
|
|
84
|
+
end
|
|
51
85
|
end
|
|
52
86
|
|
|
53
87
|
# Merge another timeout value (takes the new value, as timeout should only appear once)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "route"
|
|
7
7
|
|
|
8
8
|
module Protocol
|
|
9
9
|
module GRPC
|
|
@@ -25,6 +25,12 @@ module Protocol
|
|
|
25
25
|
class Interface
|
|
26
26
|
# RPC method definition
|
|
27
27
|
RPC = Struct.new(:name, :request_class, :response_class, :streaming, :method, keyword_init: true) do
|
|
28
|
+
# Initialize a new RPC method definition.
|
|
29
|
+
# @parameter name [Symbol] The RPC method name.
|
|
30
|
+
# @parameter request_class [Class | Streaming | Nil] The request message class.
|
|
31
|
+
# @parameter response_class [Class | Streaming | Nil] The response message class.
|
|
32
|
+
# @parameter streaming [Symbol] The streaming mode.
|
|
33
|
+
# @parameter method [Symbol | Nil] The Ruby method name.
|
|
28
34
|
def initialize(name:, request_class:, response_class:, streaming: :unary, method: nil)
|
|
29
35
|
super
|
|
30
36
|
end
|
|
@@ -153,7 +159,7 @@ module Protocol
|
|
|
153
159
|
# @parameter method_name [String | Symbol] Method name in PascalCase (e.g., :SayHello)
|
|
154
160
|
# @returns [String] gRPC path with PascalCase method name
|
|
155
161
|
def path(method_name)
|
|
156
|
-
|
|
162
|
+
Route.build(@name, method_name.to_s)
|
|
157
163
|
end
|
|
158
164
|
|
|
159
165
|
private
|
|
@@ -1,18 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "base64"
|
|
5
7
|
|
|
6
|
-
require "uri"
|
|
7
8
|
require_relative "header"
|
|
8
9
|
require_relative "status"
|
|
9
10
|
|
|
10
11
|
module Protocol
|
|
11
12
|
module GRPC
|
|
12
|
-
#
|
|
13
|
+
# Provides operations for building and extracting gRPC metadata.
|
|
13
14
|
module Metadata
|
|
15
|
+
# Build gRPC request headers containing the given metadata.
|
|
16
|
+
# @parameter metadata [Hash] Custom metadata key-value pairs.
|
|
17
|
+
# @parameter timeout [Numeric | Nil] Optional timeout in seconds.
|
|
18
|
+
# @parameter content_type [String] The request content type.
|
|
19
|
+
# @returns [Protocol::HTTP::Headers] The constructed request headers.
|
|
20
|
+
def self.build(metadata: {}, timeout: nil, content_type: "application/grpc+proto")
|
|
21
|
+
headers = Protocol::HTTP::Headers.new(policy: Protocol::GRPC::HEADER_POLICY)
|
|
22
|
+
headers["content-type"] = content_type
|
|
23
|
+
headers["te"] = "trailers"
|
|
24
|
+
|
|
25
|
+
if timeout
|
|
26
|
+
# Coerced to proper format by header policy:
|
|
27
|
+
headers["grpc-timeout"] = timeout
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
metadata.each do |key, value|
|
|
31
|
+
# Binary headers end with -bin and are base64 encoded:
|
|
32
|
+
headers[key] = if key.end_with?("-bin")
|
|
33
|
+
Base64.strict_encode64(value)
|
|
34
|
+
else
|
|
35
|
+
value.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
headers
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Extract application metadata from gRPC headers.
|
|
43
|
+
# @parameter headers [Protocol::HTTP::Headers] The headers to inspect.
|
|
44
|
+
# @returns [Hash] The extracted metadata key-value pairs.
|
|
45
|
+
def self.extract(headers)
|
|
46
|
+
metadata = {}
|
|
47
|
+
|
|
48
|
+
headers.to_h.each do |key, value|
|
|
49
|
+
# Skip reserved headers:
|
|
50
|
+
next if key.start_with?("grpc-") || key == "content-type" || key == "te"
|
|
51
|
+
|
|
52
|
+
# Decode binary headers:
|
|
53
|
+
if key.end_with?("-bin")
|
|
54
|
+
if value.is_a?(String)
|
|
55
|
+
value = Base64.strict_decode64(value)
|
|
56
|
+
elsif value.is_a?(Array)
|
|
57
|
+
value = value.map{|item| Base64.strict_decode64(item)}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
metadata[key] = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
metadata
|
|
65
|
+
end
|
|
66
|
+
|
|
14
67
|
# Extract gRPC status from headers.
|
|
15
|
-
# Convenience method that handles both Header::Status instances and raw values.
|
|
16
68
|
# Returns Status::UNKNOWN if status is not present.
|
|
17
69
|
#
|
|
18
70
|
# Note: In Protocol::HTTP::Headers, trailers are merged into the headers
|
|
@@ -29,31 +81,10 @@ module Protocol
|
|
|
29
81
|
status = headers["grpc-status"]
|
|
30
82
|
return Status::UNKNOWN unless status
|
|
31
83
|
|
|
32
|
-
|
|
33
|
-
status.to_i
|
|
34
|
-
else
|
|
35
|
-
# Fallback for when header policy isn't used
|
|
36
|
-
# Handle Array case (may occur with external clients)
|
|
37
|
-
status_value = if status.is_a?(Array)
|
|
38
|
-
# Flatten and take first non-nil value, recursively handle nested arrays
|
|
39
|
-
flattened = status.flatten.compact.first
|
|
40
|
-
# If still an array, take first element
|
|
41
|
-
flattened.is_a?(Array) ? flattened.first : flattened
|
|
42
|
-
else
|
|
43
|
-
status
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Convert to string then integer to handle various types
|
|
47
|
-
# Handle case where status_value might still be an array somehow
|
|
48
|
-
if status_value.is_a?(Array)
|
|
49
|
-
status_value = status_value.first
|
|
50
|
-
end
|
|
51
|
-
status_value.to_s.to_i
|
|
52
|
-
end
|
|
84
|
+
return status.to_i
|
|
53
85
|
end
|
|
54
86
|
|
|
55
87
|
# Extract gRPC status message from headers.
|
|
56
|
-
# Convenience method that handles both Header::Message instances and raw values.
|
|
57
88
|
# Returns `Nil` if message is not present.
|
|
58
89
|
#
|
|
59
90
|
# @parameter headers [Protocol::HTTP::Headers]
|
|
@@ -66,13 +97,7 @@ module Protocol
|
|
|
66
97
|
message = headers["grpc-message"]
|
|
67
98
|
return nil unless message
|
|
68
99
|
|
|
69
|
-
|
|
70
|
-
message.decode
|
|
71
|
-
else
|
|
72
|
-
# Fallback for when header policy isn't used
|
|
73
|
-
message_value = message.is_a?(Array) ? message.first : message.to_s
|
|
74
|
-
URI.decode_www_form_component(message_value)
|
|
75
|
-
end
|
|
100
|
+
return message.decode
|
|
76
101
|
end
|
|
77
102
|
|
|
78
103
|
# Assign gRPC status, message, and optional backtrace to headers.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
require_relative "metadata"
|
|
7
|
+
require_relative "route"
|
|
8
8
|
|
|
9
9
|
module Protocol
|
|
10
10
|
module GRPC
|
|
@@ -14,16 +14,21 @@ module Protocol
|
|
|
14
14
|
# @parameter service [String] e.g., "my_service.Greeter"
|
|
15
15
|
# @parameter method [String] e.g., "SayHello"
|
|
16
16
|
# @returns [String] e.g., "/my_service.Greeter/SayHello"
|
|
17
|
+
# @deprecated Use {Route.build} instead.
|
|
17
18
|
def self.build_path(service, method)
|
|
18
|
-
"
|
|
19
|
+
Kernel.warn("`Protocol::GRPC::Methods.build_path` is deprecated; use `Protocol::GRPC::Route.build` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
20
|
+
|
|
21
|
+
Route.build(service, method)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
# Parse service and method from gRPC path.
|
|
22
25
|
# @parameter path [String] e.g., "/my_service.Greeter/SayHello"
|
|
23
26
|
# @returns [Array(String | String)] [service, method]
|
|
27
|
+
# @deprecated Use {Route.parse} instead.
|
|
24
28
|
def self.parse_path(path)
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
Kernel.warn("`Protocol::GRPC::Methods.parse_path` is deprecated; use `Protocol::GRPC::Route.parse` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
30
|
+
|
|
31
|
+
Route.parse(path)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
# Build gRPC request headers.
|
|
@@ -31,92 +36,45 @@ module Protocol
|
|
|
31
36
|
# @parameter timeout [Numeric | Nil] Optional timeout in seconds
|
|
32
37
|
# @parameter content_type [String] Content type (default: "application/grpc+proto")
|
|
33
38
|
# @returns [Protocol::HTTP::Headers]
|
|
39
|
+
# @deprecated Use {Metadata.build} instead.
|
|
34
40
|
def self.build_headers(metadata: {}, timeout: nil, content_type: "application/grpc+proto")
|
|
35
|
-
|
|
36
|
-
headers["content-type"] = content_type
|
|
37
|
-
headers["te"] = "trailers"
|
|
38
|
-
|
|
39
|
-
if timeout
|
|
40
|
-
# Coerced to proper format by header policy:
|
|
41
|
-
headers["grpc-timeout"] = timeout
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
metadata.each do |key, value|
|
|
45
|
-
# Binary headers end with -bin and are base64 encoded:
|
|
46
|
-
headers[key] = if key.end_with?("-bin")
|
|
47
|
-
Base64.strict_encode64(value)
|
|
48
|
-
else
|
|
49
|
-
value.to_s
|
|
50
|
-
end
|
|
51
|
-
end
|
|
41
|
+
Kernel.warn("`Protocol::GRPC::Methods.build_headers` is deprecated; use `Protocol::GRPC::Metadata.build` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
52
42
|
|
|
53
|
-
|
|
43
|
+
Metadata.build(metadata: metadata, timeout: timeout, content_type: content_type)
|
|
54
44
|
end
|
|
55
45
|
|
|
56
46
|
# Extract metadata from gRPC headers.
|
|
57
47
|
# @parameter headers [Protocol::HTTP::Headers]
|
|
58
48
|
# @returns [Hash] Metadata key-value pairs
|
|
49
|
+
# @deprecated Use {Metadata.extract} instead.
|
|
59
50
|
def self.extract_metadata(headers)
|
|
60
|
-
|
|
51
|
+
Kernel.warn("`Protocol::GRPC::Methods.extract_metadata` is deprecated; use `Protocol::GRPC::Metadata.extract` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
61
52
|
|
|
62
|
-
headers
|
|
63
|
-
# Skip reserved headers:
|
|
64
|
-
next if key.start_with?("grpc-") || key == "content-type" || key == "te"
|
|
65
|
-
|
|
66
|
-
# Decode binary headers:
|
|
67
|
-
if key.end_with?("-bin")
|
|
68
|
-
if value.is_a?(String)
|
|
69
|
-
value = Base64.strict_decode64(value)
|
|
70
|
-
elsif value.is_a?(Array)
|
|
71
|
-
value = value.map{|item| Base64.strict_decode64(item)}
|
|
72
|
-
end
|
|
73
|
-
else
|
|
74
|
-
value
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
metadata[key] = value
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
metadata
|
|
53
|
+
Metadata.extract(headers)
|
|
81
54
|
end
|
|
82
55
|
|
|
83
56
|
# Format timeout for grpc-timeout header.
|
|
84
57
|
# @parameter timeout [Numeric] Timeout in seconds
|
|
85
58
|
# @returns [String] e.g., "1000m" for 1 second
|
|
59
|
+
# @deprecated Use {Protocol::GRPC::Header::Timeout.format} instead.
|
|
86
60
|
def self.format_timeout(timeout)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
elsif timeout >= 60
|
|
91
|
-
"#{(timeout / 60).to_i}M"
|
|
92
|
-
elsif timeout >= 1
|
|
93
|
-
"#{timeout.to_i}S"
|
|
94
|
-
elsif timeout >= 0.001
|
|
95
|
-
"#{(timeout * 1000).to_i}m"
|
|
96
|
-
elsif timeout >= 0.000001
|
|
97
|
-
"#{(timeout * 1_000_000).to_i}u"
|
|
98
|
-
else
|
|
99
|
-
"#{(timeout * 1_000_000_000).to_i}n"
|
|
100
|
-
end
|
|
61
|
+
Kernel.warn("`Protocol::GRPC::Methods.format_timeout` is deprecated; use `Protocol::GRPC::Header::Timeout.format` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
62
|
+
|
|
63
|
+
Header::Timeout.format(timeout)
|
|
101
64
|
end
|
|
102
65
|
|
|
103
66
|
# Parse grpc-timeout header value.
|
|
104
67
|
# @parameter value [String] e.g., "1000m"
|
|
105
68
|
# @returns [Numeric | Nil] Timeout in seconds, or `Nil` if value is invalid
|
|
69
|
+
# @deprecated Use {Protocol::GRPC::Header::Timeout#to_seconds} instead.
|
|
106
70
|
def self.parse_timeout(value)
|
|
107
|
-
|
|
71
|
+
Kernel.warn("`Protocol::GRPC::Methods.parse_timeout` is deprecated; use `Protocol::GRPC::Header::Timeout#to_seconds` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
108
72
|
|
|
109
|
-
|
|
110
|
-
unit = value[-1]
|
|
73
|
+
return nil unless value
|
|
111
74
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
when "S" then amount
|
|
116
|
-
when "m" then amount / 1000.0
|
|
117
|
-
when "u" then amount / 1_000_000.0
|
|
118
|
-
when "n" then amount / 1_000_000_000.0
|
|
119
|
-
end
|
|
75
|
+
Header::Timeout.parse(value).to_seconds
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
return nil
|
|
120
78
|
end
|
|
121
79
|
end
|
|
122
80
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Protocol
|
|
7
|
+
module GRPC
|
|
8
|
+
# Provides operations for parsing and building gRPC request paths.
|
|
9
|
+
module Route
|
|
10
|
+
IDENTIFIER_PATTERN = "[A-Za-z][A-Za-z0-9_]*"
|
|
11
|
+
SERVICE_PATTERN = /\A#{IDENTIFIER_PATTERN}(?:\.#{IDENTIFIER_PATTERN})*\z/
|
|
12
|
+
METHOD_PATTERN = /\A#{IDENTIFIER_PATTERN}\z/
|
|
13
|
+
PATTERN = %r{\A/(#{IDENTIFIER_PATTERN}(?:\.#{IDENTIFIER_PATTERN})*)/(#{IDENTIFIER_PATTERN})\z}
|
|
14
|
+
private_constant :IDENTIFIER_PATTERN, :SERVICE_PATTERN, :METHOD_PATTERN, :PATTERN
|
|
15
|
+
|
|
16
|
+
# Parse a gRPC request path into its service and method names.
|
|
17
|
+
# @parameter path [String] The gRPC request path.
|
|
18
|
+
# @returns [Array(String)] The service and method names.
|
|
19
|
+
# @raises [ArgumentError] If the path does not contain valid protobuf service and method names.
|
|
20
|
+
def self.parse(path)
|
|
21
|
+
match = PATTERN.match(path) if path.is_a?(String)
|
|
22
|
+
|
|
23
|
+
unless match
|
|
24
|
+
raise ArgumentError, "Invalid gRPC route: #{path.inspect}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
[match[1], match[2]]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Build a gRPC request path from its service and method names.
|
|
31
|
+
# @parameter service_name [String] The fully qualified service name.
|
|
32
|
+
# @parameter method_name [String] The method name.
|
|
33
|
+
# @returns [String] The gRPC request path.
|
|
34
|
+
# @raises [ArgumentError] If either component is not a valid protobuf service or method name.
|
|
35
|
+
def self.build(service_name, method_name)
|
|
36
|
+
unless service_name.is_a?(String) && SERVICE_PATTERN.match?(service_name)
|
|
37
|
+
raise ArgumentError, "Invalid gRPC service name: #{service_name.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
unless method_name.is_a?(String) && METHOD_PATTERN.match?(method_name)
|
|
41
|
+
raise ArgumentError, "Invalid gRPC method name: #{method_name.inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
"/#{service_name}/#{method_name}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/protocol/grpc.rb
CHANGED
|
@@ -7,9 +7,10 @@ require_relative "grpc/version"
|
|
|
7
7
|
|
|
8
8
|
require_relative "grpc/status"
|
|
9
9
|
require_relative "grpc/error"
|
|
10
|
-
require_relative "grpc/
|
|
10
|
+
require_relative "grpc/route"
|
|
11
11
|
require_relative "grpc/header"
|
|
12
12
|
require_relative "grpc/metadata"
|
|
13
|
+
require_relative "grpc/methods"
|
|
13
14
|
require_relative "grpc/call"
|
|
14
15
|
require_relative "grpc/body/readable"
|
|
15
16
|
require_relative "grpc/body/writable"
|
data/readme.md
CHANGED
|
@@ -28,6 +28,16 @@ Please see the [project documentation](https://socketry.github.io/protocol-grpc/
|
|
|
28
28
|
|
|
29
29
|
Please see the [project releases](https://socketry.github.io/protocol-grpc/releases/index) for all releases.
|
|
30
30
|
|
|
31
|
+
### v0.13.0
|
|
32
|
+
|
|
33
|
+
- Added `Protocol::GRPC::Route` for building and parsing gRPC request paths.
|
|
34
|
+
- Added `Protocol::GRPC::Metadata.build` and `Protocol::GRPC::Metadata.extract`, replacing the corresponding deprecated `Protocol::GRPC::Methods` helpers.
|
|
35
|
+
|
|
36
|
+
### v0.12.0
|
|
37
|
+
|
|
38
|
+
- Added `Protocol::GRPC::Call#timeout` to expose the client-supplied gRPC timeout in seconds.
|
|
39
|
+
- Added `Protocol::GRPC::Header::Timeout.format` and deprecated `Protocol::GRPC::Methods.format_timeout` and `Protocol::GRPC::Methods.parse_timeout`.
|
|
40
|
+
|
|
31
41
|
### v0.11.0
|
|
32
42
|
|
|
33
43
|
- Rename `add_status!` to `assign_status!` to better reflect its purpose of assigning status information to headers or trailers.
|
|
@@ -66,11 +76,27 @@ Please see the [project releases](https://socketry.github.io/protocol-grpc/relea
|
|
|
66
76
|
|
|
67
77
|
We welcome contributions to this project.
|
|
68
78
|
|
|
69
|
-
1. Fork
|
|
79
|
+
1. Fork the repository.
|
|
70
80
|
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
71
|
-
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
81
|
+
3. Commit your changes (`git commit -am 'Add some feature.'`).
|
|
72
82
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
73
|
-
5. Create new
|
|
83
|
+
5. Create a new pull request.
|
|
84
|
+
|
|
85
|
+
### Running Tests
|
|
86
|
+
|
|
87
|
+
To run the test suite:
|
|
88
|
+
|
|
89
|
+
``` shell
|
|
90
|
+
bundle exec sus
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Making Releases
|
|
94
|
+
|
|
95
|
+
To make a new release:
|
|
96
|
+
|
|
97
|
+
``` shell
|
|
98
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
99
|
+
```
|
|
74
100
|
|
|
75
101
|
### Developer Certificate of Origin
|
|
76
102
|
|
data/releases.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.13.0
|
|
4
|
+
|
|
5
|
+
- Added `Protocol::GRPC::Route` for building and parsing gRPC request paths.
|
|
6
|
+
- Added `Protocol::GRPC::Metadata.build` and `Protocol::GRPC::Metadata.extract`, replacing the corresponding deprecated `Protocol::GRPC::Methods` helpers.
|
|
7
|
+
|
|
8
|
+
## v0.12.0
|
|
9
|
+
|
|
10
|
+
- Added `Protocol::GRPC::Call#timeout` to expose the client-supplied gRPC timeout in seconds.
|
|
11
|
+
- Added `Protocol::GRPC::Header::Timeout.format` and deprecated `Protocol::GRPC::Methods.format_timeout` and `Protocol::GRPC::Methods.parse_timeout`.
|
|
12
|
+
|
|
3
13
|
## v0.11.0
|
|
4
14
|
|
|
5
15
|
- Rename `add_status!` to `assign_status!` to better reflect its purpose of assigning status information to headers or trailers.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-grpc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- lib/protocol/grpc/metadata.rb
|
|
119
119
|
- lib/protocol/grpc/methods.rb
|
|
120
120
|
- lib/protocol/grpc/middleware.rb
|
|
121
|
+
- lib/protocol/grpc/route.rb
|
|
121
122
|
- lib/protocol/grpc/status.rb
|
|
122
123
|
- lib/protocol/grpc/version.rb
|
|
123
124
|
- license.md
|
|
@@ -127,6 +128,8 @@ homepage: https://github.com/socketry/protocol-grpc
|
|
|
127
128
|
licenses:
|
|
128
129
|
- MIT
|
|
129
130
|
metadata:
|
|
131
|
+
bug_tracker_uri: https://github.com/socketry/protocol-grpc/issues
|
|
132
|
+
changelog_uri: https://github.com/socketry/protocol-grpc/blob/main/releases.md
|
|
130
133
|
documentation_uri: https://socketry.github.io/protocol-grpc/
|
|
131
134
|
source_code_uri: https://github.com/socketry/protocol-grpc.git
|
|
132
135
|
rdoc_options: []
|
|
@@ -136,14 +139,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
136
139
|
requirements:
|
|
137
140
|
- - ">="
|
|
138
141
|
- !ruby/object:Gem::Version
|
|
139
|
-
version: '3.
|
|
142
|
+
version: '3.3'
|
|
140
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
144
|
requirements:
|
|
142
145
|
- - ">="
|
|
143
146
|
- !ruby/object:Gem::Version
|
|
144
147
|
version: '0'
|
|
145
148
|
requirements: []
|
|
146
|
-
rubygems_version: 4.0.
|
|
149
|
+
rubygems_version: 4.0.10
|
|
147
150
|
specification_version: 4
|
|
148
151
|
summary: Protocol abstractions for gRPC, built on top of protocol-http.
|
|
149
152
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|