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.
@@ -4,120 +4,16 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http"
7
- require "uri"
8
7
 
9
8
  require_relative "status"
9
+ require_relative "header/status"
10
+ require_relative "header/message"
11
+ require_relative "header/metadata"
10
12
 
11
13
  module Protocol
12
14
  module GRPC
13
15
  # @namespace
14
16
  module Header
15
- # The `grpc-status` header represents the gRPC status code.
16
- #
17
- # The `grpc-status` header contains a numeric status code (0-16) indicating the result of the RPC call.
18
- # Status code 0 indicates success (OK), while other codes indicate various error conditions.
19
- # This header can appear both as an initial header (for trailers-only responses) and as a trailer.
20
- class Status
21
- # Initialize the status header with the given value.
22
- #
23
- # @parameter value [String, Integer, Array] The status code as a string, integer, or array (takes first element).
24
- def initialize(value)
25
- @value = normalize_value(value)
26
- end
27
-
28
- # Get the status code as an integer.
29
- #
30
- # @returns [Integer] The status code.
31
- def to_i
32
- @value
33
- end
34
-
35
- # Serialize the status code to a string.
36
- #
37
- # @returns [String] The status code as a string.
38
- def to_s
39
- @value.to_s
40
- end
41
-
42
- # Merge another status value (takes the new value, as status should only appear once)
43
- # @parameter value [String, Integer, Array] The new status code
44
- def <<(value)
45
- @value = normalize_value(value)
46
- self
47
- end
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
-
61
- # Whether this header is acceptable in HTTP trailers.
62
- # The `grpc-status` header can appear in trailers as per the gRPC specification.
63
- # @returns [Boolean] `true`, as grpc-status can appear in trailers.
64
- def self.trailer?
65
- true
66
- end
67
- end
68
-
69
- # The `grpc-message` header represents the gRPC status message.
70
- #
71
- # The `grpc-message` header contains a human-readable error message, URL-encoded according to RFC 3986.
72
- # This header is optional and typically only present when there's an error (non-zero status code).
73
- # This header can appear both as an initial header (for trailers-only responses) and as a trailer.
74
- class Message < String
75
- # Initialize the message header with the given value.
76
- #
77
- # @parameter value [String] The message value (will be URL-encoded if not already encoded).
78
- def initialize(value)
79
- super(value.to_s)
80
- end
81
-
82
- # Decode the URL-encoded message.
83
- #
84
- # @returns [String] The decoded message.
85
- def decode
86
- URI.decode_www_form_component(self)
87
- end
88
-
89
- # Encode the message for use in headers.
90
- #
91
- # @parameter message [String] The message to encode.
92
- # @returns [String] The URL-encoded message.
93
- def self.encode(message)
94
- URI.encode_www_form_component(message).gsub("+", "%20")
95
- end
96
-
97
- # Merge another message value (takes the new value, as message should only appear once)
98
- # @parameter value [String] The new message value
99
- def <<(value)
100
- replace(value.to_s)
101
- self
102
- end
103
-
104
- # Whether this header is acceptable in HTTP trailers.
105
- # The `grpc-message` header can appear in trailers as per the gRPC specification.
106
- # @returns [Boolean] `true`, as grpc-message can appear in trailers.
107
- def self.trailer?
108
- true
109
- end
110
- end
111
-
112
- # Base class for custom gRPC metadata (allowed in trailers).
113
- class Metadata < Protocol::HTTP::Header::Split
114
- # Whether this header is acceptable in HTTP trailers.
115
- # The `grpc-metadata` header can appear in trailers as per the gRPC specification.
116
- # @returns [Boolean] `true`, as grpc-metadata can appear in trailers.
117
- def self.trailer?
118
- true
119
- end
120
- end
121
17
  end
122
18
 
123
19
  # Custom header policy for gRPC.
@@ -7,6 +7,19 @@ require_relative "methods"
7
7
 
8
8
  module Protocol
9
9
  module GRPC
10
+ # Wrapper class to mark a message type as streamed.
11
+ # Used with the stream() helper method in RPC definitions.
12
+ class Streaming
13
+ # Initialize a new Streaming wrapper.
14
+ # @parameter message_class [Class] The message class being wrapped
15
+ def initialize(message_class)
16
+ @message_class = message_class
17
+ end
18
+
19
+ # @attribute [Class] The wrapped message class
20
+ attr :message_class
21
+ end
22
+
10
23
  # Represents an interface definition for gRPC methods.
11
24
  # Can be used by both client stubs and server implementations.
12
25
  class Interface
@@ -24,6 +37,21 @@ module Protocol
24
37
  end
25
38
  end
26
39
 
40
+ # Helper method to mark a message type as streamed in RPC definitions.
41
+ # Can be called directly within Interface subclasses without the Protocol::GRPC prefix.
42
+ # @parameter message_class [Class] The message class to mark as streamed
43
+ # @returns [Streaming] A wrapper indicating this type is streamed
44
+ #
45
+ # @example Define streaming RPCs
46
+ # class MyService < Protocol::GRPC::Interface
47
+ # rpc :sum, stream(Num), Num # client streaming
48
+ # rpc :fib, FibArgs, stream(Num) # server streaming
49
+ # rpc :chat, stream(Msg), stream(Msg) # bidirectional streaming
50
+ # end
51
+ def self.stream(message_class)
52
+ Streaming.new(message_class)
53
+ end
54
+
27
55
  # Hook called when a subclass is created.
28
56
  # Initializes the RPC hash for the subclass.
