protocol-grpc 0.5.1 → 0.7.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 +26 -11
- data/design.md +135 -135
- data/lib/protocol/grpc/body/readable.rb +123 -0
- data/lib/protocol/grpc/body/readable_body.rb +10 -111
- data/lib/protocol/grpc/body/writable.rb +101 -0
- data/lib/protocol/grpc/body/writable_body.rb +10 -89
- data/lib/protocol/grpc/header/message.rb +64 -0
- data/lib/protocol/grpc/header/metadata.rb +22 -0
- data/lib/protocol/grpc/header/status.rb +85 -0
- data/lib/protocol/grpc/header.rb +3 -107
- data/lib/protocol/grpc/interface.rb +60 -4
- data/lib/protocol/grpc/methods.rb +1 -1
- data/lib/protocol/grpc/version.rb +1 -1
- data/lib/protocol/grpc.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +7 -2
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http"
|
|
7
|
+
require "protocol/http/body/wrapper"
|
|
8
|
+
require "zlib"
|
|
9
|
+
|
|
10
|
+
module Protocol
|
|
11
|
+
module GRPC
|
|
12
|
+
# @namespace
|
|
13
|
+
module Body
|
|
14
|
+
# Represents a readable body for gRPC messages with length-prefixed framing.
|
|
15
|
+
# This is the standard readable body for gRPC - all gRPC responses use message framing.
|
|
16
|
+
# Wraps the underlying HTTP body and transforms raw chunks into decoded gRPC messages.
|
|
17
|
+
class Readable < Protocol::HTTP::Body::Wrapper
|
|
18
|
+
# Wrap the body of a message.
|
|
19
|
+
#
|
|
20
|
+
# @parameter message [Request | Response] The message to wrap.
|
|
21
|
+
# @parameter options [Hash] The options to pass to the initializer.
|
|
22
|
+
# @returns [Readable | Nil] The wrapped body or `nil` if the message has no body.
|
|
23
|
+
def self.wrap(message, **options)
|
|
24
|
+
if body = message.body
|
|
25
|
+
message.body = self.new(body, **options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return message.body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Initialize a new readable body for gRPC messages.
|
|
32
|
+
# @parameter body [Protocol::HTTP::Body::Readable] The underlying HTTP body
|
|
33
|
+
# @parameter message_class [Class | Nil] Protobuf message class with .decode method.
|
|
34
|
+
# If `nil`, returns raw binary data (useful for channel adapters)
|
|
35
|
+
# @parameter encoding [String | Nil] Compression encoding (from grpc-encoding header)
|
|
36
|
+
def initialize(body, message_class: nil, encoding: nil)
|
|
37
|
+
super(body)
|
|
38
|
+
@message_class = message_class
|
|
39
|
+
@encoding = encoding
|
|
40
|
+
@buffer = String.new.force_encoding(Encoding::BINARY)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @attribute [String | Nil] The compression encoding.
|
|
44
|
+
attr_reader :encoding
|
|
45
|
+
|
|
46
|
+
# Read the next gRPC message.
|
|
47
|
+
# Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages.
|
|
48
|
+
# @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended
|
|
49
|
+
def read
|
|
50
|
+
return nil if @body.nil? || @body.empty?
|
|
51
|
+
|
|
52
|
+
# Read 5-byte prefix: 1 byte compression flag + 4 bytes length
|
|
53
|
+
prefix = read_exactly(5)
|
|
54
|
+
return nil unless prefix
|
|
55
|
+
|
|
56
|
+
compressed = prefix[0].unpack1("C") == 1
|
|
57
|
+
length = prefix[1..4].unpack1("N")
|
|
58
|
+
|
|
59
|
+
# Read the message body:
|
|
60
|
+
data = read_exactly(length)
|
|
61
|
+
return nil unless data
|
|
62
|
+
|
|
63
|
+
# Decompress if needed:
|
|
64
|
+
data = decompress(data) if compressed
|
|
65
|
+
|
|
66
|
+
# Decode using message class if provided, otherwise return binary:
|
|
67
|
+
# This allows binary mode for channel adapters
|
|
68
|
+
if @message_class
|
|
69
|
+
# Use protobuf gem's decode method:
|
|
70
|
+
@message_class.decode(data)
|
|
71
|
+
else
|
|
72
|
+
data # Return raw binary
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# 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
|
|
81
|
+
def read_exactly(n)
|
|
82
|
+
# Fill buffer until we have enough data:
|
|
83
|
+
while @buffer.bytesize < n
|
|
84
|
+
return nil if @body.nil? || @body.empty?
|
|
85
|
+
|
|
86
|
+
# Read chunk from underlying body:
|
|
87
|
+
chunk = @body.read
|
|
88
|
+
|
|
89
|
+
if chunk.nil?
|
|
90
|
+
# End of stream:
|
|
91
|
+
return nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Append to buffer:
|
|
95
|
+
@buffer << chunk.force_encoding(Encoding::BINARY)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Extract the required data:
|
|
99
|
+
data = @buffer[0...n]
|
|
100
|
+
@buffer = @buffer[n..]
|
|
101
|
+
data
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Decompress data using the configured encoding.
|
|
105
|
+
# @parameter data [String] The compressed data
|
|
106
|
+
# @returns [String] The decompressed data
|
|
107
|
+
# @raises [Error] If decompression fails
|
|
108
|
+
def decompress(data)
|
|
109
|
+
case @encoding
|
|
110
|
+
when "gzip"
|
|
111
|
+
Zlib::Gunzip.new.inflate(data)
|
|
112
|
+
when "deflate"
|
|
113
|
+
Zlib::Inflate.inflate(data)
|
|
114
|
+
else
|
|
115
|
+
data
|
|
116
|
+
end
|
|
117
|
+
rescue StandardError => error
|
|
118
|
+
raise Error.new(Status::INTERNAL, "Failed to decompress message: #{error.message}")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -3,121 +3,20 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
require
|
|
6
|
+
# Compatibility shim for the old file name.
|
|
7
|
+
# This file is deprecated and will be removed in a future version.
|
|
8
|
+
# Please update your code to require 'protocol/grpc/body/readable' instead.
|
|
9
|
+
|
|
10
|
+
warn "Requiring 'protocol/grpc/body/readable_body' is deprecated. Please require 'protocol/grpc/body/readable' instead.", uplevel: 1 if $VERBOSE
|
|
11
|
+
|
|
12
|
+
require_relative "readable"
|
|
9
13
|
|
|
10
14
|
module Protocol
|
|
11
15
|
module GRPC
|
|
12
|
-
# @namespace
|
|
13
16
|
module Body
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
class ReadableBody < Protocol::HTTP::Body::Wrapper
|
|
18
|
-
# Wrap the body of a message.
|
|
19
|
-
#
|
|
20
|
-
# @parameter message [Request | Response] The message to wrap.
|
|
21
|
-
# @parameter options [Hash] The options to pass to the initializer.
|
|
22
|
-
# @returns [ReadableBody | Nil] The wrapped body or `nil` if the message has no body.
|
|
23
|
-
def self.wrap(message, **options)
|
|
24
|
-
if body = message.body
|
|
25
|
-
message.body = self.new(body, **options)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
return message.body
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Initialize a new readable body for gRPC messages.
|
|
32
|
-
# @parameter body [Protocol::HTTP::Body::Readable] The underlying HTTP body
|
|
33
|
-
# @parameter message_class [Class | Nil] Protobuf message class with .decode method.
|
|
34
|
-
# If `nil`, returns raw binary data (useful for channel adapters)
|
|
35
|
-
# @parameter encoding [String | Nil] Compression encoding (from grpc-encoding header)
|
|
36
|
-
def initialize(body, message_class: nil, encoding: nil)
|
|
37
|
-
super(body)
|
|
38
|
-
@message_class = message_class
|
|
39
|
-
@encoding = encoding
|
|
40
|
-
@buffer = String.new.force_encoding(Encoding::BINARY)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# @attribute [String | Nil] The compression encoding.
|
|
44
|
-
attr_reader :encoding
|
|
45
|
-
|
|
46
|
-
# Read the next gRPC message.
|
|
47
|
-
# Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages.
|
|
48
|
-
# @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended
|
|
49
|
-
def read
|
|
50
|
-
return nil if @body.nil? || @body.empty?
|
|
51
|
-
|
|
52
|
-
# Read 5-byte prefix: 1 byte compression flag + 4 bytes length
|
|
53
|
-
prefix = read_exactly(5)
|
|
54
|
-
return nil unless prefix
|
|
55
|
-
|
|
56
|
-
compressed = prefix[0].unpack1("C") == 1
|
|
57
|
-
length = prefix[1..4].unpack1("N")
|
|
58
|
-
|
|
59
|
-
# Read the message body:
|
|
60
|
-
data = read_exactly(length)
|
|
61
|
-
return nil unless data
|
|
62
|
-
|
|
63
|
-
# Decompress if needed:
|
|
64
|
-
data = decompress(data) if compressed
|
|
65
|
-
|
|
66
|
-
# Decode using message class if provided, otherwise return binary:
|
|
67
|
-
# This allows binary mode for channel adapters
|
|
68
|
-
if @message_class
|
|
69
|
-
# Use protobuf gem's decode method:
|
|
70
|
-
@message_class.decode(data)
|
|
71
|
-
else
|
|
72
|
-
data # Return raw binary
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
# 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
|
|
81
|
-
def read_exactly(n)
|
|
82
|
-
# Fill buffer until we have enough data:
|
|
83
|
-
while @buffer.bytesize < n
|
|
84
|
-
return nil if @body.nil? || @body.empty?
|
|
85
|
-
|
|
86
|
-
# Read chunk from underlying body:
|
|
87
|
-
chunk = @body.read
|
|
88
|
-
|
|
89
|
-
if chunk.nil?
|
|
90
|
-
# End of stream:
|
|
91
|
-
return nil
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Append to buffer:
|
|
95
|
-
@buffer << chunk.force_encoding(Encoding::BINARY)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Extract the required data:
|
|
99
|
-
data = @buffer[0...n]
|
|
100
|
-
@buffer = @buffer[n..]
|
|
101
|
-
data
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Decompress data using the configured encoding.
|
|
105
|
-
# @parameter data [String] The compressed data
|
|
106
|
-
# @returns [String] The decompressed data
|
|
107
|
-
# @raises [Error] If decompression fails
|
|
108
|
-
def decompress(data)
|
|
109
|
-
case @encoding
|
|
110
|
-
when "gzip"
|
|
111
|
-
Zlib::Gunzip.new.inflate(data)
|
|
112
|
-
when "deflate"
|
|
113
|
-
Zlib::Inflate.inflate(data)
|
|
114
|
-
else
|
|
115
|
-
data
|
|
116
|
-
end
|
|
117
|
-
rescue StandardError => error
|
|
118
|
-
raise Error.new(Status::INTERNAL, "Failed to decompress message: #{error.message}")
|
|
119
|
-
end
|
|
120
|
-
end
|
|
17
|
+
# Compatibility alias for the old class name.
|
|
18
|
+
# @deprecated Use {Readable} instead.
|
|
19
|
+
ReadableBody = Readable
|
|
121
20
|
end
|
|
122
21
|
end
|
|
123
22
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http"
|
|
7
|
+
require "protocol/http/body/writable"
|
|
8
|
+
require "zlib"
|
|
9
|
+
require "stringio"
|
|
10
|
+
|
|
11
|
+
module Protocol
|
|
12
|
+
module GRPC
|
|
13
|
+
# @namespace
|
|
14
|
+
module Body
|
|
15
|
+
# Represents a writable body for gRPC messages with length-prefixed framing.
|
|
16
|
+
# This is the standard writable body for gRPC - all gRPC requests use message framing.
|
|
17
|
+
class Writable < Protocol::HTTP::Body::Writable
|
|
18
|
+
# Initialize a new writable body for gRPC messages.
|
|
19
|
+
# @parameter encoding [String | Nil] Compression encoding (gzip, deflate, identity)
|
|
20
|
+
# @parameter level [Integer] Compression level if encoding is used
|
|
21
|
+
# @parameter message_class [Class | Nil] Expected message class for validation
|
|
22
|
+
def initialize(encoding: nil, level: Zlib::DEFAULT_COMPRESSION, message_class: nil, **options)
|
|
23
|
+
super(**options)
|
|
24
|
+
@encoding = encoding
|
|
25
|
+
@level = level
|
|
26
|
+
@message_class = message_class
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @attribute [String | Nil] The compression encoding.
|
|
30
|
+
attr_reader :encoding
|
|
31
|
+
|
|
32
|
+
# @attribute [Class | Nil] The expected message class for validation.
|
|
33
|
+
attr_reader :message_class
|
|
34
|
+
|
|
35
|
+
# Write a message with gRPC framing.
|
|
36
|
+
# @parameter message [Object, String] Protobuf message instance or raw binary data
|
|
37
|
+
# @parameter compressed [Boolean | Nil] Whether to compress this specific message. If `nil`, uses the encoding setting.
|
|
38
|
+
def write(message, compressed: nil)
|
|
39
|
+
# Validate message type if message_class is specified:
|
|
40
|
+
if @message_class && !message.is_a?(String)
|
|
41
|
+
unless message.is_a?(@message_class)
|
|
42
|
+
raise TypeError, "Expected #{@message_class}, got #{message.class}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Encode message to binary if it's not already a string:
|
|
47
|
+
# This supports both high-level (protobuf objects) and low-level (binary) usage
|
|
48
|
+
data = if message.is_a?(String)
|
|
49
|
+
message # Already binary, use as-is (for channel adapters)
|
|
50
|
+
elsif message.respond_to?(:to_proto)
|
|
51
|
+
# Use protobuf gem's to_proto method:
|
|
52
|
+
message.to_proto
|
|
53
|
+
elsif message.respond_to?(:encode)
|
|
54
|
+
# Use encode method:
|
|
55
|
+
message.encode
|
|
56
|
+
else
|
|
57
|
+
raise ArgumentError, "Message must respond to :to_proto or :encode"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Determine if we should compress this message:
|
|
61
|
+
# If compressed param is nil, use the encoding setting
|
|
62
|
+
should_compress = compressed.nil? ? (@encoding && @encoding != "identity") : compressed
|
|
63
|
+
|
|
64
|
+
# Compress if requested:
|
|
65
|
+
data = compress(data) if should_compress
|
|
66
|
+
|
|
67
|
+
# Build prefix: compression flag + length
|
|
68
|
+
compression_flag = should_compress ? 1 : 0
|
|
69
|
+
length = data.bytesize
|
|
70
|
+
prefix = [compression_flag].pack("C") + [length].pack("N")
|
|
71
|
+
|
|
72
|
+
# Write prefix + data to underlying body:
|
|
73
|
+
super(prefix + data) # Call Protocol::HTTP::Body::Writable#write
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
protected
|
|
77
|
+
|
|
78
|
+
# Compress data using the configured encoding.
|
|
79
|
+
# @parameter data [String] The data to compress
|
|
80
|
+
# @returns [String] The compressed data
|
|
81
|
+
# @raises [Error] If compression fails
|
|
82
|
+
def compress(data)
|
|
83
|
+
case @encoding
|
|
84
|
+
when "gzip"
|
|
85
|
+
io = StringIO.new
|
|
86
|
+
gz = Zlib::GzipWriter.new(io, @level)
|
|
87
|
+
gz.write(data)
|
|
88
|
+
gz.close
|
|
89
|
+
io.string
|
|
90
|
+
when "deflate"
|
|
91
|
+
Zlib::Deflate.deflate(data, @level)
|
|
92
|
+
else
|
|
93
|
+
data # No compression or identity
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => error
|
|
96
|
+
raise Error.new(Status::INTERNAL, "Failed to compress message: #{error.message}")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -3,99 +3,20 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
require
|
|
9
|
-
|
|
6
|
+
# Compatibility shim for the old file name.
|
|
7
|
+
# This file is deprecated and will be removed in a future version.
|
|
8
|
+
# Please update your code to require 'protocol/grpc/body/writable' instead.
|
|
9
|
+
|
|
10
|
+
warn "Requiring 'protocol/grpc/body/writable_body' is deprecated. Please require 'protocol/grpc/body/writable' instead.", uplevel: 1 if $VERBOSE
|
|
11
|
+
|
|
12
|
+
require_relative "writable"
|
|
10
13
|
|
|
11
14
|
module Protocol
|
|
12
15
|
module GRPC
|
|
13
|
-
# @namespace
|
|
14
16
|
module Body
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
# Initialize a new writable body for gRPC messages.
|
|
19
|
-
# @parameter encoding [String | Nil] Compression encoding (gzip, deflate, identity)
|
|
20
|
-
# @parameter level [Integer] Compression level if encoding is used
|
|
21
|
-
# @parameter message_class [Class | Nil] Expected message class for validation
|
|
22
|
-
def initialize(encoding: nil, level: Zlib::DEFAULT_COMPRESSION, message_class: nil, **options)
|
|
23
|
-
super(**options)
|
|
24
|
-
@encoding = encoding
|
|
25
|
-
@level = level
|
|
26
|
-
@message_class = message_class
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# @attribute [String | Nil] The compression encoding.
|
|
30
|
-
attr_reader :encoding
|
|
31
|
-
|
|
32
|
-
# @attribute [Class | Nil] The expected message class for validation.
|
|
33
|
-
attr_reader :message_class
|
|
34
|
-
|
|
35
|
-
# Write a message with gRPC framing.
|
|
36
|
-
# @parameter message [Object, String] Protobuf message instance or raw binary data
|
|
37
|
-
# @parameter compressed [Boolean | Nil] Whether to compress this specific message. If `nil`, uses the encoding setting.
|
|
38
|
-
def write(message, compressed: nil)
|
|
39
|
-
# Validate message type if message_class is specified:
|
|
40
|
-
if @message_class && !message.is_a?(String)
|
|
41
|
-
unless message.is_a?(@message_class)
|
|
42
|
-
raise TypeError, "Expected #{@message_class}, got #{message.class}"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Encode message to binary if it's not already a string:
|
|
47
|
-
# This supports both high-level (protobuf objects) and low-level (binary) usage
|
|
48
|
-
data = if message.is_a?(String)
|
|
49
|
-
message # Already binary, use as-is (for channel adapters)
|
|
50
|
-
elsif message.respond_to?(:to_proto)
|
|
51
|
-
# Use protobuf gem's to_proto method:
|
|
52
|
-
message.to_proto
|
|
53
|
-
elsif message.respond_to?(:encode)
|
|
54
|
-
# Use encode method:
|
|
55
|
-
message.encode
|
|
56
|
-
else
|
|
57
|
-
raise ArgumentError, "Message must respond to :to_proto or :encode"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Determine if we should compress this message:
|
|
61
|
-
# If compressed param is nil, use the encoding setting
|
|
62
|
-
should_compress = compressed.nil? ? (@encoding && @encoding != "identity") : compressed
|
|
63
|
-
|
|
64
|
-
# Compress if requested:
|
|
65
|
-
data = compress(data) if should_compress
|
|
66
|
-
|
|
67
|
-
# Build prefix: compression flag + length
|
|
68
|
-
compression_flag = should_compress ? 1 : 0
|
|
69
|
-
length = data.bytesize
|
|
70
|
-
prefix = [compression_flag].pack("C") + [length].pack("N")
|
|
71
|
-
|
|
72
|
-
# Write prefix + data to underlying body:
|
|
73
|
-
super(prefix + data) # Call Protocol::HTTP::Body::Writable#write
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
protected
|
|
77
|
-
|
|
78
|
-
# Compress data using the configured encoding.
|
|
79
|
-
# @parameter data [String] The data to compress
|
|
80
|
-
# @returns [String] The compressed data
|
|
81
|
-
# @raises [Error] If compression fails
|
|
82
|
-
def compress(data)
|
|
83
|
-
case @encoding
|
|
84
|
-
when "gzip"
|
|
85
|
-
io = StringIO.new
|
|
86
|
-
gz = Zlib::GzipWriter.new(io, @level)
|
|
87
|
-
gz.write(data)
|
|
88
|
-
gz.close
|
|
89
|
-
io.string
|
|
90
|
-
when "deflate"
|
|
91
|
-
Zlib::Deflate.deflate(data, @level)
|
|
92
|
-
else
|
|
93
|
-
data # No compression or identity
|
|
94
|
-
end
|
|
95
|
-
rescue StandardError => error
|
|
96
|
-
raise Error.new(Status::INTERNAL, "Failed to compress message: #{error.message}")
|
|
97
|
-
end
|
|
98
|
-
end
|
|
17
|
+
# Compatibility alias for the old class name.
|
|
18
|
+
# @deprecated Use {Writable} instead.
|
|
19
|
+
WritableBody = Writable
|
|
99
20
|
end
|
|
100
21
|
end
|
|
101
22
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Protocol
|
|
9
|
+
module GRPC
|
|
10
|
+
module Header
|
|
11
|
+
# The `grpc-message` header represents the gRPC status message.
|
|
12
|
+
#
|
|
13
|
+
# The `grpc-message` header contains a human-readable error message, URL-encoded according to RFC 3986.
|
|
14
|
+
# This header is optional and typically only present when there's an error (non-zero status code).
|
|
15
|
+
# This header can appear both as an initial header (for trailers-only responses) and as a trailer.
|
|
16
|
+
class Message < String
|
|
17
|
+
# Parse a message from a header value.
|
|
18
|
+
#
|
|
19
|
+
# @parameter value [String] The header value to parse.
|
|
20
|
+
# @returns [Message] A new Message instance.
|
|
21
|
+
def self.parse(value)
|
|
22
|
+
new(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Initialize the message header with the given value.
|
|
26
|
+
#
|
|
27
|
+
# @parameter value [String] The message value (will be URL-encoded if not already encoded).
|
|
28
|
+
def initialize(value)
|
|
29
|
+
super(value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Decode the URL-encoded message.
|
|
33
|
+
#
|
|
34
|
+
# @returns [String] The decoded message.
|
|
35
|
+
def decode
|
|
36
|
+
::URI.decode_www_form_component(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Encode the message for use in headers.
|
|
40
|
+
#
|
|
41
|
+
# @parameter message [String] The message to encode.
|
|
42
|
+
# @returns [String] The URL-encoded message.
|
|
43
|
+
def self.encode(message)
|
|
44
|
+
URI.encode_www_form_component(message).gsub("+", "%20")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Merge another message value (takes the new value, as message should only appear once)
|
|
48
|
+
# @parameter value [String] The new message value
|
|
49
|
+
def <<(value)
|
|
50
|
+
replace(value.to_s)
|
|
51
|
+
|
|
52
|
+
return self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Whether this header is acceptable in HTTP trailers.
|
|
56
|
+
# The `grpc-message` header can appear in trailers as per the gRPC specification.
|
|
57
|
+
# @returns [Boolean] `true`, as grpc-message can appear in trailers.
|
|
58
|
+
def self.trailer?
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http"
|
|
7
|
+
|
|
8
|
+
module Protocol
|
|
9
|
+
module GRPC
|
|
10
|
+
module Header
|
|
11
|
+
# Base class for custom gRPC metadata (allowed in trailers).
|
|
12
|
+
class Metadata < Protocol::HTTP::Header::Split
|
|
13
|
+
# Whether this header is acceptable in HTTP trailers.
|
|
14
|
+
# The `grpc-metadata` header can appear in trailers as per the gRPC specification.
|
|
15
|
+
# @returns [Boolean] `true`, as grpc-metadata can appear in trailers.
|
|
16
|
+
def self.trailer?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Protocol
|
|
7
|
+
module GRPC
|
|
8
|
+
module Header
|
|
9
|
+
# The `grpc-status` header represents the gRPC status code.
|
|
10
|
+
#
|
|
11
|
+
# The `grpc-status` header contains a numeric status code (0-16) indicating the result of the RPC call.
|
|
12
|
+
# Status code 0 indicates success (OK), while other codes indicate various error conditions.
|
|
13
|
+
# This header can appear both as an initial header (for trailers-only responses) and as a trailer.
|
|
14
|
+
class Status
|
|
15
|
+
# Parse a status code from a header value.
|
|
16
|
+
#
|
|
17
|
+
# @parameter value [String] The header value to parse.
|
|
18
|
+
# @returns [Status] A new Status instance.
|
|
19
|
+
def self.parse(value)
|
|
20
|
+
new(value.to_i)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Initialize the status header with the given value.
|
|
24
|
+
#
|
|
25
|
+
# @parameter value [String | Integer] The status code as a string or integer.
|
|
26
|
+
def initialize(value)
|
|
27
|
+
@value = value.to_i
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the status code as an integer.
|
|
31
|
+
#
|
|
32
|
+
# @returns [Integer] The status code.
|
|
33
|
+
def to_i
|
|
34
|
+
@value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Serialize the status code to a string.
|
|
38
|
+
#
|
|
39
|
+
# @returns [String] The status code as a string.
|
|
40
|
+
def to_s
|
|
41
|
+
@value.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check equality with another status or integer value.
|
|
45
|
+
#
|
|
46
|
+
# @parameter other [Status | Integer] The value to compare with.
|
|
47
|
+
# @returns [Boolean] if the status codes are equal.
|
|
48
|
+
def ==(other)
|
|
49
|
+
@value == other.to_i
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias eql? ==
|
|
53
|
+
|
|
54
|
+
# Generate hash for use in Hash/Set collections.
|
|
55
|
+
#
|
|
56
|
+
# @returns [Integer] The hash value based on the status code.
|
|
57
|
+
def hash
|
|
58
|
+
@value.hash
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if this status represents success (status code 0).
|
|
62
|
+
#
|
|
63
|
+
# @returns [Boolean] `true` if the status code is 0 (OK).
|
|
64
|
+
def ok?
|
|
65
|
+
@value == 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Merge another status value (takes the new value, as status should only appear once)
|
|
69
|
+
# @parameter value [String | Integer] The new status code
|
|
70
|
+
def <<(value)
|
|
71
|
+
@value = value.to_i
|
|
72
|
+
|
|
73
|
+
return self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether this header is acceptable in HTTP trailers.
|
|
77
|
+
# The `grpc-status` header can appear in trailers as per the gRPC specification.
|
|
78
|
+
# @returns [Boolean] `true`, as grpc-status can appear in trailers.
|
|
79
|
+
def self.trailer?
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|