Skip to content

Commit 3f59641

Browse files
RPC method is always defined.
1 parent b3b04d3 commit 3f59641

File tree

3 files changed

+147
-43
lines changed

3 files changed

+147
-43
lines changed

lib/protocol/grpc/interface.rb

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77

88
module Protocol
99
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-
1710
# Represents an interface definition for gRPC methods.
1811
# Can be used by both client stubs and server implementations.
1912
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+
end
19+
2020
# Hook called when a subclass is created.
2121
# Initializes the RPC hash for the subclass.
2222
# @parameter subclass [Class] The subclass being created
@@ -33,6 +33,9 @@ def self.inherited(subclass)
3333
# @parameter streaming [Symbol] Streaming type (:unary, :server_streaming, :client_streaming, :bidirectional)
3434
# @parameter method [Symbol | Nil] Optional explicit Ruby method name (snake_case). If not provided, automatically converts PascalCase to snake_case.
3535
def self.rpc(name, **options)
36+
# Ensure snake_case method name is always available
37+
options[:method] ||= pascal_case_to_snake_case(name.to_s).to_sym
38+
3639
@rpcs[name] = RPC.new(**options)
3740
end
3841

@@ -44,12 +47,15 @@ def self.lookup_rpc(name)
4447
klass = self
4548
while klass && klass != Interface
4649
if klass.instance_variable_defined?(:@rpcs)
47-
rpc = klass.instance_variable_get(:@rpcs)[name]
48-
return rpc if rpc
50+
if rpc = klass.instance_variable_get(:@rpcs)[name]
51+
return rpc
52+
end
4953
end
5054
klass = klass.superclass
5155
end
52-
nil
56+
57+
# Not found:
58+
return nil
5359
end
5460

5561
# Get all RPC definitions from this class and all parent classes.
@@ -69,21 +75,33 @@ def self.rpcs
6975
all_rpcs
7076
end
7177

72-
# @attribute [String] The service name (e.g., "hello.Greeter").
73-
attr :name
74-
7578
# Initialize a new interface instance.
7679
# @parameter name [String] Service name
7780
def initialize(name)
7881
@name = name
7982
end
8083

84+
# @attribute [String] The service name (e.g., "hello.Greeter").
85+
attr :name
86+
8187
# Build gRPC path for a method.
8288
# @parameter method_name [String, Symbol] Method name in PascalCase (e.g., :SayHello)
8389
# @returns [String] gRPC path with PascalCase method name
8490
def path(method_name)
8591
Methods.build_path(@name, method_name.to_s)
8692
end
93+
94+
private
95+
96+
# Convert PascalCase to snake_case.
97+
# @parameter pascal_case [String] PascalCase string (e.g., "SayHello")
98+
# @returns [String] snake_case string (e.g., "say_hello")
99+
def self.pascal_case_to_snake_case(pascal_case)
100+
pascal_case
101+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # Insert underscore before capital letters followed by lowercase
102+
.gsub(/([a-z\d])([A-Z])/, '\1_\2') # Insert underscore between lowercase/digit and uppercase
103+
.downcase
104+
end
87105
end
88106
end
89-
end
107+
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- `RCP#method` is always defined (snake case).
6+
37
## v0.1.0
48

59
- Initial design.

test/protocol/grpc/interface.rb

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
let(:response_class) {Class.new}
1111

1212
it "can define and retrieve RPC methods" do
13-
req_class = request_class
14-
res_class = response_class
13+
request_class = self.request_class
14+
response_class = self.response_class
1515

1616
interface_class = Class.new(Protocol::GRPC::Interface) do
17-
rpc :say_hello, request_class: req_class, response_class: res_class
17+
rpc :say_hello, request_class: request_class, response_class: response_class
1818
end
1919

2020
rpc = interface_class.lookup_rpc(:say_hello)
@@ -30,12 +30,12 @@
3030
end
3131

3232
it "can retrieve all RPCs" do
33-
req_class = request_class
34-
res_class = response_class
33+
request_class = self.request_class
34+
response_class = self.response_class
3535