29
57
  # @parameter subclass [Class] The subclass being created
@@ -35,13 +63,41 @@ module Protocol
35
63
 
36
64
  # Define an RPC method.
37
65
  # @parameter name [Symbol] Method name in PascalCase (e.g., :SayHello, matching .proto file)
38
- # @parameter request_class [Class] Request message class
39
- # @parameter response_class [Class] Response message class
66
+ # @parameter request_class [Class | Streaming] Request message class, optionally wrapped with stream()
67
+ # @parameter response_class [Class | Streaming] Response message class, optionally wrapped with stream()
40
68
  # @parameter streaming [Symbol] Streaming type (:unary, :server_streaming, :client_streaming, :bidirectional)
69
+ # This is automatically inferred from stream() decorators if not explicitly provided
41
70
  # @parameter method [Symbol | Nil] Optional explicit Ruby method name (snake_case). If not provided, automatically converts PascalCase to snake_case.
42
- def self.rpc(name, **options)
71
+ #
72
+ # @example Using stream() decorator syntax
73
+ # rpc :div, DivArgs, DivReply # unary
74
+ # rpc :sum, stream(Num), Num # client streaming
75
+ # rpc :fib, FibArgs, stream(Num) # server streaming
76
+ # rpc :chat, stream(DivArgs), stream(DivReply) # bidirectional streaming
77
+ def self.rpc(name, request_class = nil, response_class = nil, **options)
43
78
  options[:name] = name
44
79
 
80
+ # Check if request or response are wrapped with stream()
81
+ request_streaming = request_class.is_a?(Streaming)
82
+ response_streaming = response_class.is_a?(Streaming)
83
+
84
+ # Unwrap StreamWrapper if present
85
+ options[:request_class] ||= request_streaming ? request_class.message_class : request_class
86
+ options[:response_class] ||= response_streaming ? response_class.message_class : response_class
87
+
88
+ # Auto-detect streaming type from stream() decorators if not explicitly set
89
+ if !options.key?(:streaming)
90
+ if request_streaming && response_streaming
91
+ options[:streaming] = :bidirectional
92
+ elsif request_streaming
93
+ options[:streaming] = :client_streaming
94
+ elsif response_streaming
95
+ options[:streaming] = :server_streaming
96
+ else
97
+ options[:streaming] = :unary
98
+ end
99
+ end
100
+
45
101
  # Ensure snake_case method name is always available
46
102
  options[:method] ||= pascal_case_to_snake_case(name.to_s).to_sym
47
103
 
@@ -94,7 +150,7 @@ module Protocol
94
150
  attr :name
95
151
 
96
152
  # Build gRPC path for a method.
97
- # @parameter method_name [String, Symbol] Method name in PascalCase (e.g., :SayHello)
153
+ # @parameter method_name [String | Symbol] Method name in PascalCase (e.g., :SayHello)
98
154
  # @returns [String] gRPC path with PascalCase method name
99
155
  def path(method_name)
100
156
  Methods.build_path(@name, method_name.to_s)
@@ -20,7 +20,7 @@ module Protocol
20
20
 
21
21
  # Parse service and method from gRPC path.
22
22
  # @parameter path [String] e.g., "/my_service.Greeter/SayHello"
23
- # @returns [Array(String, String)] [service, method]
23
+ # @returns [Array(String | String)] [service, method]
24
24
  def self.parse_path(path)
25
25
  parts = path.split("/")
26
26
  [parts[1], parts[2]]
@@ -7,7 +7,7 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module GRPC
10
- VERSION = "0.5.1"
10
+ VERSION = "0.7.0"
11
11
  end
12
12
  end
13
13
 
data/lib/protocol/grpc.rb CHANGED
@@ -11,8 +11,8 @@ require_relative "grpc/methods"
11
11
  require_relative "grpc/header"
12
12
  require_relative "grpc/metadata"
13
13
  require_relative "grpc/call"
14
- require_relative "grpc/body/readable_body"
15
- require_relative "grpc/body/writable_body"
14
+ require_relative "grpc/body/readable"
15
+ require_relative "grpc/body/writable"
16
16
  require_relative "grpc/interface"
17
17
  require_relative "grpc/middleware"
18
18
  require_relative "grpc/health_check"
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.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -102,11 +102,16 @@ files:
102
102
  - context/index.yaml
103
103
  - design.md
104
104
  - lib/protocol/grpc.rb
105
+ - lib/protocol/grpc/body/readable.rb
105
106
  - lib/protocol/grpc/body/readable_body.rb
107
+ - lib/protocol/grpc/body/writable.rb
106
108
  - lib/protocol/grpc/body/writable_body.rb
107
109
  - lib/protocol/grpc/call.rb
108
110
  - lib/protocol/grpc/error.rb
109
111
  - lib/protocol/grpc/header.rb
112
+ - lib/protocol/grpc/header/message.rb
113
+ - lib/protocol/grpc/header/metadata.rb
114
+ - lib/protocol/grpc/header/status.rb
110
115
  - lib/protocol/grpc/health_check.rb
111
116
  - lib/protocol/grpc/interface.rb
112
117
  - lib/protocol/grpc/metadata.rb
@@ -137,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
142
  - !ruby/object:Gem::Version
138
143
  version: '0'
139
144
  requirements: []
140
- rubygems_version: 3.6.9
145
+ rubygems_version: 4.0.3
141
146
  specification_version: 4
142
147
  summary: Protocol abstractions for gRPC, built on top of protocol-http.
143
148
  test_files: []
metadata.gz.sig CHANGED
Binary file