Skip to content

Commit 4ea891a

Browse files
Better compatibility with grpc Ruby generated code.
1 parent 56ca281 commit 4ea891a

File tree

6 files changed

+220
-9
lines changed

6 files changed

+220
-9
lines changed

context/getting-started.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,27 @@ This gem provides protocol-level abstractions only. To actually send requests ov
3939
require "protocol/grpc/interface"
4040

4141
class GreeterInterface < Protocol::GRPC::Interface
42-
rpc :SayHello, request_class: Hello::HelloRequest, response_class: Hello::HelloReply
43-
rpc :SayHelloAgain, request_class: Hello::HelloRequest, response_class: Hello::HelloReply,
44-
streaming: :server_streaming
42+
# Unary RPC (single request, single response)
43+
rpc :SayHello, Hello::HelloRequest, Hello::HelloReply
44+
45+
# Server streaming RPC using stream() decorator
46+
rpc :SayHelloMany, Hello::HelloRequest, stream(Hello::HelloReply)
47+
48+
# Client streaming RPC
49+
rpc :SayHelloRepeatedly, stream(Hello::HelloRequest), Hello::HelloReply
50+
51+
# Bidirectional streaming RPC
52+
rpc :ChatHello, stream(Hello::HelloRequest), stream(Hello::HelloReply)
4553
end
4654
```
4755

56+
The `stream()` decorator marks message types as streamed. You can also use the keyword syntax:
57+
58+
``` ruby
59+
rpc :SayHelloAgain, request_class: Hello::HelloRequest, response_class: Hello::HelloReply,
60+
streaming: :server_streaming
61+
```
62+
4863
### Building a Request
4964

5065
Build gRPC requests using `Protocol::GRPC::Methods` and `Protocol::GRPC::Body::Writable`:

guides/getting-started/readme.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,27 @@ This gem provides protocol-level abstractions only. To actually send requests ov
3939
require "protocol/grpc/interface"
4040

4141
class GreeterInterface < Protocol::GRPC::Interface
42-
rpc :SayHello, request_class: Hello::HelloRequest, response_class: Hello::HelloReply
43-
rpc :SayHelloAgain, request_class: Hello::HelloRequest, response_class: Hello::HelloReply,
44-
streaming: :server_streaming
42+
# Unary RPC (single request, single response)
43+
rpc :SayHello, Hello::HelloRequest, Hello::HelloReply
44+
45+
# Server streaming RPC using stream() decorator
46+
rpc :SayHelloMany, Hello::HelloRequest, stream(Hello::HelloReply)
47+
48+
# Client streaming RPC
49+
rpc :SayHelloRepeatedly, stream(Hello::HelloRequest), Hello::HelloReply
50+
51+
# Bidirectional streaming RPC
52+
rpc :ChatHello, stream(Hello::HelloRequest), stream(Hello::HelloReply)
4553
end
4654
```
4755

56+
The `stream()` decorator marks message types as streamed. You can also use the keyword syntax:
57+
58+
``` ruby
59+
rpc :SayHelloAgain, request_class: Hello::HelloRequest, response_class: Hello::HelloReply,
60+
streaming: :server_streaming
61+
```
62+
4863
### Building a Request
4964

5065
Build gRPC requests using `Protocol::GRPC::Methods` and `Protocol::GRPC::Body::Writable`:

lib/protocol/grpc/interface.rb

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88
module Protocol
99
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+
def initialize(message_class)
14+
@message_class = message_class
15+
end
16+
17+
# @attribute [Class] The wrapped message class
18+
attr :message_class
19+
end
20+
1021
# Represents an interface definition for gRPC methods.
1122
# Can be used by both client stubs and server implementations.
1223
class Interface
@@ -24,6 +35,21 @@ def streaming?
2435
end
2536
end
2637

