connect_rpc_rails 0.1.0 → 0.2.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
- data/CHANGELOG.md +16 -0
- data/README.md +88 -245
- data/lib/connect_rpc_rails/codec.rb +9 -0
- data/lib/connect_rpc_rails/controller.rb +142 -62
- data/lib/connect_rpc_rails/exceptions_app.rb +13 -3
- data/lib/connect_rpc_rails/get_query.rb +70 -0
- data/lib/connect_rpc_rails/message_validatable.rb +208 -0
- data/lib/connect_rpc_rails/protovalidate.rb +12 -0
- data/lib/connect_rpc_rails/routing.rb +1 -1
- data/lib/connect_rpc_rails/service_registration.rb +7 -1
- data/lib/connect_rpc_rails/version.rb +1 -1
- data/lib/connect_rpc_rails.rb +1 -0
- data/sig/generated/connect_rpc_rails/codec.rbs +4 -0
- data/sig/generated/connect_rpc_rails/controller.rbs +63 -23
- data/sig/generated/connect_rpc_rails/exceptions_app.rbs +6 -2
- data/sig/generated/connect_rpc_rails/get_query.rbs +47 -0
- data/sig/generated/connect_rpc_rails/message_validatable.rbs +96 -0
- data/sig/generated/connect_rpc_rails/protovalidate.rbs +2 -0
- data/sig/generated/connect_rpc_rails/routing.rbs +1 -1
- data/sig/generated/connect_rpc_rails/service_registration.rbs +8 -2
- metadata +49 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2f63f561d5da0e371e9998ed36e960564676011496e5d4363e126b28fe9314b
|
|
4
|
+
data.tar.gz: f2346887acb7c9e0ff3aff3093845287ef071cb065a2af26692ff72d6d1ad3a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 004fd6cf082f101831c7e87d6208ce05f3b2e76c48d1160c6829d863d7e584b16e8e0246292ab51139759161f54cca419016b99858c94bc23037377ca752a644
|
|
7
|
+
data.tar.gz: ad4dc123f3564a44fd1f3085f1d58af546b374be1b81ff2ab9e0b6ec04be899d5b14f0c1373169c98dc3a3470fbf035b9207e2b6c34fa3cb595eca5540443e0c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
- Connect GET: a unary RPC whose `.proto` sets `idempotency_level = NO_SIDE_EFFECTS` is also
|
|
6
|
+
served over GET, with the request in the query string, as connect-es sends with `useHttpGet`
|
|
7
|
+
and connect-go with `WithHTTPGet`. A GET to any other RPC is a 405, and every 405 names the
|
|
8
|
+
allowed verbs in `Allow`. `ConnectRpcRails::ExceptionsApp` answers a GET carrying
|
|
9
|
+
`connect=v1` in the Connect error shape. `ServiceRegistration::Rpc` gains `idempotency_level`
|
|
10
|
+
and `streaming`.
|
|
11
|
+
- The `unimplemented` answer to request compression names the supported encodings in its
|
|
12
|
+
message: `unsupported content-encoding "gzip": supported encodings are identity`.
|
|
13
|
+
- `ConnectRpcRails::MessageValidatable` (`require "connect_rpc_rails/protovalidate"`) checks a
|
|
14
|
+
request against its message's `buf.validate` rules before the RPC runs, answering a
|
|
15
|
+
violation as `invalid_argument` with a `google.rpc.BadRequest` whose field paths follow the
|
|
16
|
+
request's encoding. A violation raises `MessageValidatable::ViolationError`, and
|
|
17
|
+
`connect_error_for_violation` builds the Connect error it is answered with.
|
|
18
|
+
|
|
3
19
|
## 0.1.0 (2026-09-25)
|
|
4
20
|
|
|
5
21
|
- Initial release: Connect unary RPCs served as ordinary `ActionController::API` actions,
|
data/README.md
CHANGED
|
@@ -1,290 +1,133 @@
|
|
|
1
|
-
# connect_rpc_rails
|
|
1
|
+
# connect_rpc_rails: Connect unary RPCs as ordinary Rails controller actions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Rails, built on `ActionController::API`. It gives you a Connect-for-Ruby layer that is
|
|
5
|
-
small enough to own outright: a service is an ordinary Rails controller, so it reuses
|
|
6
|
-
`google-protobuf` and the observability you already have rather than shipping a parallel
|
|
7
|
-
stack.
|
|
3
|
+
connect_rpc_rails serves [Connect](https://connectrpc.com/docs/protocol/) unary RPCs from a Rails app. A service is an `ActionController::API` controller and each RPC is one of its actions, so callbacks, `rescue_from`, and the `process_action.action_controller` instrumentation that Datadog, Sentry, lograge, and the Rails request log hook into all apply with no extra wiring.
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
Services are read off the `google-protobuf` descriptor pool, so the only generated code is the message classes `buf`/`protoc` already emit. The library is small enough to own outright: it is a Connect transport for Rails, not a parallel framework.
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
is **one Rails action on that controller**, so every call flows through the normal
|
|
13
|
-
controller lifecycle. Because `process_action.action_controller` fires, the entire
|
|
14
|
-
Rails observability ecosystem (Datadog resource naming, Sentry transactions, lograge,
|
|
15
|
-
the `Completed 200 in Xms` request log) works with no extra wiring. The RPC method
|
|
16
|
-
holds the domain logic; the library wraps it with the transport.
|
|
7
|
+
## Features
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
- One RPC is one controller action, routed per service from the `.proto` descriptor
|
|
10
|
+
- Cross-cutting logic is Rails callbacks (`before_action`, `around_action`, `rescue_from`), with no interceptor layer to learn
|
|
11
|
+
- Exceptions Rails already classifies become Connect codes without mapping: `ActiveRecord::RecordNotFound` is `not_found`
|
|
12
|
+
- Connect-shaped errors even for exceptions that escape before dispatch, through `ConnectRpcRails::ExceptionsApp`
|
|
13
|
+
- `connect-timeout-ms` deadlines, request metadata, and response trailers
|
|
14
|
+
- Connect GET for RPCs the `.proto` declares free of side effects, as connect-es sends with `useHttpGet`
|
|
15
|
+
- Optional request validation against [`buf.validate`](https://buf.build/docs/protovalidate/) rules through [protovalidate](https://github.com/sorah/protovalidate-rb)
|
|
16
|
+
- Passes every in-scope case of the official [Connect conformance suite](conformance/)
|
|
17
|
+
- RBS signatures included
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
# app/controllers/greet_controller.rb
|
|
25
|
-
class GreetController < ActionController::API
|
|
26
|
-
include ConnectRpcRails::Controller
|
|
27
|
-
include BearerAuthentication # a concern with a before_action
|
|
19
|
+
## Requirements
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
- Ruby 3.4 or later
|
|
22
|
+
- Rails (Action Pack) 7.0 or later
|
|
23
|
+
- google-protobuf 4.26 or later, below 5
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
# the request message (`connect_request`, read the way you read `params`) and encodes
|
|
33
|
-
# whatever message you return. `principal` was set by the before_action; authZ lives
|
|
34
|
-
# here.
|
|
35
|
-
def say_hello
|
|
36
|
-
Greet::V1::SayHelloResponse.new(greeting: "Hello, #{connect_request.name}!")
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
```
|
|
25
|
+
## Installation
|
|
40
26
|
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Rails.application.routes.draw do
|
|
44
|
-
connect_service "greet.v1.GreetService" => :greet
|
|
45
|
-
end
|
|
27
|
+
```
|
|
28
|
+
bundle add connect_rpc_rails
|
|
46
29
|
```
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
object to register: an object held on the controller class would be shared by every
|
|
50
|
-
request in the process, so anything one call left in an instance variable would be
|
|
51
|
-
readable by the next caller — the mismatch that bites when gRPC-style handlers (one
|
|
52
|
-
long-lived instance) are mixed into Rails (one instance per request). Here the RPC
|
|
53
|
-
method *is* an action, so Rails' per-request instance is the only lifecycle in play.
|
|
54
|
-
Domain logic that shouldn't live in a controller belongs in an ordinary object the
|
|
55
|
-
action calls, constructed inside the action like anywhere else in Rails.
|
|
31
|
+
## Usage
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
class is the default. When its methods have little in common — different authorization,
|
|
59
|
-
different validation — give each its own controller with a block, and each RPC's callbacks
|
|
60
|
-
are its own rather than the service's with `only:`:
|
|
33
|
+
Given a service contract:
|
|
61
34
|
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
rpc "SayGoodbye" => :greet_say_goodbye
|
|
67
|
-
end
|
|
68
|
-
```
|
|
35
|
+
```protobuf
|
|
36
|
+
// proto/greet/v1/greet.proto
|
|
37
|
+
syntax = "proto3";
|
|
38
|
+
package greet.v1;
|
|
69
39
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
but not that method, so it is answered `unimplemented` exactly as a declared RPC nobody
|
|
74
|
-
implements always is. The controllers declare the service once on a shared base class —
|
|
75
|
-
`connect_service` is inherited — and the service-prefix catch-all is drawn at the first of
|
|
76
|
-
them, since answering it is a 404 and nothing else.
|
|
77
|
-
|
|
78
|
-
**Routes come from the descriptor, per service.** The routes file names the service the
|
|
79
|
-
way the `.proto` does and points it at a controller as a string, exactly like any other
|
|
80
|
-
Rails route — so drawing the routes doesn't load the controller class, and both ends of the
|
|
81
|
-
mapping grep straight to the protobuf definition. Every method the descriptor declares
|
|
82
|
-
becomes one route to the action implementing it; the method list lives in the `.proto` and
|
|
83
|
-
nowhere else. What the controller actually serves is then its own business: a declared RPC
|
|
84
|
-
with no action is answered Connect `unimplemented` (HTTP 501, not a 404, as the protocol
|
|
85
|
-
wants) through Rails' own `action_missing`, and the single catch-all the DSL draws over the
|
|
86
|
-
service prefix makes a method the descriptor never declared a plain 404.
|
|
87
|
-
|
|
88
|
-
Under eager loading — production, and CI — the DSL also resolves each routed controller as
|
|
89
|
-
the routes are drawn and checks that it serves the service it was wired to, so a
|
|
90
|
-
mis-wired route raises at boot instead of 404-ing in production. Rails eager loads before it
|
|
91
|
-
draws the routes, so that costs no autoloading; with lazy loading (development) the class is
|
|
92
|
-
left untouched.
|
|
93
|
-
|
|
94
|
-
**Why `ActionController::API`, not a bare Rack transport?** A bespoke Rack transport
|
|
95
|
-
would mean going off the controller path and losing everything that hangs off
|
|
96
|
-
`process_action.action_controller` (Datadog/Sentry/lograge/the request log), then
|
|
97
|
-
rebuilding each integration by hand. `ActionController::API` ships exactly the useful
|
|
98
|
-
modules (`Instrumentation`, `Logging`, `Rescue`, `AbstractController::Callbacks`,
|
|
99
|
-
`StrongParameters`) and omits the browser concerns an RPC endpoint never uses (CSRF,
|
|
100
|
-
cookies, flash, view rendering). It also brings the per-request instance lifecycle,
|
|
101
|
-
which is what keeps request state from outliving the request.
|
|
102
|
-
|
|
103
|
-
## Reading the call, and cross-cutting logic
|
|
104
|
-
|
|
105
|
-
A Connect call *is* an HTTP request, so there is no per-call context object to learn:
|
|
106
|
-
|
|
107
|
-
| what you want | where it is |
|
|
108
|
-
|---|---|
|
|
109
|
-
| the decoded request message | `connect_request` — read it the way you read `params` |
|
|
110
|
-
| request metadata | `connect_metadata` (the request's headers, downcased and dasherized), or `request.headers` |
|
|
111
|
-
| leading response metadata | `response.headers` |
|
|
112
|
-
| trailing response metadata | `connect_trailers["x-audit"] = ["1"]` — the helper writes Connect's unary `trailer-` form |
|
|
113
|
-
| the deadline | `connect_deadline` / `connect_timeout_ms`, for budgeting your own downstream calls |
|
|
114
|
-
| anything you computed for this call | an instance variable, as in any controller |
|
|
115
|
-
|
|
116
|
-
**Cross-cutting logic is Rails callbacks, and only that.** There is no interceptor layer:
|
|
117
|
-
`before_action` for auth, `around_action` to wrap a call, `rescue_from` for exception
|
|
118
|
-
mapping. The body is decoded *before* the callbacks run, so a `before_action` can already
|
|
119
|
-
read `connect_request` — which is what makes callbacks a complete replacement rather than a
|
|
120
|
-
partial one. Reuse across services is an `ActiveSupport::Concern` (see
|
|
121
|
-
[`BearerAuthentication`](examples/greet/app/controllers/concerns/bearer_authentication.rb))
|
|
122
|
-
or a shared base controller, and on top of that you get `only:` / `except:`, inheritance
|
|
123
|
-
and `skip_before_action`, none of which an interceptor chain offers.
|
|
124
|
-
|
|
125
|
-
Callbacks halt the Rails way: `render` a response, or raise a `ConnectRpcRails::Error` and
|
|
126
|
-
let the library's `rescue_from` render the wire error.
|
|
127
|
-
|
|
128
|
-
The library's own transport checks (POST-only, media type, undecodable body) run in a
|
|
129
|
-
`prepend_before_action`, so a wrong-verb or unreadable request is answered as the protocol
|
|
130
|
-
requires before any application callback — auth never sees a request that should be a 405.
|
|
131
|
-
|
|
132
|
-
## Error handling
|
|
133
|
-
|
|
134
|
-
`ConnectRpcRails::Error` maps to its Connect code + HTTP status. The controller declares
|
|
135
|
-
`rescue_from ConnectRpcRails::Error` once, so it becomes the wire error body `{code,message,details}`
|
|
136
|
-
in exactly one place. An exception that isn't a `ConnectRpcRails::Error` propagates to the
|
|
137
|
-
host's error middleware, per the "let exceptions propagate" policy.
|
|
138
|
-
|
|
139
|
-
**Exceptions Rails already classifies need no mapping.** Rails keeps that classification in
|
|
140
|
-
`config.action_dispatch.rescue_responses` — the registry every railtie and gem writes into,
|
|
141
|
-
where `ActiveRecord::RecordNotFound` is `:not_found` and `ActiveRecord::RecordInvalid` is
|
|
142
|
-
`:unprocessable_content` — so including the module installs a Connect code for each of its
|
|
143
|
-
entries, read off the nearest classified ancestor. A `RecordNotFound` out of an RPC is a
|
|
144
|
-
Connect `not_found` without the app restating it.
|
|
145
|
-
|
|
146
|
-
Mapping the *rest* — your own domain exceptions — is `map_connect_errors`, which applies to
|
|
147
|
-
every RPC on the controller and overrides the code an entry above would have got:
|
|
40
|
+
service GreetService {
|
|
41
|
+
rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
|
|
42
|
+
}
|
|
148
43
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
44
|
+
message SayHelloRequest {
|
|
45
|
+
string name = 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
message SayHelloResponse {
|
|
49
|
+
string greeting = 1;
|
|
50
|
+
}
|
|
152
51
|
```
|
|
153
52
|
|
|
154
|
-
|
|
155
|
-
nothing is blanket-rescued and anything unmapped still propagates. It exists as a macro
|
|
156
|
-
because a hand-written `rescue_from` can't simply `raise` a `ConnectRpcRails::Error` —
|
|
157
|
-
Rails calls one handler per exception, so the raise would escape instead of reaching the
|
|
158
|
-
handler that renders the wire error.
|
|
53
|
+
### 1. Load the generated messages
|
|
159
54
|
|
|
160
|
-
|
|
161
|
-
dispatch — a routing error, an unreadable body, a middleware failing — never reaches a
|
|
162
|
-
controller, and the host's `config.exceptions_app` would answer it in a shape a Connect
|
|
163
|
-
client reads as a malformed response. `ConnectRpcRails::ExceptionsApp` wraps that app and
|
|
164
|
-
answers the Connect protocol's error shape for a request carrying
|
|
165
|
-
`connect-protocol-version`, passing everything else through untouched:
|
|
55
|
+
Generate Ruby code with `buf generate` or `protoc --ruby_out`, and require it before the routes are drawn. `connect_service` looks the service up in the descriptor pool by name.
|
|
166
56
|
|
|
167
57
|
```ruby
|
|
168
|
-
config.
|
|
58
|
+
# config/application.rb
|
|
59
|
+
require_relative "../lib/greet/v1/greet_pb"
|
|
169
60
|
```
|
|
170
61
|
|
|
171
|
-
|
|
172
|
-
configures its classification in one place; the message is the status's own text, never
|
|
173
|
-
the exception's. Override `#connect_error_for` in a subclass to stamp every error with a
|
|
174
|
-
detail of your own (a `google.rpc.RequestInfo` holding the request id, say).
|
|
175
|
-
|
|
176
|
-
Because escaping is now answered correctly, an app needs no blanket
|
|
177
|
-
`rescue_from StandardError` to keep the protocol: let the exception propagate and Rails'
|
|
178
|
-
request-error logging — and the error reporters subscribed to it — see it the way they see
|
|
179
|
-
any other.
|
|
62
|
+
### 2. Implement the service as a controller
|
|
180
63
|
|
|
181
|
-
|
|
64
|
+
```ruby
|
|
65
|
+
# app/controllers/greet_controller.rb
|
|
66
|
+
class GreetController < ActionController::API
|
|
67
|
+
include ConnectRpcRails::Controller
|
|
182
68
|
|
|
183
|
-
|
|
184
|
-
lives in [`conformance/`](conformance/) and passes **84/84** (Connect + unary) against
|
|
185
|
-
the `ActionController::API` transport, with the server-under-test mounted through an
|
|
186
|
-
`ActionDispatch` `RouteSet` — including error details, response headers/trailers (on
|
|
187
|
-
success *and* error), `connect-timeout-ms` enforcement, and the HTTP-status mapping for
|
|
188
|
-
malformed requests (404 unknown method, 405 wrong verb, 415 unsupported media type,
|
|
189
|
-
`unimplemented` for an unimplemented method and for unsupported compression). Streaming, gRPC/gRPC-Web, compression, and
|
|
190
|
-
TLS remain out of scope. This is the real interop check that hand-written specs can't give.
|
|
69
|
+
connect_service "greet.v1.GreetService"
|
|
191
70
|
|
|
192
|
-
|
|
71
|
+
def say_hello
|
|
72
|
+
Greet::V1::SayHelloResponse.new(greeting: "Hello, #{connect_request.name}!")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
193
76
|
|
|
194
|
-
|
|
195
|
-
- **Rails instrumentation for free.** `process_action.action_controller` fires for every RPC (including errors), carrying `controller`/`action`/`status` plus a `connect_method` payload key (`pkg.Service/Method`) for clean trace/log resource naming.
|
|
196
|
-
- **No object outlives the request.** The RPC is a controller action, so there is no handler singleton on the class to accumulate state between callers — the failure mode of putting gRPC-style handlers behind Rails.
|
|
197
|
-
- **Nothing to learn beyond Rails.** An RPC is an action, cross-cutting logic is a callback, exception mapping is `rescue_from`, metadata is headers. The only Connect-specific thing in a controller is `connect_request`.
|
|
198
|
-
- **authN vs authZ split.** `BearerAuthentication` (a concern standing in for a real bearer-token verifier) authenticates the `Bearer` token in a `before_action` and exposes `principal`; the RPC method authorizes against it.
|
|
199
|
-
- **Connect wire compliance for unary:** `POST /pkg.Service/Method`, `application/json` + `application/proto`, error body `{code,message,details}` with the spec's code→HTTP-status table.
|
|
77
|
+
An RPC takes no arguments, like any other action. `connect_request` is the decoded request message, and the returned message is encoded in the caller's format (`application/json` or `application/proto`).
|
|
200
78
|
|
|
201
|
-
|
|
79
|
+
### 3. Route it
|
|
202
80
|
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
service_registration.rb # descriptor -> RPC table (reflection)
|
|
209
|
-
codec.rb # JSON / proto, via google-protobuf
|
|
210
|
-
errors.rb # Connect codes -> HTTP status, wire error body
|
|
211
|
-
exceptions_app.rb # config.exceptions_app wrapper: Connect error shape for what escapes
|
|
212
|
-
examples/greet/ # the example as a real, bootable Rails app (own Gemfile + config.ru)
|
|
213
|
-
app/controllers/greet_controller.rb # connect_service + the RPC action
|
|
214
|
-
app/controllers/concerns/bearer_authentication.rb # authN as a before_action + stub verifier
|
|
215
|
-
config/routes.rb # connect_service "greet.v1.GreetService" => :greet
|
|
216
|
-
config/application.rb # api_only Rails app boot (Action Controller + Active Record)
|
|
217
|
-
proto/greet/v1/greet.proto # the service contract
|
|
218
|
-
lib/greet_pb.rb # hand-built stand-in for `buf generate` output
|
|
219
|
-
spec/ # RSpec: controller, routing, auth, error mapping, instance lifecycle
|
|
81
|
+
```ruby
|
|
82
|
+
# config/routes.rb
|
|
83
|
+
Rails.application.routes.draw do
|
|
84
|
+
connect_service "greet.v1.GreetService" => :greet
|
|
85
|
+
end
|
|
220
86
|
```
|
|
221
87
|
|
|
222
|
-
|
|
88
|
+
This draws `POST /greet.v1.GreetService/SayHello` to `GreetController#say_hello`, one route per RPC the descriptor declares.
|
|
223
89
|
|
|
224
|
-
|
|
225
|
-
interpreter automatically; otherwise use Ruby 3.4.
|
|
90
|
+
### 4. Call it
|
|
226
91
|
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
rake conformance # the Connect conformance suite (needs Go and buf on PATH)
|
|
92
|
+
```console
|
|
93
|
+
$ curl -X POST -H 'Content-Type: application/json' \
|
|
94
|
+
-d '{"name":"Ada"}' \
|
|
95
|
+
http://localhost:3000/greet.v1.GreetService/SayHello
|
|
96
|
+
{"greeting":"Hello, Ada!"}
|
|
233
97
|
```
|
|
234
98
|
|
|
235
|
-
`
|
|
99
|
+
Raise `ConnectRpcRails::Error.new(:invalid_argument, "name is required")` to answer an error, and use `before_action` for authentication as in any controller. [`examples/greet`](examples/greet/) is a bootable app with a bearer-token concern and error handling; see [docs/development.md](docs/development.md#the-example-service) to run it.
|
|
236
100
|
|
|
237
|
-
|
|
101
|
+
## Documentation
|
|
238
102
|
|
|
239
|
-
|
|
240
|
-
|
|
103
|
+
- [Writing services](docs/controllers.md): reading the request, metadata, trailers, deadlines, callbacks, serving GET
|
|
104
|
+
- [Routing](docs/routing.md): one controller per service or per RPC, and what unrouted calls are answered
|
|
105
|
+
- [Error handling](docs/error-handling.md): Connect codes, Rails' exception classification, `map_connect_errors`, `ExceptionsApp`
|
|
106
|
+
- [Validating requests with protovalidate](docs/protovalidate.md)
|
|
107
|
+
- [Development](docs/development.md): running the specs, type checks, the conformance suite, and releasing
|
|
108
|
+
- [Design](DESIGN.md): how the transport hooks into `ActionController::API`, and why
|
|
241
109
|
|
|
242
|
-
|
|
243
|
-
cd examples/greet
|
|
244
|
-
bundle install
|
|
245
|
-
bundle exec puma -b tcp://127.0.0.1:9711 config.ru
|
|
110
|
+
## Caveats
|
|
246
111
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
http://127.0.0.1:9711/greet.v1.GreetService/SayHello
|
|
251
|
-
# => {"greeting":"こんにちは, Ada!"}
|
|
252
|
-
```
|
|
112
|
+
- Only the Connect protocol's unary RPCs are served, by POST, and by GET for RPCs declared free of side effects. Streaming, gRPC and gRPC-Web, and request compression are out of scope.
|
|
113
|
+
- `connect-timeout-ms` is enforced with `Timeout.timeout`, which interrupts the action wherever it is running when the deadline passes.
|
|
114
|
+
- The message of an exception mapped to a Connect code, including Rails-classified ones such as `ActiveRecord::RecordNotFound`, is sent to the caller as the error message.
|
|
253
115
|
|
|
254
|
-
|
|
255
|
-
for `405`, and ask for a method the service doesn't declare for a `404`.
|
|
116
|
+
## Development
|
|
256
117
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`sig/generated/**/*.rbs` and runs `rbs validate`. Protobuf messages are typed
|
|
262
|
-
`untyped` — in a typical project their `.rbs` comes from buf's `rbs` plugin.
|
|
263
|
-
|
|
264
|
-
`rake steep` goes further and checks `lib` against those signatures. Dependency
|
|
265
|
-
signatures come from [gem_rbs_collection](https://github.com/ruby/gem_rbs_collection);
|
|
266
|
-
run `rbs collection install` once to populate `.gem_rbs_collection` from
|
|
267
|
-
`rbs_collection.lock.yaml`. Note the collection's `actionpack` and `google-protobuf`
|
|
268
|
-
signatures lag the versions this gem builds against, and much of that surface is
|
|
269
|
-
`untyped` there, so Steep checks this library's own logic rather than its use of Rails.
|
|
118
|
+
```
|
|
119
|
+
bundle install
|
|
120
|
+
bundle exec rspec
|
|
121
|
+
```
|
|
270
122
|
|
|
271
|
-
|
|
272
|
-
what it is mixed into (`ActionController::API`) plus the class-level accessors
|
|
273
|
-
`extend ClassMethods` installs — a shape RBS cannot infer from the module body.
|
|
123
|
+
See [docs/development.md](docs/development.md) for the full set of checks.
|
|
274
124
|
|
|
275
|
-
##
|
|
125
|
+
## Contributing
|
|
276
126
|
|
|
277
|
-
|
|
278
|
-
test workflow as a gate, then creates a draft GitHub release and publishes the gem to
|
|
279
|
-
RubyGems through OIDC trusted publishing — there is no API key stored anywhere.
|
|
127
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ivry-inc/connect_rpc_rails.
|
|
280
128
|
|
|
281
|
-
|
|
282
|
-
`CHANGELOG.md` to `## <version> (<YYYY-MM-DD>)`. Merge that as its own PR.
|
|
283
|
-
2. `git tag v<version> && git push origin v<version>`.
|
|
284
|
-
3. Once the workflow finishes, review the draft release and publish it.
|
|
129
|
+
## License
|
|
285
130
|
|
|
286
|
-
|
|
131
|
+
This project is licensed under the Apache-2.0 License.
|
|
287
132
|
|
|
288
|
-
|
|
289
|
-
and the idempotent-GET variant. Unary over the Connect protocol is the whole surface
|
|
290
|
-
here; add the rest only when a real consumer needs it.
|
|
133
|
+
Copyright 2026 IVRy Inc.
|
|
@@ -23,6 +23,15 @@ module ConnectRpcRails
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# The codec a Connect GET names in its `encoding` query parameter.
|
|
27
|
+
#: (String?) -> _Codec?
|
|
28
|
+
def self.for_name(name)
|
|
29
|
+
case name
|
|
30
|
+
when 'json' then Json
|
|
31
|
+
when 'proto' then Proto
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
module Json
|
|
27
36
|
CONTENT_TYPE = 'application/json' #: String
|
|
28
37
|
|