protocol-grpc 0.1.0 → 0.3.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: 27870f6774a55a9ea77ac67685009adc591273272ba29dc43a41d9362c32d933
4
- data.tar.gz: 754b918b3865e479cbf853c2d0f4e3e7b8a5041e00ff95e6bc14f91a15341836
3
+ metadata.gz: a889e5eb3f6b74e8609a27a4667b62611ab8dc0515d24d9ecf70c4fee7e25b11
4
+ data.tar.gz: 56b5bc9e9d8a767db66ead55bcc97404e8092ef6a75a008d4bd893624391b808
5
5
  SHA512:
6
- metadata.gz: 686459772400743ad026f2ea4ee001e3967ce2e5d584ba041cb1edf5a25200c77893dd919b70e90984f340b8008eb6d2c7faca70731c67c46bd4665915ae4f68
7
- data.tar.gz: d1c6c68e07e91936ce968e15a942ca2c77dd5229d32d8eb778d351e8e19e6d52056bd9752abdb4757f2acfec810fc0f9216d6fa33ac6dc190ebe1a47c391513b
6
+ metadata.gz: 6aab2ee9f75421070a6348c39da680c7cd320e31021b9492f48d48832ab4caf580ffa77113a792e336cd87158890d22ed12d9e4650079954b33c99c7da85e422
7
+ data.tar.gz: 67c0e25c06ed8f5c8ad431233506f6133655d64a6e546d9767c74049d85b0fa6b54e37384e15e6458c429f0431779fad96c8b01e30821acd8f7952a260ccf3de
checksums.yaml.gz.sig ADDED
Binary file
@@ -130,3 +130,4 @@ call.deadline.exceeded? # => false
130
130
  call.peer # => Protocol::HTTP::Address
