Skip to content

Commit b006249

Browse files
Minor improvements.
1 parent c6514aa commit b006249

File tree

14 files changed

+81
-85
lines changed

14 files changed

+81
-85
lines changed

context/getting-started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,4 @@ call.deadline.exceeded? # => false
130130
call.peer # => Protocol::HTTP::Address
131131
```
132132

133+

context/index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ files:
1010
title: Getting Started
1111
description: This guide explains how to use `protocol-grpc` for building abstract
1212
gRPC interfaces.
13+

guides/getting-started/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,4 @@ call.deadline.exceeded? # => false
130130
call.peer # => Protocol::HTTP::Address
131131
```
132132

133+

lib/protocol/grpc/body/readable_body.rb

Lines changed: 15 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright, 2025, by Samuel Williams.
55

66
require "protocol/http"
7+
require "protocol/http/body/wrapper"
78
require "zlib"
89

910
module Protocol
@@ -12,56 +13,36 @@ module GRPC
1213
module Body
1314
# Represents a readable body for gRPC messages with length-prefixed framing.
1415
# This is the standard readable body for gRPC - all gRPC responses use message framing.
15-
class ReadableBody
16+
# Wraps the underlying HTTP body and transforms raw chunks into decoded gRPC messages.
17+
class ReadableBody < Protocol::HTTP::Body::Wrapper
18+
def self.wrap(message, **options)
19+
if body = message.body
20+
message.body = self.new(body, **options)
21+
end
22+
23+
return message.body
24+
end
25+
1626
# Initialize a new readable body for gRPC messages.
1727
# @parameter body [Protocol::HTTP::Body::Readable] The underlying HTTP body
1828
# @parameter message_class [Class | Nil] Protobuf message class with .decode method.
1929
# If `nil`, returns raw binary data (useful for channel adapters)
2030
# @parameter encoding [String | Nil] Compression encoding (from grpc-encoding header)
2131
def initialize(body, message_class: nil, encoding: nil)
22-
@body = body
32+
super(body)
2333
@message_class = message_class
2434
@encoding = encoding
2535
@buffer = String.new.force_encoding(Encoding::BINARY)
26-
@closed = false
2736
end
2837

29-
# @attribute [Protocol::HTTP::Body::Readable] The underlying HTTP body.
30-
attr_reader :body
31-
3238
# @attribute [String | Nil] The compression encoding.
3339
attr_reader :encoding
3440

35-
# Close the input body.
36-
# @parameter error [Exception | Nil] Optional error that caused the close
37-
# @returns [Nil]
38-
def close(error = nil)
39-
@closed = true
40-
41-
if @body
42-
@body.close(error)
43-
@body = nil
44-
end
45-
46-
nil
47-
end
48-
49-
# Check if the stream has been closed.
50-
# @returns [Boolean] `true` if the stream is closed, `false` otherwise
51-
def closed?
52-
@closed or @body.nil?
53-
end
54-
55-
# Check if there are any input chunks remaining.
56-
# @returns [Boolean] `true` if the body is empty, `false` otherwise
57-
def empty?
58-
@body.nil?
59-
end
60-
6141
# Read the next gRPC message.
42+
# Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages.
6243
# @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended
6344
def read
64-
return nil if closed?
45+
return nil if @body.nil? || @body.empty?
6546

6647
# Read 5-byte prefix: 1 byte compression flag + 4 bytes length
6748
prefix = read_exactly(5)
@@ -87,24 +68,6 @@ def read
8768
end
8869
end
8970

90-
# Enumerate all messages until finished, then invoke {close}.
91-
# @yields {|message| ...} The block to call with each message.
92-
def each
93-
return to_enum unless block_given?
94-
95-
error = nil
96-
begin
97-
while (message = read)
98-
yield message
99-
end
100-
rescue StandardError => e
101-
error = e
102-
raise
103-
ensure
104-
close(error)
105-
end
106-
end
107-
10871
private
10972

11073
# Read exactly n bytes from the underlying body.
@@ -113,28 +76,18 @@ def each
11376
def read_exactly(n)
11477
# Fill buffer until we have enough data:
11578
while @buffer.bytesize < n
116-
return nil if closed?
79+
return nil if @body.nil? || @body.empty?
11780

11881
# Read chunk from underlying body:
11982
chunk = @body.read
12083

