respondo 0.1.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +564 -0
- data/lib/respondo/configuration.rb +40 -0
- data/lib/respondo/controller_helpers.rb +243 -0
- data/lib/respondo/pagination.rb +94 -0
- data/lib/respondo/railtie.rb +18 -0
- data/lib/respondo/response_builder.rb +123 -0
- data/lib/respondo/serializer.rb +110 -0
- data/lib/respondo/version.rb +5 -0
- data/lib/respondo.rb +45 -0
- data/respondo.gemspec +29 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 94ebeacf9ef38e8d7d2c41182edca0cd2ae55b0520e93a5900dafea65e84584d
|
|
4
|
+
data.tar.gz: 4e185739d3eb720712a9c0264558a7669d1f6bdd420d474ce9b68a85409ddf45
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 560d4983326902e3c8c5859af0105142a8799aeeeda2a4c6036917db393f117fc3d885cc8389ec6ad6f24b6a0846d4ad5262809e5b37af4c2c63dcdceb7a6059
|
|
7
|
+
data.tar.gz: 35a6a092f71d876b30c749ce92dfeff9414689898aa12adb2c1c74466b2e9df233763f446745d93fdcb63c9a4a9f7b1332aa1c2a743fe6683afb804a0e713f35
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — Initial Release
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Standardized JSON response structure: `success`, `message`, `data`, `meta`
|
|
7
|
+
- `render_success` — 200 OK with optional data, message, meta, pagy
|
|
8
|
+
- `render_error` — 422 default with field errors, machine-readable code
|
|
9
|
+
- Convenience helpers: `render_unauthorized`, `render_forbidden`, `render_not_found`, `render_server_error`, `render_created`, `render_no_content`
|
|
10
|
+
- Auto-serialization: ActiveRecord, Relation, ActiveModel::Errors, Hash, Array, Exception
|
|
11
|
+
- Pagination meta: Kaminari, Pagy, WillPaginate — auto-detected, no extra code
|
|
12
|
+
- `camelize_keys` config — snake_case → camelCase for Flutter/JS clients
|
|
13
|
+
- `include_request_id` config — adds Rails request ID to every meta block
|
|
14
|
+
- Custom serializer hook — plug in ActiveModelSerializers, Blueprinter, Panko, etc.
|
|
15
|
+
- Railtie — auto-includes into ActionController::Base and ActionController::API
|
|
16
|
+
- Zero hard dependencies — Rails, Kaminari, Pagy are all optional
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RESPONDO AUTHORS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# Respondo 🎯
|
|
2
|
+
|
|
3
|
+
Smart JSON API response formatter for Rails — consistent structure every time, across every app.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"success": true,
|
|
8
|
+
"message": "Users fetched",
|
|
9
|
+
"data": [...],
|
|
10
|
+
"meta": {
|
|
11
|
+
"timestamp": "2024-06-15T10:30:00Z",
|
|
12
|
+
"pagination": {
|
|
13
|
+
"currentPage": 1,
|
|
14
|
+
"perPage": 25,
|
|
15
|
+
"totalPages": 4,
|
|
16
|
+
"totalCount": 98,
|
|
17
|
+
"nextPage": 2,
|
|
18
|
+
"prevPage": null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The Problem
|
|
25
|
+
|
|
26
|
+
Different developers return different JSON structures — some use `data`, some use `result`, some forget `success: true`. This makes frontend integration (Flutter, React, etc.) brittle and unpredictable.
|
|
27
|
+
|
|
28
|
+
Respondo enforces one structure, everywhere, automatically.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem "respondo"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# config/initializers/respondo.rb
|
|
44
|
+
Respondo.configure do |config|
|
|
45
|
+
config.default_success_message = "OK"
|
|
46
|
+
config.default_error_message = "Something went wrong"
|
|
47
|
+
config.include_request_id = true # adds request_id to every meta
|
|
48
|
+
config.camelize_keys = true # snake_case → camelCase for Flutter/JS
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Respondo auto-includes itself into `ActionController::Base` and `ActionController::API` via Railtie. No manual `include` needed in `ApplicationController`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Response Structure
|
|
57
|
+
|
|
58
|
+
Every single response — success or error — returns the same four keys:
|
|
59
|
+
|
|
60
|
+
| Key | Type | Description |
|
|
61
|
+
|-----------|------------------|----------------------------------------------|
|
|
62
|
+
| `success` | Boolean | `true` or `false` |
|
|
63
|
+
| `message` | String | Human-readable description |
|
|
64
|
+
| `data` | Object/Array/nil | The payload |
|
|
65
|
+
| `meta` | Object | Timestamp + pagination + optional request_id |
|
|
66
|
+
|
|
67
|
+
Error responses additionally include:
|
|
68
|
+
|
|
69
|
+
| Key | Type | Description |
|
|
70
|
+
|----------|------|--------------------------------------|
|
|
71
|
+
| `errors` | Hash | Field-level errors `{field: [msgs]}` |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Complete Helper Reference
|
|
76
|
+
|
|
77
|
+
### 2xx — Success Helpers
|
|
78
|
+
|
|
79
|
+
#### `render_ok` — 200 OK
|
|
80
|
+
Explicit alias for `render_success`. Use when you want to be more descriptive.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
render_ok(data: @user, message: "User found")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `render_created` — 201 Created
|
|
87
|
+
Use after a successful POST that creates a resource.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
render_created(data: @post, message: "Post published")
|
|
91
|
+
render_created(data: @user) # uses default "Created successfully" message
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### `render_accepted` — 202 Accepted
|
|
95
|
+
Use for async operations — the request was received but processing happens in the background.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
render_accepted(message: "Your export is being processed. You will receive an email when ready.")
|
|
99
|
+
render_accepted(data: { job_id: "abc123" }, message: "Job queued")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### `render_no_content` — 200 OK
|
|
103
|
+
Use after DELETE or actions with no meaningful response body. Returns standard JSON structure for consistency.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
render_no_content # "Deleted successfully"
|
|
107
|
+
render_no_content(message: "Account deactivated")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `render_partial_content` — 206 Partial Content
|
|
111
|
+
Use for chunked or range-based responses.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
render_partial_content(data: @chunk, message: "Page 1 of 5")
|
|
115
|
+
render_partial_content(data: @results, meta: { range: "0-99/500" })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `render_multi_status` — 207 Multi-Status
|
|
119
|
+
Use for batch operations where some succeed and some fail.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
render_multi_status(
|
|
123
|
+
data: { created: 8, failed: 2 },
|
|
124
|
+
message: "Batch completed with partial failures"
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### 4xx — Client Error Helpers
|
|
131
|
+
|
|
132
|
+
#### `render_bad_request` — 400 Bad Request
|
|
133
|
+
Malformed request, missing required parameters, invalid format.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
render_bad_request # default message
|
|
137
|
+
render_bad_request("The 'date' parameter is required")
|
|
138
|
+
render_bad_request("Invalid input", errors: { date: ["must be a valid date"] })
|
|
139
|
+
render_bad_request("Invalid input", code: "INVALID_FORMAT") # custom error code
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### `render_unauthorized` — 401 Unauthorized
|
|
143
|
+
User is not authenticated. Use when no valid token/session is present.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
render_unauthorized # "Unauthorized"
|
|
147
|
+
render_unauthorized("Please log in to continue")
|
|
148
|
+
render_unauthorized("Token has expired", code: "TOKEN_EXPIRED")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### `render_payment_required` — 402 Payment Required
|
|
152
|
+
Feature is behind a paywall or subscription.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
render_payment_required
|
|
156
|
+
render_payment_required("Upgrade to Pro to access this feature")
|
|
157
|
+
render_payment_required("Subscription expired", code: "SUBSCRIPTION_EXPIRED")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `render_forbidden` — 403 Forbidden
|
|
161
|
+
User is authenticated but lacks permission for this action.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
render_forbidden
|
|
165
|
+
render_forbidden("You can only edit your own posts")
|
|
166
|
+
render_forbidden("Admin access required", code: "ADMIN_REQUIRED")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `render_not_found` — 404 Not Found
|
|
170
|
+
Requested resource does not exist.
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
render_not_found
|
|
174
|
+
render_not_found("User not found")
|
|
175
|
+
render_not_found("Post ##{params[:id]} does not exist")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `render_method_not_allowed` — 405 Method Not Allowed
|
|
179
|
+
The HTTP verb used is not supported for this endpoint.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
render_method_not_allowed
|
|
183
|
+
render_method_not_allowed("This endpoint only accepts POST requests")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `render_not_acceptable` — 406 Not Acceptable
|
|
187
|
+
The server cannot produce a response matching the client's Accept header.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
render_not_acceptable
|
|
191
|
+
render_not_acceptable("Only application/json is supported")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `render_request_timeout` — 408 Request Timeout
|
|
195
|
+
The request took too long to process.
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
render_request_timeout
|
|
199
|
+
render_request_timeout("The query took too long. Try a smaller date range.")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### `render_conflict` — 409 Conflict
|
|
203
|
+
Request conflicts with the current state of the resource. Use for duplicate records, state conflicts.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
render_conflict
|
|
207
|
+
render_conflict("Email address is already registered")
|
|
208
|
+
render_conflict("Cannot cancel a completed order", code: "INVALID_STATE_TRANSITION")
|
|
209
|
+
render_conflict("Duplicate entry", errors: { email: ["has already been taken"] })
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### `render_gone` — 410 Gone
|
|
213
|
+
Resource existed but has been permanently deleted.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
render_gone
|
|
217
|
+
render_gone("This account has been permanently deleted")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### `render_precondition_failed` — 412 Precondition Failed
|
|
221
|
+
Conditional request headers (If-Match, If-None-Match) did not match.
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
render_precondition_failed
|
|
225
|
+
render_precondition_failed("Resource has been modified since your last request")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `render_unsupported_media_type` — 415 Unsupported Media Type
|
|
229
|
+
The Content-Type header is not supported.
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
render_unsupported_media_type
|
|
233
|
+
render_unsupported_media_type("Please send requests as application/json")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `render_unprocessable` — 422 Unprocessable Entity
|
|
237
|
+
Validation errors. The most commonly used error helper in Rails APIs.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
render_unprocessable("Validation failed", errors: user.errors)
|
|
241
|
+
render_unprocessable("Invalid data", errors: { name: ["can't be blank"], age: ["must be over 18"] })
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `render_locked` — 423 Locked
|
|
245
|
+
Resource is locked and cannot be modified.
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
render_locked
|
|
249
|
+
render_locked("This record is locked by another user")
|
|
250
|
+
render_locked("Invoice is locked after approval", code: "INVOICE_LOCKED")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### `render_too_many_requests` — 429 Too Many Requests
|
|
254
|
+
Rate limit exceeded.
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
render_too_many_requests
|
|
258
|
+
render_too_many_requests("You have exceeded 100 requests per minute. Retry after 60 seconds.")
|
|
259
|
+
render_too_many_requests("API limit reached", code: "API_LIMIT_EXCEEDED")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### 5xx — Server Error Helpers
|
|
265
|
+
|
|
266
|
+
#### `render_server_error` — 500 Internal Server Error
|
|
267
|
+
An unexpected error occurred on the server.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
render_server_error
|
|
271
|
+
render_server_error("Something went wrong. Our team has been notified.")
|
|
272
|
+
|
|
273
|
+
# Common pattern — rescue unexpected exceptions
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
Rails.logger.error(e)
|
|
276
|
+
render_server_error("An unexpected error occurred")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### `render_not_implemented` — 501 Not Implemented
|
|
280
|
+
The requested feature has not been built yet.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
render_not_implemented
|
|
284
|
+
render_not_implemented("CSV export is coming soon")
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `render_bad_gateway` — 502 Bad Gateway
|
|
288
|
+
An upstream service (third-party API, microservice) returned an invalid response.
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
render_bad_gateway
|
|
292
|
+
render_bad_gateway("Payment gateway is currently unavailable")
|
|
293
|
+
render_bad_gateway("Could not reach the SMS service", code: "SMS_GATEWAY_ERROR")
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### `render_service_unavailable` — 503 Service Unavailable
|
|
297
|
+
Server is temporarily unable to handle the request — maintenance, overloaded.
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
render_service_unavailable
|
|
301
|
+
render_service_unavailable("We are currently under maintenance. Back in 30 minutes.")
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### `render_gateway_timeout` — 504 Gateway Timeout
|
|
305
|
+
An upstream service timed out before responding.
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
render_gateway_timeout
|
|
309
|
+
render_gateway_timeout("The payment processor did not respond in time. Please try again.")
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Real-World Controller Examples
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
class UsersController < ApplicationController
|
|
318
|
+
|
|
319
|
+
def index
|
|
320
|
+
users = User.active.page(params[:page]).per(25)
|
|
321
|
+
render_ok(data: users, message: "Users fetched")
|
|
322
|
+
# → 200, with pagination meta auto-included
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def show
|
|
326
|
+
user = User.find(params[:id])
|
|
327
|
+
render_ok(data: user, message: "User found")
|
|
328
|
+
rescue ActiveRecord::RecordNotFound
|
|
329
|
+
render_not_found("User ##{params[:id]} not found")
|
|
330
|
+
# → 404, { success: false, errors_code: "NOT_FOUND" }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def create
|
|
334
|
+
user = User.new(user_params)
|
|
335
|
+
if user.save
|
|
336
|
+
render_created(data: user, message: "Account created successfully")
|
|
337
|
+
# → 201
|
|
338
|
+
else
|
|
339
|
+
render_unprocessable("Validation failed", errors: user.errors)
|
|
340
|
+
# → 422, { errors: { email: ["is invalid"] } }
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def update
|
|
345
|
+
user = User.find(params[:id])
|
|
346
|
+
|
|
347
|
+
unless user == current_user || current_user.admin?
|
|
348
|
+
render_forbidden("You can only update your own profile")
|
|
349
|
+
# → 403
|
|
350
|
+
return
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if user.update(user_params)
|
|
354
|
+
render_ok(data: user, message: "Profile updated")
|
|
355
|
+
else
|
|
356
|
+
render_conflict("Could not update profile", errors: user.errors)
|
|
357
|
+
# → 409
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def destroy
|
|
362
|
+
User.find(params[:id]).destroy!
|
|
363
|
+
render_no_content(message: "Account deleted")
|
|
364
|
+
# → 200
|
|
365
|
+
rescue ActiveRecord::RecordNotFound
|
|
366
|
+
render_gone("This account no longer exists")
|
|
367
|
+
# → 410
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
class PaymentsController < ApplicationController
|
|
373
|
+
|
|
374
|
+
def create
|
|
375
|
+
result = PaymentGateway.charge(amount: params[:amount], token: params[:token])
|
|
376
|
+
render_created(data: result, message: "Payment successful")
|
|
377
|
+
rescue PaymentGateway::CardDeclined => e
|
|
378
|
+
render_unprocessable(e.message)
|
|
379
|
+
rescue PaymentGateway::Timeout
|
|
380
|
+
render_gateway_timeout("Payment processor timed out. You have not been charged.")
|
|
381
|
+
rescue PaymentGateway::Error => e
|
|
382
|
+
render_bad_gateway("Payment gateway error: #{e.message}")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
class ReportsController < ApplicationController
|
|
388
|
+
|
|
389
|
+
def generate
|
|
390
|
+
ReportJob.perform_later(current_user.id, params[:type])
|
|
391
|
+
render_accepted(
|
|
392
|
+
data: { estimated_time: "2 minutes" },
|
|
393
|
+
message: "Report is being generated. We will email you when it is ready."
|
|
394
|
+
)
|
|
395
|
+
# → 202
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Quick Reference Card
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# 2xx — Success
|
|
407
|
+
render_success(data:, message:, meta:, pagy:, pagination:,code: , status:)
|
|
408
|
+
render_ok(data:, message:, meta:, pagination:)
|
|
409
|
+
render_created(data:, message:, pagination:)
|
|
410
|
+
render_accepted(data:, message:)
|
|
411
|
+
render_no_content(message:)
|
|
412
|
+
render_partial_content(data:, message:, meta:)
|
|
413
|
+
render_multi_status(data:, message:, meta:)
|
|
414
|
+
|
|
415
|
+
# 4xx — Client Errors
|
|
416
|
+
render_bad_request(message, errors:, code:)
|
|
417
|
+
render_unauthorized(message, code:)
|
|
418
|
+
render_payment_required(message, code:)
|
|
419
|
+
render_forbidden(message, code:)
|
|
420
|
+
render_not_found(message, code:)
|
|
421
|
+
render_method_not_allowed(message, code:)
|
|
422
|
+
render_not_acceptable(message, code:)
|
|
423
|
+
render_request_timeout(message, code:)
|
|
424
|
+
render_conflict(message, errors:, code:)
|
|
425
|
+
render_gone(message, code:)
|
|
426
|
+
render_precondition_failed(message, code:)
|
|
427
|
+
render_unsupported_media_type(message, code:)
|
|
428
|
+
render_unprocessable(message, errors:)
|
|
429
|
+
render_locked(message, code:)
|
|
430
|
+
render_too_many_requests(message, code:)
|
|
431
|
+
|
|
432
|
+
# 5xx — Server Errors
|
|
433
|
+
render_server_error(message, code:)
|
|
434
|
+
render_not_implemented(message, code:)
|
|
435
|
+
render_bad_gateway(message, code:)
|
|
436
|
+
render_service_unavailable(message, code:)
|
|
437
|
+
render_gateway_timeout(message, code:)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Auto-Serialization
|
|
443
|
+
|
|
444
|
+
Respondo automatically handles:
|
|
445
|
+
|
|
446
|
+
| Input type | Output |
|
|
447
|
+
|---------------------------------|---------------------------------------------|
|
|
448
|
+
| `ActiveRecord::Base` instance | `record.as_json` |
|
|
449
|
+
| `ActiveRecord::Relation` | Array of `as_json` records |
|
|
450
|
+
| `ActiveModel::Errors` | `{ field: ["message", ...] }` |
|
|
451
|
+
| `Hash` | Passed through (values serialized) |
|
|
452
|
+
| `Array` | Each element serialized recursively |
|
|
453
|
+
| `Exception` | `{ message: e.message }` |
|
|
454
|
+
| Anything with `#as_json` | `.as_json` |
|
|
455
|
+
| Anything with `#to_h` | `.to_h` |
|
|
456
|
+
| Primitives (String, Integer...) | As-is |
|
|
457
|
+
|
|
458
|
+
### Custom serializer
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
Respondo.configure do |config|
|
|
462
|
+
# Use ActiveModelSerializers, Blueprinter, Panko, etc.
|
|
463
|
+
config.serializer = ->(obj) { UserSerializer.new(obj).as_json }
|
|
464
|
+
end
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Pagination Meta
|
|
470
|
+
|
|
471
|
+
Automatically detected — no extra code needed.
|
|
472
|
+
|
|
473
|
+
### Kaminari
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
users = User.page(params[:page]).per(25)
|
|
477
|
+
render_ok(data: users)
|
|
478
|
+
# meta.pagination is populated automatically
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Pagy
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
pagy, users = pagy(User.all)
|
|
485
|
+
render_ok(data: users, pagy: pagy)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### WillPaginate
|
|
489
|
+
|
|
490
|
+
```ruby
|
|
491
|
+
users = User.paginate(page: params[:page], per_page: 25)
|
|
492
|
+
render_ok(data: users)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Suppress pagination
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
# Even if the collection is paginated, hide the meta
|
|
499
|
+
render_ok(data: users, pagination: false)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## camelCase for Flutter / JavaScript
|
|
505
|
+
|
|
506
|
+
```ruby
|
|
507
|
+
Respondo.configure { |c| c.camelize_keys = true }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
All keys in the response — including nested `meta.pagination` — are camelized:
|
|
511
|
+
`current_page` → `currentPage`, `total_count` → `totalCount`, `error_code` → `errorCode`.
|
|
512
|
+
|
|
513
|
+
### Flutter Integration
|
|
514
|
+
|
|
515
|
+
```dart
|
|
516
|
+
// Every response follows the same shape
|
|
517
|
+
class ApiResponse<T> {
|
|
518
|
+
final bool success;
|
|
519
|
+
final String message;
|
|
520
|
+
final T? data;
|
|
521
|
+
final Map<String, dynamic> meta;
|
|
522
|
+
final Map<String, dynamic>? errors;
|
|
523
|
+
|
|
524
|
+
const ApiResponse({
|
|
525
|
+
required this.success,
|
|
526
|
+
required this.message,
|
|
527
|
+
this.data,
|
|
528
|
+
required this.meta,
|
|
529
|
+
this.errors,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Architecture
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
lib/
|
|
540
|
+
├── respondo.rb # Entry point, configure, Railtie hook
|
|
541
|
+
└── respondo/
|
|
542
|
+
├── version.rb # VERSION
|
|
543
|
+
├── configuration.rb # Config with defaults
|
|
544
|
+
├── serializer.rb # Auto-detects and serializes any object
|
|
545
|
+
├── pagination.rb # Kaminari / Pagy / WillPaginate extractor
|
|
546
|
+
├── response_builder.rb # Assembles the final Hash
|
|
547
|
+
├── controller_helpers.rb # All render_* helpers (2xx, 4xx, 5xx)
|
|
548
|
+
└── railtie.rb # Auto-includes into Rails controllers
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Running Tests
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
bundle install
|
|
557
|
+
bundle exec rspec --format documentation
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## License
|
|
563
|
+
|
|
564
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Respondo
|
|
4
|
+
# Global configuration for Respondo.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# Respondo.configure do |config|
|
|
8
|
+
# config.default_success_message = "OK"
|
|
9
|
+
# config.default_error_message = "Something went wrong"
|
|
10
|
+
# config.include_request_id = true
|
|
11
|
+
# config.camelize_keys = true
|
|
12
|
+
# end
|
|
13
|
+
class Configuration
|
|
14
|
+
# Message used when none is supplied to render_success
|
|
15
|
+
attr_accessor :default_success_message
|
|
16
|
+
|
|
17
|
+
# Message used when none is supplied to render_error
|
|
18
|
+
attr_accessor :default_error_message
|
|
19
|
+
|
|
20
|
+
# When true, includes request.request_id in every meta block (Rails only)
|
|
21
|
+
attr_accessor :include_request_id
|
|
22
|
+
|
|
23
|
+
# When true, all response keys are camelized (suits Flutter/JS clients)
|
|
24
|
+
attr_accessor :camelize_keys
|
|
25
|
+
|
|
26
|
+
# Custom serializer callable — receives (object) and must return a Hash.
|
|
27
|
+
# Defaults to nil (built-in serialization strategy is used).
|
|
28
|
+
# @example use ActiveModelSerializers
|
|
29
|
+
# config.serializer = ->(obj) { SomeSerializer.new(obj).as_json }
|
|
30
|
+
attr_accessor :serializer
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@default_success_message = "Success"
|
|
34
|
+
@default_error_message = "An error occurred"
|
|
35
|
+
@include_request_id = false
|
|
36
|
+
@camelize_keys = false
|
|
37
|
+
@serializer = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|