38+
# Helper method to mark a message type as streamed in RPC definitions.
39+
# Can be called directly within Interface subclasses without the Protocol::GRPC prefix.
40+
# @parameter message_class [Class] The message class to mark as streamed
41+
# @returns [Streaming] A wrapper indicating this type is streamed
42+
#
43+
# @example Define streaming RPCs
44+
# class MyService < Protocol::GRPC::Interface
45+
# rpc :sum, stream(Num), Num # client streaming
46+
# rpc :fib, FibArgs, stream(Num) # server streaming
47+
# rpc :chat, stream(Msg), stream(Msg) # bidirectional streaming
48+
# end
49+
def self.stream(message_class)
50+
Streaming.new(message_class)
51+
end
52+
2753
# Hook called when a subclass is created.
2854
# Initializes the RPC hash for the subclass.
2955
# @parameter subclass [Class] The subclass being created
@@ -35,13 +61,41 @@ def self.inherited(subclass)
3561

3662
# Define an RPC method.
3763
# @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
64+
# @parameter request_class [Class | Streaming] Request message class, optionally wrapped with stream()
65+
# @parameter response_class [Class | Streaming] Response message class, optionally wrapped with stream()
4066
# @parameter streaming [Symbol] Streaming type (:unary, :server_streaming, :client_streaming, :bidirectional)
67+
# This is automatically inferred from stream() decorators if not explicitly provided
4168
# @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)
69+
#
70+
# @example Using stream() decorator syntax
71+
# rpc :div, DivArgs, DivReply # unary
72+
# rpc :sum, stream(Num), Num # client streaming
73+
# rpc :fib, FibArgs, stream(Num) # server streaming
74+
# rpc :chat, stream(DivArgs), stream(DivReply) # bidirectional streaming
75+
def self.rpc(name, request_class = nil, response_class = nil, **options)
4376
options[:name] = name
4477

78+
# Check if request or response are wrapped with stream()
79+
request_streaming = request_class.is_a?(Streaming)
80+
response_streaming = response_class.is_a?(Streaming)
81+
82+
# Unwrap StreamWrapper if present
83+
options[:request_class] ||= request_streaming ? request_class.message_class : request_class
84+
options[:response_class] ||= response_streaming ? response_class.message_class : response_class
85+
86+
# Auto-detect streaming type from stream() decorators if not explicitly set
87+
if !options.key?(:streaming)
88+
if request_streaming && response_streaming
89+
options[:streaming] = :bidirectional
90+
elsif request_streaming
91+
options[:streaming] = :client_streaming
92+
elsif response_streaming
93+
options[:streaming] = :server_streaming
94+
else
95+
options[:streaming] = :unary
96+
end
97+
end
98+
4599
# Ensure snake_case method name is always available
46100
options[:method] ||= pascal_case_to_snake_case(name.to_s).to_sym
47101

test/protocol/grpc/body/readable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,4 @@ def write_message(message, compressed: false)
138138
end
139139

140140

141+

test/protocol/grpc/body/writable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,4 @@ def wrong_message.to_proto
298298
end
299299

300300

301+