131
131
  ```
132
132
 
133
+
@@ -4,6 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http"
7
+ require "protocol/http/body/wrapper"
7
8
  require "zlib"
8
9
 
9
10
  module Protocol
@@ -12,56 +13,36 @@ module Protocol
12
13
  module Body
13
14
  # Represents a readable body for gRPC messages with length-prefixed framing.
14
15
  # 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
+
16
26
  # Initialize a new readable body for gRPC messages.
17
27
  # @parameter body [Protocol::HTTP::Body::Readable] The underlying HTTP body
18
28
  # @parameter message_class [Class | Nil] Protobuf message class with .decode method.
19
29
  # If `nil`, returns raw binary data (useful for channel adapters)
20
30
  # @parameter encoding [String | Nil] Compression encoding (from grpc-encoding header)
21
31
  def initialize(body, message_class: nil, encoding: nil)
22
- @body = body
32
+ super(body)
23
33
  @message_class = message_class
24
34
  @encoding = encoding
25
35
  @buffer = String.new.force_encoding(Encoding::BINARY)
26
- @closed = false
27
36
  end
28
37
 
29
- # @attribute [Protocol::HTTP::Body::Readable] The underlying HTTP body.
30
- attr_reader :body
31
-
32
38
  # @attribute [String | Nil] The compression encoding.
33
39
  attr_reader :encoding
34
40
 
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
-
61
41
  # Read the next gRPC message.
42
+ # Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages.
62
43
  # @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended
63
44
  def read
64
- return nil if closed?
45
+ return nil if @body.nil? || @body.empty?
65
46
 
66
47
  # Read 5-byte prefix: 1 byte compression flag + 4 bytes length
67
48
  prefix = read_exactly(5)
@@ -87,24 +68,6 @@ module Protocol
87
68
  end
88
69
  end
89
70
 
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
-
108
71
  private
109
72
 
110
73
  # Read exactly n bytes from the underlying body.
@@ -113,28 +76,18 @@ module Protocol
113
76
  def read_exactly(n)
114
77
  # Fill buffer until we have enough data:
115
78
  while @buffer.bytesize < n
116
- return nil if closed?
79
+ return nil if @body.nil? || @body.empty?
117
80
 
118
81
  # Read chunk from underlying body:
119
82
  chunk = @body.read
120
83
 
121
84
  if chunk.nil?
122
85
  # End of stream:
123
- if @body && !@closed
124
- @body.close
125
- @closed = true
126
- end
127
86
  return nil
128
87
  end
129
88
 
130
89
  # Append to buffer:
131
90
  @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
138
91
  end
139
92
 
140
93
  # Extract the required data:
@@ -12,9 +12,11 @@ module Protocol
12
12
  class Call
13
13
  # Initialize a new RPC call context.
14
14
  # @parameter request [Protocol::HTTP::Request] The HTTP request
15
+ # @parameter response [Protocol::HTTP::Response | Nil] The HTTP response (for setting metadata and trailers)
15
16
  # @parameter deadline [Async::Deadline | Nil] Deadline for the call
16
- def initialize(request, deadline: nil)
17
+ def initialize(request, response = nil, deadline: nil)
17
18
  @request = request
19
+ @response = response
18
20
  @deadline = deadline
19
21
  @cancelled = false
20
22
  end
@@ -22,6 +24,9 @@ module Protocol
22
24
  # @attribute [Protocol::HTTP::Request] The underlying HTTP request.
23
25
  attr_reader :request
24
26
 
27
+ # @attribute [Protocol::HTTP::Response | Nil] The HTTP response.
28
+ attr_reader :response
29
+
25
30
  # @attribute [Async::Deadline | Nil] The deadline for this call.
26
31
  attr_reader :deadline
27
32
 
@@ -20,9 +20,9 @@ module Protocol
20
20
  class Status
21
21
  # Initialize the status header with the given value.
22
22
  #
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).
24
24
  def initialize(value)
25
- @value = value.is_a?(String) ? value.to_i : value.to_i
25
+ @value = normalize_value(value)
26
26
  end
27
27
 
28
28
  # Get the status code as an integer.
@@ -40,12 +40,24 @@ module Protocol
40
40
  end
41
41
 
42
42
  # 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
44
44
  def <<(value)
45
- @value = value.is_a?(String) ? value.to_i : value.to_i
45
+ @value = normalize_value(value)
46
46
  self
47
47
  end
48
48
 
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
+
49
61
  # Whether this header is acceptable in HTTP trailers.
50
62
  # The `grpc-status` header can appear in trailers as per the gRPC specification.
51
63
  # @returns [Boolean] `true`, as grpc-status can appear in trailers.
@@ -7,16 +7,23 @@ require_relative "methods"
7
7
 
8
8
  module Protocol
9
9
  module GRPC
10
- # RPC method definition
11
- RPC = Struct.new(:request_class, :response_class, :streaming, :method, keyword_init: true) do
12
- def initialize(request_class:, response_class:, streaming: :unary, method: nil)
13
- super
14
- end
15
- end
16
-
17
10
  # Represents an interface definition for gRPC methods.
18
11
  # Can be used by both client stubs and server implementations.
19
12
  class Interface
13
+ # RPC method definition
14
+ RPC = Struct.new(:request_class, :response_class, :streaming, :method, keyword_init: true) do
15
+ def initialize(request_class:, response_class:, streaming: :unary, method: nil)
16
+ super
17
+ 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
25
+ end
26
+
20
27
  # Hook called when a subclass is created.
21
28
  # Initializes the RPC hash for the subclass.
22
29
  # @parameter subclass [Class] The subclass being created
@@ -33,6 +40,9 @@ module Protocol
33
40
  # @parameter streaming [Symbol] Streaming type (:unary, :server_streaming, :client_streaming, :bidirectional)
34
41
  # @parameter method [Symbol | Nil] Optional explicit Ruby method name (snake_case). If not provided, automatically converts PascalCase to snake_case.
35
42
  def self.rpc(name, **options)
43
+ # Ensure snake_case method name is always available
44
+ options[:method] ||= pascal_case_to_snake_case(name.to_s).to_sym
45
+
36
46
  @rpcs[name] = RPC.new(**options)
37
47
  end
38
48
 
@@ -44,12 +54,15 @@ module Protocol
44
54
  klass = self
45
55
  while klass && klass != Interface
46
56
  if klass.instance_variable_defined?(:@rpcs)
47
- rpc = klass.instance_variable_get(:@rpcs)[name]
48
- return rpc if rpc
57
+ if rpc = klass.instance_variable_get(:@rpcs)[name]
58
+ return rpc
59
+ end
49
60
  end
50
61
  klass = klass.superclass
51
62
  end
52
- nil
63
+
64
+ # Not found:
65
+ return nil
53
66
  end
54
67
 
55
68
  # Get all RPC definitions from this class and all parent classes.
@@ -69,21 +82,33 @@ module Protocol
69
82
  all_rpcs
70
83
  end
71
84
 
72
- # @attribute [String] The service name (e.g., "hello.Greeter").
73
- attr :name
74
-
75
85
  # Initialize a new interface instance.
76
86
  # @parameter name [String] Service name
77
87
  def initialize(name)
78
88
  @name = name
79
89
  end
80
90
 
91
+ # @attribute [String] The service name (e.g., "hello.Greeter").
92
+ attr :name
93
+
81
94
  # Build gRPC path for a method.
82
95
  # @parameter method_name [String, Symbol] Method name in PascalCase (e.g., :SayHello)
83
96
  # @returns [String] gRPC path with PascalCase method name
84
97
  def path(method_name)
85
98
  Methods.build_path(@name, method_name.to_s)
86
99
  end
100
+
101
+ private
102
+
103
+ # Convert PascalCase to snake_case.
104
+ # @parameter pascal_case [String] PascalCase string (e.g., "SayHello")
105
+ # @returns [String] snake_case string (e.g., "say_hello")
106
+ def self.pascal_case_to_snake_case(pascal_case)
107
+ pascal_case
108
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # Insert underscore before capital letters followed by lowercase
109
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2') # Insert underscore between lowercase/digit and uppercase
110
+ .downcase
111
+ end
87
112
  end
88
113
  end
89
114
  end
@@ -33,8 +33,22 @@ module Protocol
33
33
  status.to_i
34
34
  else
35
35
  # 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
38
52
  end
39
53
  end
40
54
 
@@ -7,6 +7,7 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module GRPC
10
- VERSION = "0.1.0"
10
+ VERSION = "0.3.0"
11
11
  end
12
12
  end
13
+
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.3.0
32
+
33
+ - **Breaking**: `Protocol::GRPC::Call` now takes a `response` object parameter instead of separate `response_headers`.
34
+ - **Breaking**: Removed `Call#response_headers` method. Use `call.response.headers` directly.
35
+ - Added `RPC#streaming?` method to check if an RPC is streaming.
36
+
37
+ ### v0.2.0
38
+
39
+ - `RPC#method` is always defined (snake case).
40
+
31
41
  ### v0.1.0
32
42
 
33
43
  - Initial design.
data/releases.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.0
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
+
9
+ ## v0.2.0
10
+
11
+ - `RPC#method` is always defined (snake case).
12
+
3
13
  ## v0.1.0
4
14
 
5
15
  - Initial design.
data.tar.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ H�����\�{̡���鴋V�D X�7�� �^:6'�B�C��ںR�w�x��͂�~˜��"c�q(?Ս���׻�v%sz���01�D�1�z��V���ۘL�~�'�/z�W?�1�-�6|��F�$����륁���`�y>�ˡqua�6��<n�\�
2
+ �<��x���x<$�O�O���u� ߣ̕��]]�s���#�X7'�^Һ�&��]�%o4��&6��+��{��w!�h��#A��=��j]�y����Z��
metadata CHANGED
@@ -1,12 +1,41 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-grpc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  bindir: bin
9
- cert_chain: []
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
10
39
  date: 1980-01-02 00:00:00.000000000 Z
11
40
  dependencies:
12
41
  - !ruby/object:Gem::Dependency
metadata.gz.sig ADDED
Binary file