12184
if chunk.nil?
12285
# End of stream:
123-
if @body && !@closed
124-
@body.close
125-
@closed = true
126-
end
12786
return nil
12887
end
12988

13089
# Append to buffer:
13190
@buffer << chunk.force_encoding(Encoding::BINARY)
132-
133-
# Check if body is empty and close if needed:
134-
if @body.empty?
135-
@body.close
136-
@closed = true
137-
end
13891
end
13992

14093
# Extract the required data:

lib/protocol/grpc/call.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@ module GRPC
1212
class Call
1313
# Initialize a new RPC call context.
1414
# @parameter request [Protocol::HTTP::Request] The HTTP request
15+
# @parameter response [Protocol::HTTP::Response | Nil] The HTTP response (for setting metadata and trailers)
1516
# @parameter deadline [Async::Deadline | Nil] Deadline for the call
16-
def initialize(request, deadline: nil)
17+
def initialize(request, response = nil, deadline: nil)
1718
@request = request
19+
@response = response
1820
@deadline = deadline
1921
@cancelled = false
2022
end
2123

2224
# @attribute [Protocol::HTTP::Request] The underlying HTTP request.
2325
attr_reader :request
2426

27+
# @attribute [Protocol::HTTP::Response | Nil] The HTTP response.
28+
attr_reader :response
29+
2530
# @attribute [Async::Deadline | Nil] The deadline for this call.
2631
attr_reader :deadline
2732

lib/protocol/grpc/header.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ module Header
2020
class Status
2121
# Initialize the status header with the given value.
2222
#
23-
# @parameter value [String, Integer] The status code as a string or integer.
23+
# @parameter value [String, Integer, Array] The status code as a string, integer, or array (takes first element).
2424
def initialize(value)
25-
@value = value.is_a?(String) ? value.to_i : value.to_i
25+
@value = normalize_value(value)
2626
end
2727

2828
# Get the status code as an integer.
@@ -40,12 +40,24 @@ def to_s
4040
end
4141

4242
# Merge another status value (takes the new value, as status should only appear once)
43-
# @parameter value [String, Integer] The new status code
43+
# @parameter value [String, Integer, Array] The new status code
4444
def <<(value)
45-
@value = value.is_a?(String) ? value.to_i : value.to_i
45+
@value = normalize_value(value)
4646
self
4747
end
4848

49+
private
50+
51+
# Normalize a value to an integer status code.
52+
# Handles arrays (from external clients), strings, and integers.
53+
# @parameter value [String, Integer, Array] The raw value
54+
# @returns [Integer] The normalized status code
55+
def normalize_value(value)
56+
# Handle Array case (may occur with external clients)
57+
actual_value = value.is_a?(Array) ? value.flatten.compact.first : value
58+
actual_value.to_i
59+
end
60+
4961
# Whether this header is acceptable in HTTP trailers.
5062
# The `grpc-status` header can appear in trailers as per the gRPC specification.
5163
# @returns [Boolean] `true`, as grpc-status can appear in trailers.

lib/protocol/grpc/interface.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ class Interface
1515
def initialize(request_class:, response_class:, streaming: :unary, method: nil)
1616
super
1717
end
18+
19+
# Check if this RPC is a streaming RPC (server, client, or bidirectional).
20+
# Server-side handlers for streaming RPCs are expected to block until all messages are sent.
21+
# @returns [Boolean] `true` if streaming, `false` if unary
22+
def streaming?
23+
streaming != :unary
24+
end
1825
end
1926

2027
# Hook called when a subclass is created.

lib/protocol/grpc/metadata.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,22 @@ def self.extract_status(headers)
3333
status.to_i
3434
else
3535
# Fallback for when header policy isn't used
36-
status_value = status.is_a?(Array) ? status.first : status.to_s
37-
status_value.to_i
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
3852
end
3953
end
4054

lib/protocol/grpc/version.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ module GRPC
1010
VERSION = "0.2.0"
1111
end
1212
end
13+

releases.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- **Breaking**: `Protocol::GRPC::Call` now takes a `response` object parameter instead of separate `response_headers`.
6+
- **Breaking**: Removed `Call#response_headers` method. Use `call.response.headers` directly.
7+
- Added `RPC#streaming?` method to check if an RPC is streaming.
8+
39
## v0.2.0
410

5-
- `RCP#method` is always defined (snake case).
11+
- `RPC#method` is always defined (snake case).
612

713
## v0.1.0
814

0 commit comments

Comments
 (0)