protocol-grpc 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82a5d69076bd57b3e2fe9c3e8d46f2caf79874136d4577664240179aa39f0966
4
- data.tar.gz: 1b46c0bde1016ebc61998c76592f64b97714b9b701d264c5a2dd4c64433b1745
3
+ metadata.gz: a991dcf85eaa66f7a1cbd8d31c919e82b37e0b2034ad594da58d342391b00590
4
+ data.tar.gz: 0fe139aeffb04fbaee3700bcfde98588625dc37706195598fa99dd632786396a
5
5
  SHA512:
6
- metadata.gz: c7aa9a55156f31361fe0c8afc57f74b3c69760e39c14985ec999250c507b8f9cad650ac70435dda2b7620eb3557d6105126a6a5fb2ad045b5fb0f306f1aa572d
7
- data.tar.gz: efe791eece6660faced232e8d1d9fc0a207ee5866b9905fa93cc6b78c1781c4d9f7c804d5083df76dffcc65b6798cd7e4f3fb07d185987ef8a99a7247f688b5f
6
+ metadata.gz: cbd273eb5cb2481235b14404bdc2742f95b3dee7a53383e1a69926e5b93508e2701bbba165d26023953b73cd9cc1438872d3fffa9dd1e77f2c5f0116c2ec95ce
7
+ data.tar.gz: 7e532651a9629d38558c1078eb98fb97b13c4d3c999d13ef22e01d8f6e31822bfdd10c11ce7e9640eb42db393f3441420e804fc7c6d6b74ddcdc49b3ceef560e
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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::Methods` and `Protocol::GRPC::Body::Writable`:
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::Methods.build_headers(timeout: 5.0)
79
- path = Protocol::GRPC::Methods.build_path("hello.Greeter", "SayHello")
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
- # Parse service and method from path
121
- service_name, method_name = Protocol::GRPC::Methods.parse_path(request.path)
119
+ # Parse the service and method from the path:
120
+ service_name, method_name = Protocol::GRPC::Route.parse(request.path)
122
121
 
123
- # Handle the request and return a response
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/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::Methods` (build/parse paths, headers, timeouts)
36
- 3. **Metadata** - `Protocol::GRPC::Metadata` (extract status, build trailers)
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::Methods`
125
+ #### 2. `Protocol::GRPC::Route` and `Protocol::GRPC::Metadata`
126
126
 
127
- Helper module for building gRPC-compatible HTTP requests:
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 Methods
133
- # Build gRPC path from service and method
134
- # @parameter service [String] e.g., "my_service.Greeter"
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
- # Parse service and method from gRPC path
142
- # @parameter path [String] e.g., "/my_service.Greeter/SayHello"
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.build_headers(metadata: {}, timeout: nil, content_type: "application/grpc+proto")
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.extract_metadata(headers)
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 ||= Methods.extract_metadata(@request.headers)
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 = Methods.parse_path(request.path)
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::Methods.build_headers(
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::Methods.build_path("my_service.Greeter", "SayHello")
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::Methods.parse_path(http_request.path)
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::Methods` (path parsing, header building) (✅ Designed)
1440
- - `Protocol::GRPC::Header` classes (Status, Message, Metadata) (✅ Designed)
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
-
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "async/deadline"
7
- require_relative "methods"
7
+ require_relative "metadata"
8
8
 
9
9
  module Protocol
10
10
  module GRPC
@@ -46,7 +46,7 @@ module Protocol
46
46
  # Extract metadata from request headers.
47
47
  # @returns [Hash] Custom metadata key-value pairs
48
48
  def metadata
49
- @metadata ||= Methods.extract_metadata(@request.headers)
49
+ @metadata ||= Metadata.extract(@request.headers)
50
50
  end
51
51
 
52
52
  # Get the timeout requested by the client.
@@ -3,7 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
- require_relative "methods"
6
+ require_relative "route"
7
7
 
8
8
  module Protocol
9
9
  module GRPC
@@ -159,7 +159,7 @@ module Protocol
159
159
  # @parameter method_name [String | Symbol] Method name in PascalCase (e.g., :SayHello)
160
160
  # @returns [String] gRPC path with PascalCase method name
161
161
  def path(method_name)
162
- Methods.build_path(@name, method_name.to_s)
162
+ Route.build(@name, method_name.to_s)
163
163
  end
164
164
 
165
165
  private
@@ -3,13 +3,67 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
+ require "base64"
7
+
6
8
  require_relative "header"
7
9
  require_relative "status"
8
10
 
9
11
  module Protocol
10
12
  module GRPC
