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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +26 -11
- data/design.md +130 -130
- data/lib/protocol/grpc/body/readable.rb +123 -0
- data/lib/protocol/grpc/body/readable_body.rb +10 -111
- data/lib/protocol/grpc/body/writable.rb +101 -0
- data/lib/protocol/grpc/body/writable_body.rb +10 -89
- data/lib/protocol/grpc/interface.rb +57 -3
- data/lib/protocol/grpc/version.rb +1 -1
- data/lib/protocol/grpc.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +3 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13f745749ba7c9ac86eff356ee01f6d655c3996a1633ab9091c76e4776e41705
|
|
4
|
+
data.tar.gz: 9428aba4ba76ca6fb8d3ee751a102f7fc249e27a6630a961b27504da27b3444e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba9a029154bfad46d73a4f844799508f5ca384042bbbfb832281b4f735dfdf93ddc8ff53f0363d636b8b94d52d6cabdfcefc7d624f2ba16f63e500700789bab8
|
|
7
|
+
data.tar.gz: 35c83469b178ad06bf0e3b36bb52683f07f8b8d4e7c2d9e331b32dde6b74bf74748ac925627f822a733c748b9bb84f3875675c5929742e3a1e5d164c265babd7
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/getting-started.md
CHANGED
|
@@ -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::
|
|
19
|
-
- A {ruby Protocol::GRPC::Body::
|
|
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
|
-
|
|
43
|
-
rpc :
|
|
44
|
-
|
|
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::
|
|
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/
|
|
70
|
+
require "protocol/grpc/body/writable"
|
|
56
71
|
|
|
57
72
|
# Build request body
|
|
58
|
-
body = Protocol::GRPC::Body::
|
|
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::
|
|
87
|
+
Read gRPC responses using `Protocol::GRPC::Body::Readable`:
|
|
73
88
|
|
|
74
89
|
``` ruby
|
|
75
|
-
require "protocol/grpc/body/
|
|
90
|
+
require "protocol/grpc/body/readable"
|
|
76
91
|
|
|
77
92
|
# Read response body
|
|
78
|
-
readable_body = Protocol::GRPC::Body::
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
774
|
+
# Invoke handler
|
|
775
775
|
handler.send(method, input, output, call)
|
|
776
776
|
output.close_write unless output.closed?
|
|
777
777
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
891
|
+
# Parse gRPC path
|
|
892
892
|
service, method = Protocol::GRPC::Methods.parse_path(http_request.path)
|
|
893
893
|
|
|
894
|
-
|
|
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
|
-
|
|
902
|
+
# Process the request
|
|
903
903
|
reply = MyService::HelloReply.new(
|
|
904
904
|
message: "Hello, #{request_message.name}!"
|
|
905
905
|
)
|
|
906
906
|
|
|
907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
916
|
+
# Mark that trailers will follow (after body)
|
|
917
917
|
headers.trailer!
|
|
918
918
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
1094
|
+
# Client stub for Greeter service
|
|
1095
1095
|
class GreeterClient
|
|
1096
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
1130
|
-
|
|
1129
|
+
metadata: metadata,
|
|
1130
|
+
timeout: timeout,
|
|
1131
1131
|
&block
|
|
1132
1132
|
)
|
|
1133
1133
|
end
|
|
1134
1134
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1143
|
+
metadata: metadata,
|
|
1144
|
+
timeout: timeout,
|
|
1145
1145
|
&block
|
|
1146
1146
|
)
|
|
1147
1147
|
end
|
|
1148
1148
|
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1175
|
+
# Base class for Greeter service implementation
|
|
1176
|
+
# Inherit from this class and implement the RPC methods
|
|
1177
1177
|
class GreeterService
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1341
|
+
# Generate client stub
|
|
1342
1342
|
end
|
|
1343
1343
|
|
|
1344
1344
|
def generate_server(output_path)
|
|
1345
|
-
|
|
1345
|
+
# Generate server base class
|
|
1346
1346
|
end
|
|
1347
1347
|
|
|
1348
|
-
|
|
1348
|
+
private
|
|
1349
1349
|
|
|
1350
1350
|
def parse_proto(file)
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
require
|
|
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
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
require
|
|
9
|
-
|
|
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
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
15
|
-
require_relative "grpc/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.
|
|
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
|