Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions design.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ module Protocol

# Parse service and method from gRPC path
# @parameter path [String] e.g., "/my_service.Greeter/SayHello"
# @returns [Array(String, String)] [service, method]
# @returns [Tuple(String, String)] of service and method.
def self.parse_path(path)
parts = path.split("/")
[parts[1], parts[2]]
Expand Down Expand Up @@ -287,7 +287,7 @@ module Protocol

# Extract gRPC status message from headers
# @parameter headers [Protocol::HTTP::Headers]
# @returns [String, nil] Status message
# @returns [String | Nil] Status message
def self.extract_message(headers)
message = headers["grpc-message"]
message ? URI.decode_www_form_component(message) : nil
Expand Down Expand Up @@ -329,7 +329,7 @@ module Protocol
# @parameter body [Protocol::HTTP::Body::Readable] The underlying HTTP body
# @parameter message_class [Class, nil] Protobuf message class with .decode method
# If nil, returns raw binary data (useful for channel adapters)
# @parameter encoding [String, nil] Compression encoding (from grpc-encoding header)
# @parameter encoding [String | Nil] Compression encoding (from grpc-encoding header)
def initialize(body, message_class: nil, encoding: nil)
super(body)
@message_class = message_class
Expand Down Expand Up @@ -408,7 +408,7 @@ module Protocol
# Writes length-prefixed gRPC messages
# This is the standard writable body for gRPC - all gRPC requests use message framing
class Writable < Protocol::HTTP::Body::Writable
# @parameter encoding [String, nil] Compression encoding (gzip, deflate, identity)
# @parameter encoding [String | Nil] Compression encoding (gzip, deflate, identity)
# @parameter level [Integer] Compression level if encoding is used
def initialize(encoding: nil, level: Zlib::DEFAULT_COMPRESSION, **options)
super(**options)
Expand Down Expand Up @@ -574,7 +574,7 @@ module Protocol
end

# Get peer information (client address)
# @returns [String, nil]
# @returns [String | Nil]
def peer
@request.peer&.to_s
end
Expand Down
110 changes: 3 additions & 107 deletions lib/protocol/grpc/header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,120 +4,16 @@
# Copyright, 2025, by Samuel Williams.

require "protocol/http"
require "uri"

require_relative "status"
require_relative "header/status"
require_relative "header/message"
require_relative "header/metadata"

module Protocol
module GRPC
# @namespace
module Header
# The `grpc-status` header represents the gRPC status code.
#
# The `grpc-status` header contains a numeric status code (0-16) indicating the result of the RPC call.
# Status code 0 indicates success (OK), while other codes indicate various error conditions.
# This header can appear both as an initial header (for trailers-only responses) and as a trailer.
class Status
# Initialize the status header with the given value.
#
# @parameter value [String, Integer, Array] The status code as a string, integer, or array (takes first element).
def initialize(value)
@value = normalize_value(value)
end

# Get the status code as an integer.
#
# @returns [Integer] The status code.
def to_i
@value
end

# Serialize the status code to a string.
#
# @returns [String] The status code as a string.
def to_s
@value.to_s
end

# Merge another status value (takes the new value, as status should only appear once)
# @parameter value [String, Integer, Array] The new status code
def <<(value)
@value = normalize_value(value)
self
end

private

# Normalize a value to an integer status code.
# Handles arrays (from external clients), strings, and integers.
# @parameter value [String, Integer, Array] The raw value
# @returns [Integer] The normalized status code
def normalize_value(value)
# Handle Array case (may occur with external clients)
actual_value = value.is_a?(Array) ? value.flatten.compact.first : value
actual_value.to_i
end

# Whether this header is acceptable in HTTP trailers.
# The `grpc-status` header can appear in trailers as per the gRPC specification.
# @returns [Boolean] `true`, as grpc-status can appear in trailers.
def self.trailer?
true
end
end

# The `grpc-message` header represents the gRPC status message.
#
# The `grpc-message` header contains a human-readable error message, URL-encoded according to RFC 3986.
# This header is optional and typically only present when there's an error (non-zero status code).
# This header can appear both as an initial header (for trailers-only responses) and as a trailer.
class Message < String
# Initialize the message header with the given value.
#
# @parameter value [String] The message value (will be URL-encoded if not already encoded).
def initialize(value)
super(value.to_s)
end

# Decode the URL-encoded message.
#
# @returns [String] The decoded message.
def decode
URI.decode_www_form_component(self)
end

# Encode the message for use in headers.
#
# @parameter message [String] The message to encode.
# @returns [String] The URL-encoded message.
def self.encode(message)
URI.encode_www_form_component(message).gsub("+", "%20")
end

# Merge another message value (takes the new value, as message should only appear once)
# @parameter value [String] The new message value
def <<(value)
replace(value.to_s)
self
end

# Whether this header is acceptable in HTTP trailers.
# The `grpc-message` header can appear in trailers as per the gRPC specification.
# @returns [Boolean] `true`, as grpc-message can appear in trailers.
def self.trailer?
true
end
end

# Base class for custom gRPC metadata (allowed in trailers).
class Metadata < Protocol::HTTP::Header::Split
# Whether this header is acceptable in HTTP trailers.
# The `grpc-metadata` header can appear in trailers as per the gRPC specification.
# @returns [Boolean] `true`, as grpc-metadata can appear in trailers.
def self.trailer?
true
end
end
end

# Custom header policy for gRPC.
Expand Down
64 changes: 64 additions & 0 deletions lib/protocol/grpc/header/message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