11
- # @namespace
13
+ # Provides operations for building and extracting gRPC metadata.
12
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
+
13
67
  # Extract gRPC status from headers.
14
68
  # Returns Status::UNKNOWN if status is not present.
15
69
  #
@@ -3,10 +3,8 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
- require "base64"
7
- require "protocol/http"
8
-
9
- require_relative "header/timeout"
6
+ require_relative "metadata"
7
+ require_relative "route"
10
8
 
11
9
  module Protocol
12
10
  module GRPC
@@ -16,16 +14,21 @@ module Protocol
16
14
  # @parameter service [String] e.g., "my_service.Greeter"
17
15
  # @parameter method [String] e.g., "SayHello"
18
16
  # @returns [String] e.g., "/my_service.Greeter/SayHello"
17
+ # @deprecated Use {Route.build} instead.
19
18
  def self.build_path(service, method)
20
- "/#{service}/#{method}"
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)
21
22
  end
22
23
 
23
24
  # Parse service and method from gRPC path.
24
25
  # @parameter path [String] e.g., "/my_service.Greeter/SayHello"
25
26
  # @returns [Array(String | String)] [service, method]
27
+ # @deprecated Use {Route.parse} instead.
26
28
  def self.parse_path(path)
27
- parts = path.split("/")
28
- [parts[1], parts[2]]
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)
29
32
  end
30
33
 
31
34
  # Build gRPC request headers.
@@ -33,53 +36,21 @@ module Protocol
33
36
  # @parameter timeout [Numeric | Nil] Optional timeout in seconds
34
37
  # @parameter content_type [String] Content type (default: "application/grpc+proto")
35
38
  # @returns [Protocol::HTTP::Headers]
39
+ # @deprecated Use {Metadata.build} instead.
36
40
  def self.build_headers(metadata: {}, timeout: nil, content_type: "application/grpc+proto")
37
- headers = Protocol::HTTP::Headers.new(policy: Protocol::GRPC::HEADER_POLICY)
38
- headers["content-type"] = content_type
39
- headers["te"] = "trailers"
41
+ Kernel.warn("`Protocol::GRPC::Methods.build_headers` is deprecated; use `Protocol::GRPC::Metadata.build` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
40
42
 
41
- if timeout
42
- # Coerced to proper format by header policy:
43
- headers["grpc-timeout"] = timeout
44
- end
45
-
46
- metadata.each do |key, value|
47
- # Binary headers end with -bin and are base64 encoded:
48
- headers[key] = if key.end_with?("-bin")
49
- Base64.strict_encode64(value)
50
- else
51
- value.to_s
52
- end
53
- end
54
-
55
- headers
43
+ Metadata.build(metadata: metadata, timeout: timeout, content_type: content_type)
56
44
  end
57
45
 
58
46
  # Extract metadata from gRPC headers.
59
47
  # @parameter headers [Protocol::HTTP::Headers]
60
48
  # @returns [Hash] Metadata key-value pairs
49
+ # @deprecated Use {Metadata.extract} instead.
61
50
  def self.extract_metadata(headers)
62
- metadata = {}
63
-
64
- headers.to_h.each do |key, value|
65
- # Skip reserved headers:
66
- next if key.start_with?("grpc-") || key == "content-type" || key == "te"
67
-
68
- # Decode binary headers:
69
- if key.end_with?("-bin")
70
- if value.is_a?(String)
71
- value = Base64.strict_decode64(value)
72
- elsif value.is_a?(Array)
73
- value = value.map{|item| Base64.strict_decode64(item)}
74
- end
75
- else
76
- value
77
- end
78
-
79
- metadata[key] = value
80
- end
51
+ Kernel.warn("`Protocol::GRPC::Methods.extract_metadata` is deprecated; use `Protocol::GRPC::Metadata.extract` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
81
52
 
82
- metadata
53
+ Metadata.extract(headers)
83
54
  end
84
55
 
85
56
  # Format timeout for grpc-timeout header.
@@ -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
@@ -7,7 +7,7 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module GRPC
10
- VERSION = "0.12.0"
10
+ VERSION = "0.13.0"
11
11
  end
12
12
  end
13
13
 
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/methods"
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,11 @@ 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
+
31
36
  ### v0.12.0
32
37
 
33
38
  - Added `Protocol::GRPC::Call#timeout` to expose the client-supplied gRPC timeout in seconds.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## v0.12.0
4
9
 
5
10
  - Added `Protocol::GRPC::Call#timeout` to expose the client-supplied gRPC timeout in seconds.
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.12.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
metadata.gz.sig CHANGED
Binary file