protocol-grpc 0.5.1 → 0.6.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: 121472581aa9d3d84bc5b1542bbe6d49274180ae0129d151a2b8f6aca9377026
4
- data.tar.gz: 820130b179cf141d525cc483900a14edbc68705c6f318e469c258c1e6cb0735d
3
+ metadata.gz: 13f745749ba7c9ac86eff356ee01f6d655c3996a1633ab9091c76e4776e41705
4
+ data.tar.gz: 9428aba4ba76ca6fb8d3ee751a102f7fc249e27a6630a961b27504da27b3444e
5
5
  SHA512:
6
- metadata.gz: 8576fe2b1c9f6f0b88e2472c34bd5720e48e33c43da18845360e52c10017410ea1b730d206a71c47ad8e8b5e7b9e132e9e795524e8baea89f67156bb660e0bfc
7
- data.tar.gz: cc61e8bc843aa2e27ad8a6c9d9e63788be37729a53060b19a486b572e6761aa35fb066edf1493b63ff4b71bb1f5058af6014a7c8deb3c9f92d6c76a272925e9a
6
+ metadata.gz: ba9a029154bfad46d73a4f844799508f5ca384042bbbfb832281b4f735dfdf93ddc8ff53f0363d636b8b94d52d6cabdfcefc7d624f2ba16f63e500700789bab8
7
+ data.tar.gz: 35c83469b178ad06bf0e3b36bb52683f07f8b8d4e7c2d9e331b32dde6b74bf74748ac925627f822a733c748b9bb84f3875675c5929742e3a1e5d164c265babd7
checksums.yaml.gz.sig CHANGED
Binary file
@@ -15,8 +15,8 @@ $ bundle add protocol-grpc
15
15
  `protocol-grpc` has several core concepts:
16
16
 
17
17
  - A {ruby Protocol::GRPC::Interface} class which defines gRPC service contracts with RPC methods, request/response types, and streaming patterns.
18
- - A {ruby Protocol::GRPC::Body::ReadableBody} class which handles reading gRPC messages from HTTP request/response bodies with automatic framing and decoding.
19
- - A {ruby Protocol::GRPC::Body::WritableBody} class which handles writing gRPC messages to HTTP request/response bodies with automatic framing and encoding.
18
+ - A {ruby Protocol::GRPC::Body::Readable} class which handles reading gRPC messages from HTTP request/response bodies with automatic framing and decoding.
19
+ - A {ruby Protocol::GRPC::Body::Writable} class which handles writing gRPC messages to HTTP request/response bodies with automatic framing and encoding.
20
20
  - A {ruby Protocol::GRPC::Middleware} abstract base class for building gRPC server applications.
21
21
  - A {ruby Protocol::GRPC::Call} class which represents the context of a single gRPC RPC call, including deadline tracking.
22
22
  - A {ruby Protocol::GRPC::Status} module with gRPC status code constants.
@@ -39,23 +39,38 @@ This gem provides protocol-level abstractions only. To actually send requests ov
39
39
  require "protocol/grpc/interface"
40
40
 
41
41
  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)
45
53
  end
46
54
  ```
47
55
 
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
+
48
63
  ### Building a Request
49
64
 
50
- Build gRPC requests using `Protocol::GRPC::Methods` and `Protocol::GRPC::Body::WritableBody`:
65
+ Build gRPC requests using `Protocol::GRPC::Methods` and `Protocol::GRPC::Body::Writable`:
51
66
 
52
67
  ``` ruby
53
68
  require "protocol/grpc"
54
69
  require "protocol/grpc/methods"
55
- require "protocol/grpc/body/writable_body"
70
+ require "protocol/grpc/body/writable"
56
71
 
57
72
  # Build request body
58
- body = Protocol::GRPC::Body::WritableBody.new(message_class: Hello::HelloRequest)
73
+ body = Protocol::GRPC::Body::Writable.new(message_class: Hello::HelloRequest)
59
74
  body.write(Hello::HelloRequest.new(name: "World"))
60
75
  body.close_write
61
76
 
@@ -69,13 +84,13 @@ request = Protocol::HTTP::Request["POST", path, headers, body]
69
84
 
70
85
  ### Reading a Response
71
86
 
72
- Read gRPC responses using `Protocol::GRPC::Body::ReadableBody`:
87
+ Read gRPC responses using `Protocol::GRPC::Body::Readable`:
73
88
 