3636
interface_class = Class.new(Protocol::GRPC::Interface) do
37-
rpc :method1, request_class: req_class, response_class: res_class
38-
rpc :method2, request_class: req_class, response_class: res_class, streaming: :server_streaming
37+
rpc :method1, request_class: request_class, response_class: response_class
38+
rpc :method2, request_class: request_class, response_class: response_class, streaming: :server_streaming
3939
end
4040

4141
rpcs = interface_class.rpcs
@@ -45,15 +45,15 @@
4545
end
4646

4747
it "inherits RPCs from parent class" do
48-
req_class = request_class
49-
res_class = response_class
48+
request_class = self.request_class
49+
response_class = self.response_class
5050

5151
base_class = Class.new(Protocol::GRPC::Interface) do
52-
rpc :base_method, request_class: req_class, response_class: res_class
52+
rpc :base_method, request_class: request_class, response_class: response_class
5353
end
5454

5555
subclass = Class.new(base_class) do
56-
rpc :sub_method, request_class: req_class, response_class: res_class
56+
rpc :sub_method, request_class: request_class, response_class: response_class
5757
end
5858

5959
# Subclass should have both methods
@@ -71,13 +71,13 @@
7171
end
7272

7373
it "can override RPCs in subclass" do
74-
req_class = request_class
75-
res_class = response_class
74+
request_class = self.request_class
75+
response_class = self.response_class
7676
other_request_class = Class.new
7777
other_response_class = Class.new
7878

7979
base_class = Class.new(Protocol::GRPC::Interface) do
80-
rpc :method, request_class: req_class, response_class: res_class
80+
rpc :method, request_class: request_class, response_class: response_class
8181
end
8282

8383
subclass = Class.new(base_class) do
@@ -98,19 +98,19 @@
9898
end
9999

100100
it "supports multiple levels of inheritance" do
101-
req_class = request_class
102-
res_class = response_class
101+
request_class = self.request_class
102+
response_class = self.response_class
103103

104104
level1 = Class.new(Protocol::GRPC::Interface) do
105-
rpc :level1_method, request_class: req_class, response_class: res_class
105+
rpc :level1_method, request_class: request_class, response_class: response_class
106106
end
107107

108108
level2 = Class.new(level1) do
109-
rpc :level2_method, request_class: req_class, response_class: res_class
109+
rpc :level2_method, request_class: request_class, response_class: response_class
110110
end
111111

112112
level3 = Class.new(level2) do
113-
rpc :level3_method, request_class: req_class, response_class: res_class
113+
rpc :level3_method, request_class: request_class, response_class: response_class
114114
end
115115

116116
# Level 3 should have all methods
@@ -130,15 +130,15 @@
130130
end
131131

132132
it "maintains separate RPC definitions for different classes" do
133-
req_class = request_class
134-
res_class = response_class
133+
request_class = self.request_class
134+
response_class = self.response_class
135135

136136
class1 = Class.new(Protocol::GRPC::Interface) do
137-
rpc :method1, request_class: req_class, response_class: res_class
137+
rpc :method1, request_class: request_class, response_class: response_class
138138
end
139139

140140
class2 = Class.new(Protocol::GRPC::Interface) do
141-
rpc :method2, request_class: req_class, response_class: res_class
141+
rpc :method2, request_class: request_class, response_class: response_class
142142
end
143143

144144
# Each class should only have its own RPCs
@@ -150,20 +150,102 @@
150150
end
151151

152152
it "supports explicit method name in RPC definition" do
153-
req_class = request_class
154-
res_class = response_class
153+
request_class = self.request_class
154+
response_class = self.response_class
155155

156156
# Create interface with explicit method name
157157
explicit_interface = Class.new(Protocol::GRPC::Interface) do
158-
rpc :XMLParser, request_class: req_class, response_class: res_class,
158+
rpc :XMLParser, request_class: request_class, response_class: response_class,
159159
method: :xml_parser
160160
end
161161