test/protocol/grpc/interface.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,4 +301,129 @@
301301
expect(rpc.method).to be == :greet_user
302302
end
303303
end
304+
305+
with "stream() decorator syntax" do
306+
it "supports unary RPC (no stream)" do
307+
request_class = self.request_class
308+
response_class = self.response_class
309+
310+
interface_class = Class.new(Protocol::GRPC::Interface) do
311+
rpc :div, request_class, response_class
312+
end
313+
314+
rpc = interface_class.lookup_rpc(:div)
315+
expect(rpc.name).to be == :div
316+
expect(rpc.request_class).to be == request_class
317+
expect(rpc.response_class).to be == response_class
318+
expect(rpc.streaming).to be == :unary
319+
expect(rpc.streaming?).to be == false
320+
end
321+
322+
it "supports client streaming with stream() on request" do
323+
request_class = self.request_class
324+
response_class = self.response_class
325+
326+
interface_class = Class.new(Protocol::GRPC::Interface) do
327+
rpc :sum, stream(request_class), response_class
328+
end
329+
330+
rpc = interface_class.lookup_rpc(:sum)
331+
expect(rpc.name).to be == :sum
332+
expect(rpc.request_class).to be == request_class
333+
expect(rpc.response_class).to be == response_class
334+
expect(rpc.streaming).to be == :client_streaming
335+
expect(rpc.streaming?).to be == true
336+
end
337+
338+
it "supports server streaming with stream() on response" do
339+
request_class = self.request_class
340+
response_class = self.response_class
341+
342+
interface_class = Class.new(Protocol::GRPC::Interface) do
343+
rpc :fib, request_class, stream(response_class)
344+
end
345+
346+
rpc = interface_class.lookup_rpc(:fib)
347+
expect(rpc.name).to be == :fib
348+
expect(rpc.request_class).to be == request_class
349+
expect(rpc.response_class).to be == response_class
350+
expect(rpc.streaming).to be == :server_streaming
351+
expect(rpc.streaming?).to be == true
352+
end
353+
354+
it "supports bidirectional streaming with stream() on both" do
355+
request_class = self.request_class
356+
response_class = self.response_class
357+
358+
interface_class = Class.new(Protocol::GRPC::Interface) do
359+
rpc :div_many, stream(request_class), stream(response_class)
360+
end
361+
362+
rpc = interface_class.lookup_rpc(:div_many)
363+
expect(rpc.name).to be == :div_many
364+
expect(rpc.request_class).to be == request_class
365+
expect(rpc.response_class).to be == response_class
366+
expect(rpc.streaming).to be == :bidirectional
367+
expect(rpc.streaming?).to be == true
368+
end
369+
370+
it "explicit streaming option overrides stream() decorator" do
371+
request_class = self.request_class
372+
response_class = self.response_class
373+
374+
interface_class = Class.new(Protocol::GRPC::Interface) do
375+
rpc :custom, stream(request_class), response_class, streaming: :unary
376+
end
377+
378+
rpc = interface_class.lookup_rpc(:custom)
379+
expect(rpc.request_class).to be == request_class
380+
expect(rpc.response_class).to be == response_class
381+
expect(rpc.streaming).to be == :unary
382+
expect(rpc.streaming?).to be == false
383+
end
384+
385+
it "unwraps message classes from Streaming wrapper" do
386+
request_class = self.request_class
387+
response_class = self.response_class
388+
389+
interface_class = Class.new(Protocol::GRPC::Interface) do
390+
rpc :test, stream(request_class), stream(response_class)
391+
end
392+
393+
rpc = interface_class.lookup_rpc(:test)
394+
395+
# Should unwrap to actual classes, not Streaming instances
396+
expect(rpc.request_class).to be == request_class
397+
expect(rpc.response_class).to be == response_class
398+
expect(rpc.request_class).not.to be_a(Protocol::GRPC::Streaming)
399+
expect(rpc.response_class).not.to be_a(Protocol::GRPC::Streaming)
400+
end
401+
402+
it "can use stream() directly in Interface subclass" do
403+
request_class = self.request_class
404+
response_class = self.response_class
405+
406+
interface_class = Class.new(Protocol::GRPC::Interface) do
407+
# Use stream() without Protocol::GRPC prefix
408+
rpc :sum, stream(request_class), response_class
409+
rpc :fib, request_class, stream(response_class)
410+
rpc :chat, stream(request_class), stream(response_class)
411+
end
412+
413+
sum_rpc = interface_class.lookup_rpc(:sum)
414+
expect(sum_rpc.streaming).to be == :client_streaming
415+
expect(sum_rpc.request_class).to be == request_class
416+
expect(sum_rpc.response_class).to be == response_class
417+
418+
fib_rpc = interface_class.lookup_rpc(:fib)
419+
expect(fib_rpc.streaming).to be == :server_streaming
420+
expect(fib_rpc.request_class).to be == request_class
421+
expect(fib_rpc.response_class).to be == response_class
422+
423+
chat_rpc = interface_class.lookup_rpc(:chat)
424+
expect(chat_rpc.streaming).to be == :bidirectional
425+
expect(chat_rpc.request_class).to be == request_class
426+
expect(chat_rpc.response_class).to be == response_class
427+
end
428+
end
304429
end

0 commit comments

Comments
 (0)