74
89
  ``` ruby
75
- require "protocol/grpc/body/readable_body"
90
+ require "protocol/grpc/body/readable"
76
91
 
77
92
  # Read response body
78
- readable_body = Protocol::GRPC::Body::ReadableBody.new(
93
+ readable_body = Protocol::GRPC::Body::Readable.new(
79
94
  response.body,
80
95
  message_class: Hello::HelloReply
81
96
  )
data/design.md CHANGED
@@ -293,22 +293,22 @@ module Protocol
293
293
  message ? URI.decode_www_form_component(message) : nil
294
294
  end
295
295
 
296
- # Add gRPC status, message, and optional backtrace to headers.
297
- # Whether these become headers or trailers is controlled by the protocol layer.
298
- # @parameter headers [Protocol::HTTP::Headers]
299
- # @parameter status [Integer] gRPC status code
300
- # @parameter message [String | Nil] Optional status message
301
- # @parameter error [Exception | Nil] Optional error object (used to extract backtrace)
302
- def self.add_status!(headers, status: Status::OK, message: nil, error: nil)
303
- headers["grpc-status"] = Header::Status.new(status)
304
- headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
305
-
296
+ # Add gRPC status, message, and optional backtrace to headers.
297
+ # Whether these become headers or trailers is controlled by the protocol layer.
298
+ # @parameter headers [Protocol::HTTP::Headers]
299
+ # @parameter status [Integer] gRPC status code
300
+ # @parameter message [String | Nil] Optional status message
301
+ # @parameter error [Exception | Nil] Optional error object (used to extract backtrace)
302
+ def self.add_status!(headers, status: Status::OK, message: nil, error: nil)
303
+ headers["grpc-status"] = Header::Status.new(status)
304
+ headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
305
+
306
306
  # Add backtrace from error if available
307
- if error && error.backtrace && !error.backtrace.empty?
308
- headers["backtrace"] = error.backtrace
307
+ if error && error.backtrace && !error.backtrace.empty?
308
+ headers["backtrace"] = error.backtrace
309
+ end
309
310
  end
310
311
  end
311
- end
312
312
  end
313
313
  end
314
314
  ```
@@ -446,7 +446,7 @@ module Protocol
446
446
  super(prefix + data) # Call Protocol::HTTP::Body::Writable#write
447
447
  end
448
448
 
449
- protected
449
+ protected
450
450
 
451
451
  def compress(data)
452
452
  case @encoding
@@ -741,13 +741,13 @@ module Protocol
741
741
  end
742
742
 
743
743
  # Handle the RPC
744
- begin
745
- handle_rpc(request, handler, handler_method, request_class, response_class)
746
- rescue Error => error
747
- make_response(error.status_code, error.message, error: error)
748
- rescue => error
749
- make_response(Status::INTERNAL, error.message, error: error)
750
- end
744
+ begin
745
+ handle_rpc(request, handler, handler_method, request_class, response_class)
746
+ rescue Error => error
747
+ make_response(error.status_code, error.message, error: error)
748
+ rescue => error
749
+ make_response(Status::INTERNAL, error.message, error: error)
750
+ end
751
751
  end
752
752
 
753
753
  protected
@@ -764,33 +764,33 @@ module Protocol
764
764
  input = Body::Readable.new(request.body, message_class: request_class, encoding: encoding)
765
765
  output = Body::Writable.new(encoding: encoding)
766
766
 
767
- # Create call context
767
+ # Create call context
768
768
  response_headers = Protocol::HTTP::Headers.new([], nil, policy: HEADER_POLICY)
769
769
  response_headers["content-type"] = "application/grpc+proto"
770
770
  response_headers["grpc-encoding"] = encoding if encoding
771
771
 
772
772
  call = Call.new(request)
773
773
 
774
- # Invoke handler
774
+ # Invoke handler
775
775
  handler.send(method, input, output, call)
776
776
  output.close_write unless output.closed?
777
777
 
778
- # Mark trailers and add status
779
- response_headers.trailer!
780
- Metadata.add_status!(response_headers, status: Status::OK)
778
+ # Mark trailers and add status
779
+ response_headers.trailer!
780
+ Metadata.add_status!(response_headers, status: Status::OK)
781
+
782
+ Protocol::HTTP::Response[200, response_headers, output]
783
+ end
781
784
 