162162
rpc = explicit_interface.lookup_rpc(:XMLParser)
163-
expect(rpc).to be_a(Protocol::GRPC::RPC)
163+
expect(rpc).to be_a(Protocol::GRPC::Interface::RPC)
164164
expect(rpc.method).to be == :xml_parser
165-
expect(rpc.request_class).to be == req_class
166-
expect(rpc.response_class).to be == res_class
165+
expect(rpc.request_class).to be == request_class
166+
expect(rpc.response_class).to be == response_class
167+
end
168+
169+
with "method field auto-conversion" do
170+
it "always sets method field when not explicitly provided" do
171+
request_class = self.request_class
172+
response_class = self.response_class
173+
174+
interface_class = Class.new(Protocol::GRPC::Interface) do
175+
rpc :SayHello, request_class: request_class, response_class: response_class
176+
end
177+
178+
rpc = interface_class.lookup_rpc(:SayHello)
179+
expect(rpc.method).not.to be_nil
180+
expect(rpc.method).to be == :say_hello
181+
end
182+
183+
it "converts PascalCase to snake_case correctly" do
184+
request_class = self.request_class
185+
response_class = self.response_class
186+
187+
interface_class = Class.new(Protocol::GRPC::Interface) do
188+
rpc :SayHello, request_class: request_class, response_class: response_class
189+
rpc :UnaryCall, request_class: request_class, response_class: response_class
190+
rpc :ServerStreamingCall, request_class: request_class, response_class: response_class
191+
rpc :XMLParser, request_class: request_class, response_class: response_class
192+
end
193+
194+
expect(interface_class.lookup_rpc(:SayHello).method).to be == :say_hello
195+
expect(interface_class.lookup_rpc(:UnaryCall).method).to be == :unary_call
196+
expect(interface_class.lookup_rpc(:ServerStreamingCall).method).to be == :server_streaming_call
197+
expect(interface_class.lookup_rpc(:XMLParser).method).to be == :xml_parser
198+
end
199+
200+
it "uses explicit method name when provided" do
201+
request_class = self.request_class
202+
response_class = self.response_class
203+
204+
interface_class = Class.new(Protocol::GRPC::Interface) do
205+
rpc :SayHello, request_class: request_class, response_class: response_class,
206+
method: :greet_user
207+
rpc :XMLParser, request_class: request_class, response_class: response_class,
208+
method: :parse_xml
209+
end
210+
211+
expect(interface_class.lookup_rpc(:SayHello).method).to be == :greet_user
212+
expect(interface_class.lookup_rpc(:XMLParser).method).to be == :parse_xml
213+
end
214+
215+
it "ensures method field is never nil" do
216+
request_class = self.request_class
217+
response_class = self.response_class
218+
219+
interface_class = Class.new(Protocol::GRPC::Interface) do
220+
rpc :SayHello, request_class: request_class, response_class: response_class
221+
rpc :UnaryCall, request_class: request_class, response_class: response_class,
222+
method: :unary_call
223+
end
224+
225+
# Both should have method set
226+
rpc1 = interface_class.lookup_rpc(:SayHello)
227+
rpc2 = interface_class.lookup_rpc(:UnaryCall)
228+
229+
expect(rpc1.method).not.to be_nil
230+
expect(rpc2.method).not.to be_nil
231+
expect(rpc1.method).to be_a(Symbol)
232+
expect(rpc2.method).to be_a(Symbol)
233+
end
234+
235+
it "handles edge cases in PascalCase conversion" do
236+
request_class = self.request_class
237+
response_class = self.response_class
238+
239+
interface_class = Class.new(Protocol::GRPC::Interface) do
240+
rpc :HTTPRequest, request_class: request_class, response_class: response_class
241+
rpc :XMLHTTPRequest, request_class: request_class, response_class: response_class
242+
rpc :GetUserByID, request_class: request_class, response_class: response_class
243+
end
244+
245+
expect(interface_class.lookup_rpc(:HTTPRequest).method).to be == :http_request
246+
expect(interface_class.lookup_rpc(:XMLHTTPRequest).method).to be == :xmlhttp_request
247+
expect(interface_class.lookup_rpc(:GetUserByID).method).to be == :get_user_by_id
248+
end
167249
end
168250
end
169251

0 commit comments

Comments
 (0)