require "uri"

module Protocol
module GRPC
module Header
# The `grpc-message` header represents the gRPC status message.
#
# The `grpc-message` header contains a human-readable error message, URL-encoded according to RFC 3986.
# This header is optional and typically only present when there's an error (non-zero status code).
# This header can appear both as an initial header (for trailers-only responses) and as a trailer.
class Message < String
# Parse a message from a header value.
#
# @parameter value [String] The header value to parse.
# @returns [Message] A new Message instance.
def self.parse(value)
new(value)
end

# Initialize the message header with the given value.
#
# @parameter value [String] The message value (will be URL-encoded if not already encoded).
def initialize(value)
super(value)
end

# Decode the URL-encoded message.
#
# @returns [String] The decoded message.
def decode
::URI.decode_www_form_component(self)
end

# Encode the message for use in headers.
#
# @parameter message [String] The message to encode.
# @returns [String] The URL-encoded message.
def self.encode(message)
URI.encode_www_form_component(message).gsub("+", "%20")
end

# Merge another message value (takes the new value, as message should only appear once)
# @parameter value [String] The new message value
def <<(value)
replace(value.to_s)

return self
end

# Whether this header is acceptable in HTTP trailers.
# The `grpc-message` header can appear in trailers as per the gRPC specification.
# @returns [Boolean] `true`, as grpc-message can appear in trailers.
def self.trailer?
true
end
end
end
end
end
22 changes: 22 additions & 0 deletions lib/protocol/grpc/header/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

require "protocol/http"

module Protocol
module GRPC
module Header
# Base class for custom gRPC metadata (allowed in trailers).
class Metadata < Protocol::HTTP::Header::Split
# Whether this header is acceptable in HTTP trailers.
# The `grpc-metadata` header can appear in trailers as per the gRPC specification.
# @returns [Boolean] `true`, as grpc-metadata can appear in trailers.
def self.trailer?
true
end
end
end
end
end
85 changes: 85 additions & 0 deletions lib/protocol/grpc/header/status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

module Protocol
module GRPC
module Header
# The `grpc-status` header represents the gRPC status code.
#
# The `grpc-status` header contains a numeric status code (0-16) indicating the result of the RPC call.
# Status code 0 indicates success (OK), while other codes indicate various error conditions.
# This header can appear both as an initial header (for trailers-only responses) and as a trailer.
class Status
# Parse a status code from a header value.
#
# @parameter value [String] The header value to parse.
# @returns [Status] A new Status instance.
def self.parse(value)
new(value.to_i)
end

# Initialize the status header with the given value.
#
# @parameter value [String | Integer] The status code as a string or integer.
def initialize(value)
@value = value.to_i
end

# Get the status code as an integer.
#
# @returns [Integer] The status code.
def to_i
@value
end

# Serialize the status code to a string.
#
# @returns [String] The status code as a string.
def to_s
@value.to_s
end

# Check equality with another status or integer value.
#
# @parameter other [Status | Integer] The value to compare with.
# @returns [Boolean] if the status codes are equal.
def ==(other)
@value == other.to_i
end

alias eql? ==

# Generate hash for use in Hash/Set collections.
#
# @returns [Integer] The hash value based on the status code.
def hash
@value.hash
end

# Check if this status represents success (status code 0).
#
# @returns [Boolean] `true` if the status code is 0 (OK).
def ok?
@value == 0
end

# Merge another status value (takes the new value, as status should only appear once)
# @parameter value [String | Integer] The new status code
def <<(value)
@value = value.to_i

return self
end

# Whether this header is acceptable in HTTP trailers.
# The `grpc-status` header can appear in trailers as per the gRPC specification.
# @returns [Boolean] `true`, as grpc-status can appear in trailers.
def self.trailer?
true
end
end
end
end
end
4 changes: 3 additions & 1 deletion lib/protocol/grpc/interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module GRPC
# Wrapper class to mark a message type as streamed.
# Used with the stream() helper method in RPC definitions.
class Streaming
# Initialize a new Streaming wrapper.
# @parameter message_class [Class] The message class being wrapped
def initialize(message_class)
@message_class = message_class
end
Expand Down Expand Up @@ -148,7 +150,7 @@ def initialize(name)
attr :name

# Build gRPC path for a method.
# @parameter method_name [String, Symbol] Method name in PascalCase (e.g., :SayHello)
# @parameter method_name [String | Symbol] Method name in PascalCase (e.g., :SayHello)
# @returns [String] gRPC path with PascalCase method name
def path(method_name)
Methods.build_path(@name, method_name.to_s)
Expand Down
2 changes: 1 addition & 1 deletion lib/protocol/grpc/methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def self.build_path(service, method)

# Parse service and method from gRPC path.
# @parameter path [String] e.g., "/my_service.Greeter/SayHello"
# @returns [Array(String, String)] [service, method]
# @returns [Array(String | String)] [service, method]
def self.parse_path(path)
parts = path.split("/")
[parts[1], parts[2]]
Expand Down
20 changes: 20 additions & 0 deletions test/protocol/grpc/header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

require "protocol/grpc/header"

describe Protocol::GRPC::HEADER_POLICY do
it "includes grpc-status mapping" do
expect(subject["grpc-status"]).to be == Protocol::GRPC::Header::Status
end

it "includes grpc-message mapping" do
expect(subject["grpc-message"]).to be == Protocol::GRPC::Header::Message
end

it "is frozen" do
expect(subject).to be(:frozen?)
end
end
Loading
Loading