782
- Protocol::HTTP::Response[200, response_headers, output]
783
- end
784
-
785
785
  protected
786
-
787
- def make_response(status_code, message, error: nil)
788
- headers = Protocol::HTTP::Headers.new([], nil, policy: HEADER_POLICY)
789
- headers["content-type"] = "application/grpc+proto"
790
- Metadata.add_status!(headers, status: status_code, message: message, error: error)
791
786
 
792
- Protocol::HTTP::Response[200, headers, nil]
793
- end
787
+ def make_response(status_code, message, error: nil)
788
+ headers = Protocol::HTTP::Headers.new([], nil, policy: HEADER_POLICY)
789
+ headers["content-type"] = "application/grpc+proto"
790
+ Metadata.add_status!(headers, status: status_code, message: message, error: error)
791
+
792
+ Protocol::HTTP::Response[200, headers, nil]
793
+ end
794
794
  end
795
795
  end
796
796
  end
@@ -804,7 +804,7 @@ Standard health checking protocol:
804
804
  module Protocol
805
805
  module GRPC
806
806
  module HealthCheck
807
- # Health check status constants
807
+ # Health check status constants
808
808
  module ServingStatus
809
809
  UNKNOWN = 0
810
810
  SERVING = 1
@@ -888,10 +888,10 @@ require "protocol/grpc"
888
888
 
889
889
  # This would be inside a Rack/HTTP middleware/handler
890
890
  def handle_grpc_request(http_request)
891
- # Parse gRPC path
891
+ # Parse gRPC path
892
892
  service, method = Protocol::GRPC::Methods.parse_path(http_request.path)
893
893
 
894
- # Read input messages
894
+ # Read input messages
895
895
  input = Protocol::GRPC::Body::Readable.new(
896
896
  http_request.body,
897
897
  message_class: MyService::HelloRequest
@@ -899,26 +899,26 @@ def handle_grpc_request(http_request)
899
899
 
900
900
  request_message = input.read
901
901
 
902
- # Process the request
902
+ # Process the request
903
903
  reply = MyService::HelloReply.new(
904
904
  message: "Hello, #{request_message.name}!"
905
905
  )
906
906
 
907
- # Create response body
907
+ # Create response body
908
908
  output = Protocol::GRPC::Body::Writable.new
909
909
  output.write(reply)
910
910
  output.close_write
911
911
 
912
- # Build response headers with gRPC policy
912
+ # Build response headers with gRPC policy
913
913
  headers = Protocol::HTTP::Headers.new([], nil, policy: Protocol::GRPC::HEADER_POLICY)
914
914
  headers["content-type"] = "application/grpc+proto"
915
915
 
916
- # Mark that trailers will follow (after body)
916
+ # Mark that trailers will follow (after body)
917
917
  headers.trailer!
918
918
 
919
- # Add status as trailer - these will be sent after the response body
920
- # Note: The user just adds them to headers; the @tail marker ensures
921
- # they're recognized as trailers internally
919
+ # Add status as trailer - these will be sent after the response body
920
+ # Note: The user just adds them to headers; the @tail marker ensures
921
+ # they're recognized as trailers internally
922
922
  Protocol::GRPC::Metadata.add_status!(headers, status: Protocol::GRPC::Status::OK)
923
923
 
924
924
  Protocol::HTTP::Response[200, headers, output]
@@ -1091,20 +1091,20 @@ require "protocol/grpc"
1091
1091
  require_relative "my_service_pb" # Generated by protoc --ruby_out
1092
1092
 
1093
1093
  module MyService
1094
- # Client stub for Greeter service
1094
+ # Client stub for Greeter service
1095
1095
  class GreeterClient
1096
- # @parameter client [Async::GRPC::Client] The gRPC client
1096
+ # @parameter client [Async::GRPC::Client] The gRPC client
1097
1097
  def initialize(client)
1098
1098
  @client = client
1099
1099
  end
1100
1100
 
1101
1101
  SERVICE_PATH = "my_service.Greeter"
1102
1102
 
1103
- # Unary RPC: SayHello
1104
- # @parameter request [MyService::HelloRequest]
1105
- # @parameter metadata [Hash] Custom metadata
1106
- # @parameter timeout [Numeric] Deadline
1107
- # @returns [MyService::HelloReply]
1103
+ # Unary RPC: SayHello
1104
+ # @parameter request [MyService::HelloRequest]
1105
+ # @parameter metadata [Hash] Custom metadata
1106
+ # @parameter timeout [Numeric] Deadline
1107
+ # @returns [MyService::HelloReply]
1108
1108
  def say_hello(request, metadata: {}, timeout: nil)
1109
1109
  @client.unary(
1110
1110
  SERVICE_PATH,
@@ -1116,45 +1116,45 @@ module MyService
1116
1116
  )
1117
1117
  end
1118
1118
 
1119
- # Server streaming RPC: StreamNumbers
1120
- # @parameter request [MyService::HelloRequest]
1121
- # @yields {|response| ...} Each HelloReply message
1122
- # @returns [Enumerator<MyService::HelloReply>] if no block given
1119
+ # Server streaming RPC: StreamNumbers
1120
+ # @parameter request [MyService::HelloRequest]
1121
+ # @yields {|response| ...} Each HelloReply message
1122
+ # @returns [Enumerator<MyService::HelloReply>] if no block given
1123
1123
  def stream_numbers(request, metadata: {}, timeout: nil, &block)
1124
1124
  @client.server_streaming(
1125
1125
  SERVICE_PATH,
1126
1126
  "StreamNumbers",
1127
1127
  request,
1128
1128
  response_class: MyService::HelloReply,
1129
- metadata: metadata,
1130
- timeout: timeout,
1129
+ metadata: metadata,
1130
+ timeout: timeout,
1131
1131
  &block
1132
1132
  )
1133
1133
  end
1134
1134
 
1135
- # Client streaming RPC: RecordRoute
1136
- # @yields {|stream| ...} Block that writes Point messages
1137
- # @returns [MyService::RouteSummary]
1135
+ # Client streaming RPC: RecordRoute
1136
+ # @yields {|stream| ...} Block that writes Point messages
1137
+ # @returns [MyService::RouteSummary]
1138
1138
  def record_route(metadata: {}, timeout: nil, &block)
1139
1139
  @client.client_streaming(
1140
1140
  SERVICE_PATH,
1141
1141
  "RecordRoute",
1142
1142
  response_class: MyService::RouteSummary,
1143
- metadata: metadata,
1144
- timeout: timeout,
1143
+ metadata: metadata,
1144
+ timeout: timeout,
1145
1145
  &block
1146
1146
  )
1147
1147
  end
1148
1148
 
1149
- # Bidirectional streaming RPC: RouteChat
1150
- # @yields {|input, output| ...} input for writing, output for reading
1149
+ # Bidirectional streaming RPC: RouteChat
1150
+ # @yields {|input, output| ...} input for writing, output for reading
1151
1151
  def route_chat(metadata: {}, timeout: nil, &block)
1152
1152
  @client.bidirectional_streaming(
1153
1153
  SERVICE_PATH,
1154
1154
  "RouteChat",
1155
1155
  response_class: MyService::Point,
1156
- metadata: metadata,
1157
- timeout: timeout,
1156
+ metadata: metadata,
1157
+ timeout: timeout,
1158
1158
  &block
1159
1159
  )
1160
1160
  end
@@ -1172,76 +1172,76 @@ require "protocol/grpc"
1172
1172
  require_relative "my_service_pb" # Generated by protoc --ruby_out
1173
1173
 
1174
1174
  module MyService
1175
- # Base class for Greeter service implementation
1176
- # Inherit from this class and implement the RPC methods
1175
+ # Base class for Greeter service implementation
1176
+ # Inherit from this class and implement the RPC methods
1177
1177
  class GreeterService
1178
- # Unary RPC: SayHello
1179
- # Override this method in your implementation
1180
- # @parameter request [MyService::HelloRequest]
1181
- # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1182
- # @returns [MyService::HelloReply]
1178
+ # Unary RPC: SayHello
1179
+ # Override this method in your implementation
1180
+ # @parameter request [MyService::HelloRequest]
1181
+ # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1182
+ # @returns [MyService::HelloReply]
1183
1183
  def say_hello(request, call)
1184
1184
  raise NotImplementedError, "#{self.class}#say_hello not implemented"
1185
1185
  end
1186
1186
 
1187
- # Server streaming RPC: StreamNumbers
1188
- # Override this method in your implementation
1189
- # @parameter request [MyService::HelloRequest]
1190
- # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1191
- # @yields [MyService::HelloReply] Yield each response message
1187
+ # Server streaming RPC: StreamNumbers
1188
+ # Override this method in your implementation
1189
+ # @parameter request [MyService::HelloRequest]
1190
+ # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1191
+ # @yields [MyService::HelloReply] Yield each response message
1192
1192
  def stream_numbers(request, call)
1193
1193
  raise NotImplementedError, "#{self.class}#stream_numbers not implemented"
1194
1194
  end
1195
1195
 
1196
- # Client streaming RPC: RecordRoute
1197
- # Override this method in your implementation
1198
- # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1199
- # @yields [MyService::Point] Each request message from client
1200
- # @returns [MyService::RouteSummary]
1196
+ # Client streaming RPC: RecordRoute
1197
+ # Override this method in your implementation
1198
+ # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1199
+ # @yields [MyService::Point] Each request message from client
1200
+ # @returns [MyService::RouteSummary]
1201
1201
  def record_route(call)
1202
1202
  raise NotImplementedError, "#{self.class}#record_route not implemented"
1203
1203
  end
1204
1204
 
1205
- # Bidirectional streaming RPC: RouteChat
1206
- # Override this method in your implementation
1207
- # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1208
- # @returns [Enumerator, Enumerator] (input, output) - input for reading, output for writing
1205
+ # Bidirectional streaming RPC: RouteChat
1206
+ # Override this method in your implementation
1207
+ # @parameter call [Protocol::GRPC::ServerCall] Call context with metadata
1208
+ # @returns [Enumerator, Enumerator] (input, output) - input for reading, output for writing
1209
1209
  def route_chat(call)
1210
1210
  raise NotImplementedError, "#{self.class}#route_chat not implemented"
1211
1211
  end
1212
1212
 
1213
- # Internal: Dispatch method for Async::GRPC::Server
1214
- # Maps RPC calls to handler methods
1213
+ # Internal: Dispatch method for Async::GRPC::Server
1214
+ # Maps RPC calls to handler methods
1215
1215
  def self.rpc_descriptions
1216
1216
  {
1217
1217
  "SayHello" => {
1218
1218
  method: :say_hello,
1219
- request_class: MyService::HelloRequest,
1220
- response_class: MyService::HelloReply,
1221
- request_streaming: false,
1222
- response_streaming: false
1219
+ request_class: MyService::HelloRequest,
1220
+ response_class: MyService::HelloReply,
1221
+ request_streaming: false,
1222
+ response_streaming: false
1223
+ },
1224
+ "StreamNumbers" => {
1225
+ method: :stream_numbers,
1226
+ request_class: MyService::HelloRequest,
1227
+ response_class: MyService::HelloReply,
1228
+ request_streaming: false,
1229
+ response_streaming: true
1230
+ },
1231
+ "RecordRoute" => {
1232
+ method: :record_route,
1233
+ request_class: MyService::Point,
1234
+ response_class: MyService::RouteSummary,
1235
+ request_streaming: true,
1236
+ response_streaming: false
1223
1237
  },
1224
- "StreamNumbers" => {
1225
- method: :stream_numbers,
1226
- request_class: MyService::HelloRequest,
1227
- response_class: MyService::HelloReply,
1228
- request_streaming: false,
1229
- response_streaming: true
1230
- },
1231
- "RecordRoute" => {
1232
- method: :record_route,
1233
- request_class: MyService::Point,
1234
- response_class: MyService::RouteSummary,
1235
- request_streaming: true,
1236
- response_streaming: false
1237
- },
1238
- "RouteChat" => {
1239
- method: :route_chat,
1240
- request_class: MyService::Point,
1241
- response_class: MyService::Point,
1242
- request_streaming: true,
1243
- response_streaming: true
1244
- }
1238
+ "RouteChat" => {
1239
+ method: :route_chat,
1240
+ request_class: MyService::Point,
1241
+ response_class: MyService::Point,
1242
+ request_streaming: true,
1243
+ response_streaming: true
1244
+ }
1245
1245
  }
1246
1246
  end
1247
1247
  end
@@ -1261,12 +1261,12 @@ Async do
1261
1261
  client = Async::GRPC::Client.new(endpoint)
1262
1262
  stub = MyService::GreeterClient.new(client)
1263
1263
 
1264
- # Clean, typed interface!
1264
+ # Clean, typed interface!
1265
1265
  request = MyService::HelloRequest.new(name: "World")
1266
1266
  response = stub.say_hello(request)
1267
1267
  puts response.message
1268
1268
 
1269
- # Server streaming
1269
+ # Server streaming
1270
1270
  stub.stream_numbers(request) do |reply|
1271
1271
  puts reply.message
1272
1272
  end
@@ -1315,7 +1315,7 @@ Async do
1315
1315
  server = Async::GRPC::Server.new
1316
1316
  server.register("my_service.Greeter", MyGreeter.new)
1317
1317
 
1318
- # ... start server
1318
+ # ... start server
1319
1319
  end
1320
1320
  ```
1321
1321
 
@@ -1332,27 +1332,27 @@ Key classes:
1332
1332
  module Protocol
1333
1333
  module GRPC
1334
1334
  class Generator
1335
- # @parameter proto_file [String] Path to .proto file
1335
+ # @parameter proto_file [String] Path to .proto file
1336
1336
  def initialize(proto_file)
1337
1337
  @proto = parse_proto(proto_file)
1338
1338
  end
1339
1339
 
1340
1340
  def generate_client(output_path)
1341
- # Generate client stub
1341
+ # Generate client stub
1342
1342
  end
1343
1343
 
1344
1344
  def generate_server(output_path)
1345
- # Generate server base class
1345
+ # Generate server base class
1346
1346
  end
1347
1347
 
1348
- private
1348
+ private
1349
1349
 
1350
1350
  def parse_proto(file)
1351
- # Simple parsing - extract:
1352
- # - package name
1353
- # - message names (just reference them, protoc generates these)
1354
- # - service definitions
1355
- # - RPC methods with request/response types and streaming flags
1351
+ # Simple parsing - extract:
1352
+ # - package name
1353
+ # - message names (just reference them, protoc generates these)
1354
+ # - service definitions
1355
+ # - RPC methods with request/response types and streaming flags
1356
1356
  end
1357
1357
  end
1358
1358
  end
@@ -1392,8 +1392,8 @@ module Bake
1392
1392
  Console.logger.info(self){"Generated #{output_path}"}
1393
1393
  end
1394
1394
 
1395
- # Generate gRPC stubs for all .proto files in directory
1396
- # @parameter directory [String] Directory containing .proto files
1395
+ # Generate gRPC stubs for all .proto files in directory
1396
+ # @parameter directory [String] Directory containing .proto files
1397
1397
  def generate_all(directory: ".")
1398
1398
  Dir.glob(File.join(directory, "**/*.proto")).each do |proto_file|
1399
1399
  generate(proto_file)
@@ -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
- require "protocol/http"
7
- require "protocol/http/body/wrapper"
8
- require "zlib"
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
- # 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 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
- require "protocol/http"
7
- require "protocol/http/body/writable"
8
- require "zlib"
9
- require "stringio"
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
- # 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 WritableBody < 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
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
@@ -7,6 +7,17 @@ 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
+ 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
+
10
21
  # Represents an interface definition for gRPC methods.
11
22
  # Can be used by both client stubs and server implementations.
12
23
  class Interface
@@ -24,6 +35,21 @@ module Protocol
24
35
  end
25
36
  end
26
37
 
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
+
27
53
  # Hook called when a subclass is created.
28
54
  # Initializes the RPC hash for the subclass.
29
55
  # @parameter subclass [Class] The subclass being created
@@ -35,13 +61,41 @@ module Protocol
35
61
 
36
62
  # Define an RPC method.
37
63
  # @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()
40
66
  # @parameter streaming [Symbol] Streaming type (:unary, :server_streaming, :client_streaming, :bidirectional)
67
+ # This is automatically inferred from stream() decorators if not explicitly provided
41
68
  # @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)
43
76
  options[:name] = name
44
77
 
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
+
45
99
  # Ensure snake_case method name is always available
46
100
  options[:method] ||= pascal_case_to_snake_case(name.to_s).to_sym
47
101
 
@@ -7,7 +7,7 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module GRPC
10
- VERSION = "0.5.1"
10
+ VERSION = "0.6.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.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -102,7 +102,9 @@ 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
metadata.gz.sig CHANGED
Binary file