respondo 0.1.0 → 2.0.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 +179 -0
- data/README.md +419 -188
- data/lib/respondo/configuration.rb +6 -0
- data/lib/respondo/controller_helpers.rb +272 -67
- data/lib/respondo/pagination.rb +141 -83
- data/lib/respondo/response_builder.rb +48 -19
- data/lib/respondo/version.rb +1 -1
- data/lib/respondo.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Respondo 🎯
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/respondo)
|
|
4
|
+

|
|
5
|
+
|
|
3
6
|
Smart JSON API response formatter for Rails — consistent structure every time, across every app.
|
|
4
7
|
|
|
5
8
|
```json
|
|
@@ -57,23 +60,54 @@ Respondo auto-includes itself into `ActionController::Base` and `ActionControlle
|
|
|
57
60
|
|
|
58
61
|
Every single response — success or error — returns the same four keys:
|
|
59
62
|
|
|
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 |
|
|
63
|
+
| Key | Type | Description |
|
|
64
|
+
|-----------|------------------|------------------------------------------------------|
|
|
65
|
+
| `success` | Boolean | `true` or `false` |
|
|
66
|
+
| `message` | String | Human-readable description |
|
|
67
|
+
| `data` | Object/Array/nil | The payload |
|
|
68
|
+
| `meta` | Object | Timestamp + pagination (if passed) + optional request_id |
|
|
66
69
|
|
|
67
70
|
Error responses additionally include:
|
|
68
71
|
|
|
69
72
|
| Key | Type | Description |
|
|
70
73
|
|----------|------|--------------------------------------|
|
|
71
74
|
| `errors` | Hash | Field-level errors `{field: [msgs]}` |
|
|
75
|
+
| `errors` | Hash | Field-level errors `{field: msgs}` |
|
|
72
76
|
|
|
73
77
|
---
|
|
74
78
|
|
|
75
79
|
## Complete Helper Reference
|
|
76
80
|
|
|
81
|
+
### 1xx — Informational Helpers
|
|
82
|
+
|
|
83
|
+
> 1xx responses are protocol-level and carry no body in standard HTTP/1.1. Respondo still returns a JSON body for API logging consistency. Use with care.
|
|
84
|
+
|
|
85
|
+
#### `render_continue` — 100
|
|
86
|
+
```ruby
|
|
87
|
+
render_continue
|
|
88
|
+
render_continue(message: "Continue sending request body")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### `render_switching_protocols` — 101
|
|
92
|
+
```ruby
|
|
93
|
+
render_switching_protocols
|
|
94
|
+
render_switching_protocols(message: "Upgrading to WebSocket")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `render_processing` — 102
|
|
98
|
+
```ruby
|
|
99
|
+
render_processing
|
|
100
|
+
render_processing(message: "Working on it — please wait")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `render_early_hints` — 103
|
|
104
|
+
```ruby
|
|
105
|
+
render_early_hints
|
|
106
|
+
render_early_hints(message: "Early hints provided", meta: { link: "</style.css>; rel=preload" })
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
77
111
|
### 2xx — Success Helpers
|
|
78
112
|
|
|
79
113
|
#### `render_ok` — 200 OK
|
|
@@ -99,6 +133,13 @@ render_accepted(message: "Your export is being processed. You will receive an em
|
|
|
99
133
|
render_accepted(data: { job_id: "abc123" }, message: "Job queued")
|
|
100
134
|
```
|
|
101
135
|
|
|
136
|
+
#### `render_non_authoritative` — 203 Non-Authoritative Information
|
|
137
|
+
Use when data comes from a third-party or cache rather than the origin server.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
render_non_authoritative(data: @user, message: "Data sourced from cache")
|
|
141
|
+
```
|
|
142
|
+
|
|
102
143
|
#### `render_no_content` — 200 OK
|
|
103
144
|
Use after DELETE or actions with no meaningful response body. Returns standard JSON structure for consistency.
|
|
104
145
|
|
|
@@ -107,6 +148,13 @@ render_no_content # "Deleted successfully"
|
|
|
107
148
|
render_no_content(message: "Account deactivated")
|
|
108
149
|
```
|
|
109
150
|
|
|
151
|
+
#### `render_reset_content` — 205 Reset Content
|
|
152
|
+
Tell the client to reset the document view (e.g. clear a form after submission).
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
render_reset_content(message: "Form submitted — please reset the view")
|
|
156
|
+
```
|
|
157
|
+
|
|
110
158
|
#### `render_partial_content` — 206 Partial Content
|
|
111
159
|
Use for chunked or range-based responses.
|
|
112
160
|
|
|
@@ -125,138 +173,216 @@ render_multi_status(
|
|
|
125
173
|
)
|
|
126
174
|
```
|
|
127
175
|
|
|
176
|
+
#### `render_already_reported` — 208 Already Reported
|
|
177
|
+
```ruby
|
|
178
|
+
render_already_reported(data: @resource, message: "Already reported in this binding")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### `render_im_used` — 226 IM Used
|
|
182
|
+
```ruby
|
|
183
|
+
render_im_used(data: @resource, message: "Delta encoding applied")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### 3xx — Redirect Helpers
|
|
189
|
+
|
|
190
|
+
> These return a JSON body so your API can communicate redirect intent with context.
|
|
191
|
+
> Pass the target URL via `meta: { redirect_url: "..." }`.
|
|
192
|
+
|
|
193
|
+
#### `render_multiple_choices` — 300
|
|
194
|
+
```ruby
|
|
195
|
+
render_multiple_choices(
|
|
196
|
+
data: [{ format: "json", url: "/resource.json" }, { format: "xml", url: "/resource.xml" }],
|
|
197
|
+
message: "Multiple representations available"
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### `render_moved_permanently` — 301
|
|
202
|
+
```ruby
|
|
203
|
+
render_moved_permanently(message: "This endpoint has moved", meta: { redirect_url: new_url })
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `render_found` — 302
|
|
207
|
+
```ruby
|
|
208
|
+
render_found(message: "Resource temporarily at another URL", meta: { redirect_url: temp_url })
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `render_see_other` — 303
|
|
212
|
+
```ruby
|
|
213
|
+
render_see_other(message: "See the canonical resource", meta: { redirect_url: canonical_url })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### `render_not_modified` — 304
|
|
217
|
+
```ruby
|
|
218
|
+
render_not_modified(message: "Your cached version is still valid")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `render_temporary_redirect` — 307
|
|
222
|
+
```ruby
|
|
223
|
+
render_temporary_redirect(message: "Repeat this request at the redirect URL", meta: { redirect_url: url })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `render_permanent_redirect` — 308
|
|
227
|
+
```ruby
|
|
228
|
+
render_permanent_redirect(message: "Permanently moved — update your bookmarks", meta: { redirect_url: url })
|
|
229
|
+
```
|
|
230
|
+
|
|
128
231
|
---
|
|
129
232
|
|
|
130
233
|
### 4xx — Client Error Helpers
|
|
131
234
|
|
|
132
235
|
#### `render_bad_request` — 400 Bad Request
|
|
133
|
-
Malformed request, missing required parameters, invalid format.
|
|
134
|
-
|
|
135
236
|
```ruby
|
|
136
|
-
render_bad_request
|
|
137
|
-
render_bad_request("
|
|
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
|
|
237
|
+
render_bad_request(message: "The 'date' parameter is required")
|
|
238
|
+
render_bad_request(message: "Invalid input", errors: { date: ["must be a valid date"] })
|
|
140
239
|
```
|
|
141
240
|
|
|
142
241
|
#### `render_unauthorized` — 401 Unauthorized
|
|
143
|
-
User is not authenticated. Use when no valid token/session is present.
|
|
144
|
-
|
|
145
242
|
```ruby
|
|
146
|
-
render_unauthorized
|
|
147
|
-
render_unauthorized("
|
|
148
|
-
render_unauthorized("Token has expired", code: "TOKEN_EXPIRED")
|
|
243
|
+
render_unauthorized(message: "Please log in to continue")
|
|
244
|
+
render_unauthorized(message: "Token has expired")
|
|
149
245
|
```
|
|
150
246
|
|
|
151
247
|
#### `render_payment_required` — 402 Payment Required
|
|
152
|
-
Feature is behind a paywall or subscription.
|
|
153
|
-
|
|
154
248
|
```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")
|
|
249
|
+
render_payment_required(message: "Upgrade to Pro to access this feature")
|
|
158
250
|
```
|
|
159
251
|
|
|
160
252
|
#### `render_forbidden` — 403 Forbidden
|
|
161
|
-
User is authenticated but lacks permission for this action.
|
|
162
|
-
|
|
163
253
|
```ruby
|
|
164
|
-
render_forbidden
|
|
165
|
-
render_forbidden("You can only edit your own posts")
|
|
166
|
-
render_forbidden("Admin access required", code: "ADMIN_REQUIRED")
|
|
254
|
+
render_forbidden(message: "You can only edit your own posts")
|
|
167
255
|
```
|
|
168
256
|
|
|
169
257
|
#### `render_not_found` — 404 Not Found
|
|
170
|
-
|
|
258
|
+
```ruby
|
|
259
|
+
render_not_found(message: "User not found")
|
|
260
|
+
render_not_found(message: "Post ##{params[:id]} does not exist")
|
|
261
|
+
```
|
|
171
262
|
|
|
263
|
+
#### `render_method_not_allowed` — 405
|
|
172
264
|
```ruby
|
|
173
|
-
|
|
174
|
-
render_not_found("User not found")
|
|
175
|
-
render_not_found("Post ##{params[:id]} does not exist")
|
|
265
|
+
render_method_not_allowed(message: "This endpoint only accepts POST requests")
|
|
176
266
|
```
|
|
177
267
|
|
|
178
|
-
#### `
|
|
179
|
-
|
|
268
|
+
#### `render_not_acceptable` — 406
|
|
269
|
+
```ruby
|
|
270
|
+
render_not_acceptable(message: "Only application/json is supported")
|
|
271
|
+
```
|
|
180
272
|
|
|
273
|
+
#### `render_proxy_auth_required` — 407
|
|
181
274
|
```ruby
|
|
182
|
-
|
|
183
|
-
render_method_not_allowed("This endpoint only accepts POST requests")
|
|
275
|
+
render_proxy_auth_required(message: "Authenticate with the proxy first")
|
|
184
276
|
```
|
|
185
277
|
|
|
186
|
-
#### `
|
|
187
|
-
|
|
278
|
+
#### `render_request_timeout` — 408
|
|
279
|
+
```ruby
|
|
280
|
+
render_request_timeout(message: "The query took too long. Try a smaller date range.")
|
|
281
|
+
```
|
|
188
282
|
|
|
283
|
+
#### `render_conflict` — 409 Conflict
|
|
189
284
|
```ruby
|
|
190
|
-
|
|
191
|
-
|
|
285
|
+
render_conflict(message: "Email address is already registered")
|
|
286
|
+
render_conflict(message: "Duplicate entry", errors: { email: ["has already been taken"] })
|
|
192
287
|
```
|
|
193
288
|
|
|
194
|
-
#### `
|
|
195
|
-
|
|
289
|
+
#### `render_gone` — 410 Gone
|
|
290
|
+
```ruby
|
|
291
|
+
render_gone(message: "This account has been permanently deleted")
|
|
292
|
+
```
|
|
196
293
|
|
|
294
|
+
#### `render_length_required` — 411
|
|
197
295
|
```ruby
|
|
198
|
-
|
|
199
|
-
render_request_timeout("The query took too long. Try a smaller date range.")
|
|
296
|
+
render_length_required(message: "Content-Length header is required")
|
|
200
297
|
```
|
|
201
298
|
|
|
202
|
-
#### `
|
|
203
|
-
|
|
299
|
+
#### `render_precondition_failed` — 412
|
|
300
|
+
```ruby
|
|
301
|
+
render_precondition_failed(message: "Resource has been modified since your last request")
|
|
302
|
+
```
|
|
204
303
|
|
|
304
|
+
#### `render_payload_too_large` — 413
|
|
205
305
|
```ruby
|
|
206
|
-
|
|
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"] })
|
|
306
|
+
render_payload_too_large(message: "File exceeds the 10 MB upload limit")
|
|
210
307
|
```
|
|
211
308
|
|
|
212
|
-
#### `
|
|
213
|
-
|
|
309
|
+
#### `render_uri_too_long` — 414
|
|
310
|
+
```ruby
|
|
311
|
+
render_uri_too_long(message: "That URL is too long to process")
|
|
312
|
+
```
|
|
214
313
|
|
|
314
|
+
#### `render_unsupported_media_type` — 415
|
|
215
315
|
```ruby
|
|
216
|
-
|
|
217
|
-
render_gone("This account has been permanently deleted")
|
|
316
|
+
render_unsupported_media_type(message: "Please send requests as application/json")
|
|
218
317
|
```
|
|
219
318
|
|
|
220
|
-
#### `
|
|
221
|
-
|
|
319
|
+
#### `render_range_not_satisfiable` — 416
|
|
320
|
+
```ruby
|
|
321
|
+
render_range_not_satisfiable(message: "Requested byte range is out of bounds")
|
|
322
|
+
```
|
|
222
323
|
|
|
324
|
+
#### `render_expectation_failed` — 417
|
|
223
325
|
```ruby
|
|
224
|
-
|
|
225
|
-
render_precondition_failed("Resource has been modified since your last request")
|
|
326
|
+
render_expectation_failed(message: "Expect header value cannot be met")
|
|
226
327
|
```
|
|
227
328
|
|
|
228
|
-
#### `
|
|
229
|
-
|
|
329
|
+
#### `render_im_a_teapot` — 418
|
|
330
|
+
```ruby
|
|
331
|
+
render_im_a_teapot(message: "I'm a teapot — I cannot brew coffee")
|
|
332
|
+
```
|
|
230
333
|
|
|
334
|
+
#### `render_misdirected_request` — 421
|
|
231
335
|
```ruby
|
|
232
|
-
|
|
233
|
-
render_unsupported_media_type("Please send requests as application/json")
|
|
336
|
+
render_misdirected_request(message: "Request sent to the wrong server")
|
|
234
337
|
```
|
|
235
338
|
|
|
236
339
|
#### `render_unprocessable` — 422 Unprocessable Entity
|
|
237
340
|
Validation errors. The most commonly used error helper in Rails APIs.
|
|
238
341
|
|
|
239
342
|
```ruby
|
|
240
|
-
render_unprocessable("Validation failed", errors: user.errors)
|
|
241
|
-
render_unprocessable("Invalid data", errors: { name: ["can't be blank"]
|
|
343
|
+
render_unprocessable(message: "Validation failed", errors: user.errors)
|
|
344
|
+
render_unprocessable(message: "Invalid data", errors: { name: ["can't be blank"] })
|
|
242
345
|
```
|
|
243
346
|
|
|
244
|
-
#### `render_locked` — 423
|
|
245
|
-
|
|
347
|
+
#### `render_locked` — 423
|
|
348
|
+
```ruby
|
|
349
|
+
render_locked(message: "This record is locked by another user")
|
|
350
|
+
```
|
|
246
351
|
|
|
352
|
+
#### `render_failed_dependency` — 424
|
|
247
353
|
```ruby
|
|
248
|
-
|
|
249
|
-
render_locked("This record is locked by another user")
|
|
250
|
-
render_locked("Invoice is locked after approval", code: "INVOICE_LOCKED")
|
|
354
|
+
render_failed_dependency(message: "Prerequisite resource creation failed")
|
|
251
355
|
```
|
|
252
356
|
|
|
253
|
-
#### `
|
|
254
|
-
|
|
357
|
+
#### `render_too_early` — 425
|
|
358
|
+
```ruby
|
|
359
|
+
render_too_early(message: "Request may be a replay — rejected for safety")
|
|
360
|
+
```
|
|
255
361
|
|
|
362
|
+
#### `render_upgrade_required` — 426
|
|
256
363
|
```ruby
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
364
|
+
render_upgrade_required(message: "Please upgrade to TLS 1.3")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### `render_precondition_required` — 428
|
|
368
|
+
```ruby
|
|
369
|
+
render_precondition_required(message: "Include an If-Match header with your request")
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### `render_too_many_requests` — 429
|
|
373
|
+
```ruby
|
|
374
|
+
render_too_many_requests(message: "You have exceeded 100 requests per minute.")
|
|
375
|
+
render_too_many_requests(message: "Rate limit hit", meta: { retry_after: 60 })
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### `render_request_header_fields_too_large` — 431
|
|
379
|
+
```ruby
|
|
380
|
+
render_request_header_fields_too_large(message: "Cookie header is too large")
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### `render_unavailable_for_legal_reasons` — 451
|
|
384
|
+
```ruby
|
|
385
|
+
render_unavailable_for_legal_reasons(message: "This content is blocked in your region")
|
|
260
386
|
```
|
|
261
387
|
|
|
262
388
|
---
|
|
@@ -264,11 +390,8 @@ render_too_many_requests("API limit reached", code: "API_LIMIT_EXCEEDED")
|
|
|
264
390
|
### 5xx — Server Error Helpers
|
|
265
391
|
|
|
266
392
|
#### `render_server_error` — 500 Internal Server Error
|
|
267
|
-
An unexpected error occurred on the server.
|
|
268
|
-
|
|
269
393
|
```ruby
|
|
270
|
-
render_server_error
|
|
271
|
-
render_server_error("Something went wrong. Our team has been notified.")
|
|
394
|
+
render_server_error(message: "Something went wrong. Our team has been notified.")
|
|
272
395
|
|
|
273
396
|
# Common pattern — rescue unexpected exceptions
|
|
274
397
|
rescue StandardError => e
|
|
@@ -276,37 +399,55 @@ rescue StandardError => e
|
|
|
276
399
|
render_server_error("An unexpected error occurred")
|
|
277
400
|
```
|
|
278
401
|
|
|
279
|
-
#### `render_not_implemented` — 501
|
|
280
|
-
|
|
402
|
+
#### `render_not_implemented` — 501
|
|
403
|
+
```ruby
|
|
404
|
+
render_not_implemented(message: "CSV export is coming soon")
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### `render_bad_gateway` — 502
|
|
408
|
+
```ruby
|
|
409
|
+
render_bad_gateway(message: "Payment gateway is currently unavailable")
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### `render_service_unavailable` — 503
|
|
413
|
+
```ruby
|
|
414
|
+
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.")
|
|
415
|
+
render_service_unavailable(message: "Maintenance window", meta: { retry_after: 1800 })
|
|
416
|
+
```
|
|
281
417
|
|
|
418
|
+
#### `render_gateway_timeout` — 504
|
|
282
419
|
```ruby
|
|
283
|
-
|
|
284
|
-
render_not_implemented("CSV export is coming soon")
|
|
420
|
+
render_gateway_timeout(message: "The payment processor did not respond in time.")
|
|
285
421
|
```
|
|
286
422
|
|
|
287
|
-
#### `
|
|
288
|
-
|
|
423
|
+
#### `render_http_version_not_supported` — 505
|
|
424
|
+
```ruby
|
|
425
|
+
render_http_version_not_supported(message: "Only HTTP/1.1 and HTTP/2 are supported")
|
|
426
|
+
```
|
|
289
427
|
|
|
428
|
+
#### `render_variant_also_negotiates` — 506
|
|
290
429
|
```ruby
|
|
291
|
-
|
|
292
|
-
render_bad_gateway("Payment gateway is currently unavailable")
|
|
293
|
-
render_bad_gateway("Could not reach the SMS service", code: "SMS_GATEWAY_ERROR")
|
|
430
|
+
render_variant_also_negotiates(message: "Server content-negotiation loop detected")
|
|
294
431
|
```
|
|
295
432
|
|
|
296
|
-
#### `
|
|
297
|
-
|
|
433
|
+
#### `render_insufficient_storage` — 507
|
|
434
|
+
```ruby
|
|
435
|
+
render_insufficient_storage(message: "Disk quota exceeded on this node")
|
|
436
|
+
```
|
|
298
437
|
|
|
438
|
+
#### `render_loop_detected` — 508
|
|
299
439
|
```ruby
|
|
300
|
-
|
|
301
|
-
render_service_unavailable("We are currently under maintenance. Back in 30 minutes.")
|
|
440
|
+
render_loop_detected(message: "Infinite redirect loop detected")
|
|
302
441
|
```
|
|
303
442
|
|
|
304
|
-
#### `
|
|
305
|
-
|
|
443
|
+
#### `render_not_extended` — 510
|
|
444
|
+
```ruby
|
|
445
|
+
render_not_extended(message: "Further extensions required to fulfil this request")
|
|
446
|
+
```
|
|
306
447
|
|
|
448
|
+
#### `render_network_authentication_required` — 511
|
|
307
449
|
```ruby
|
|
308
|
-
|
|
309
|
-
render_gateway_timeout("The payment processor did not respond in time. Please try again.")
|
|
450
|
+
render_network_authentication_required(message: "Sign in to the network portal first")
|
|
310
451
|
```
|
|
311
452
|
|
|
312
453
|
---
|
|
@@ -317,27 +458,38 @@ render_gateway_timeout("The payment processor did not respond in time. Please tr
|
|
|
317
458
|
class UsersController < ApplicationController
|
|
318
459
|
|
|
319
460
|
def index
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
461
|
+
page = (params[:page] || 1).to_i
|
|
462
|
+
per_page = (params[:per_page] || 5).to_i
|
|
463
|
+
|
|
464
|
+
@users = Kaminari.paginate_array(User.active).page(page).per(per_page)
|
|
465
|
+
|
|
466
|
+
render_ok(
|
|
467
|
+
data: @users,
|
|
468
|
+
message: "Users fetched",
|
|
469
|
+
pagination: {
|
|
470
|
+
per_page: per_page.to_i,
|
|
471
|
+
current_page: @users.current_page,
|
|
472
|
+
next_page: @users.next_page,
|
|
473
|
+
prev_page: @users.prev_page,
|
|
474
|
+
total_pages: @users.total_pages,
|
|
475
|
+
total_count: @users.total_count
|
|
476
|
+
}
|
|
477
|
+
)
|
|
323
478
|
end
|
|
324
479
|
|
|
325
480
|
def show
|
|
326
481
|
user = User.find(params[:id])
|
|
327
482
|
render_ok(data: user, message: "User found")
|
|
328
483
|
rescue ActiveRecord::RecordNotFound
|
|
329
|
-
render_not_found("User ##{params[:id]} not found")
|
|
330
|
-
# → 404, { success: false, errors_code: "NOT_FOUND" }
|
|
484
|
+
render_not_found(message: "User ##{params[:id]} not found", error: { id: "User #{params[:id]} not exist"})
|
|
331
485
|
end
|
|
332
486
|
|
|
333
487
|
def create
|
|
334
488
|
user = User.new(user_params)
|
|
335
489
|
if user.save
|
|
336
490
|
render_created(data: user, message: "Account created successfully")
|
|
337
|
-
# → 201
|
|
338
491
|
else
|
|
339
|
-
render_unprocessable("Validation failed", errors: user.errors)
|
|
340
|
-
# → 422, { errors: { email: ["is invalid"] } }
|
|
492
|
+
render_unprocessable(message: "Validation failed", errors: user.errors)
|
|
341
493
|
end
|
|
342
494
|
end
|
|
343
495
|
|
|
@@ -345,26 +497,22 @@ class UsersController < ApplicationController
|
|
|
345
497
|
user = User.find(params[:id])
|
|
346
498
|
|
|
347
499
|
unless user == current_user || current_user.admin?
|
|
348
|
-
render_forbidden("You can only update your own profile")
|
|
349
|
-
# → 403
|
|
500
|
+
render_forbidden(message: "You can only update your own profile", error: { profile: "update your own profile" })
|
|
350
501
|
return
|
|
351
502
|
end
|
|
352
503
|
|
|
353
504
|
if user.update(user_params)
|
|
354
505
|
render_ok(data: user, message: "Profile updated")
|
|
355
506
|
else
|
|
356
|
-
render_conflict("Could not update profile", errors: user.errors)
|
|
357
|
-
# → 409
|
|
507
|
+
render_conflict(message: "Could not update profile", errors: user.errors)
|
|
358
508
|
end
|
|
359
509
|
end
|
|
360
510
|
|
|
361
511
|
def destroy
|
|
362
512
|
User.find(params[:id]).destroy!
|
|
363
513
|
render_no_content(message: "Account deleted")
|
|
364
|
-
# → 200
|
|
365
514
|
rescue ActiveRecord::RecordNotFound
|
|
366
|
-
render_gone("This account no longer exists")
|
|
367
|
-
# → 410
|
|
515
|
+
render_gone(message: "This account no longer exists")
|
|
368
516
|
end
|
|
369
517
|
|
|
370
518
|
end
|
|
@@ -375,11 +523,11 @@ class PaymentsController < ApplicationController
|
|
|
375
523
|
result = PaymentGateway.charge(amount: params[:amount], token: params[:token])
|
|
376
524
|
render_created(data: result, message: "Payment successful")
|
|
377
525
|
rescue PaymentGateway::CardDeclined => e
|
|
378
|
-
render_unprocessable(e.message)
|
|
526
|
+
render_unprocessable(message: e.message)
|
|
379
527
|
rescue PaymentGateway::Timeout
|
|
380
|
-
render_gateway_timeout("Payment processor timed out. You have not been charged.")
|
|
528
|
+
render_gateway_timeout(message: "Payment processor timed out. You have not been charged.")
|
|
381
529
|
rescue PaymentGateway::Error => e
|
|
382
|
-
render_bad_gateway("Payment gateway error: #{e.message}")
|
|
530
|
+
render_bad_gateway(message: "Payment gateway error: #{e.message}")
|
|
383
531
|
end
|
|
384
532
|
|
|
385
533
|
end
|
|
@@ -389,10 +537,9 @@ class ReportsController < ApplicationController
|
|
|
389
537
|
def generate
|
|
390
538
|
ReportJob.perform_later(current_user.id, params[:type])
|
|
391
539
|
render_accepted(
|
|
392
|
-
data:
|
|
540
|
+
data: { estimated_time: "2 minutes" },
|
|
393
541
|
message: "Report is being generated. We will email you when it is ready."
|
|
394
542
|
)
|
|
395
|
-
# → 202
|
|
396
543
|
end
|
|
397
544
|
|
|
398
545
|
end
|
|
@@ -400,103 +547,188 @@ end
|
|
|
400
547
|
|
|
401
548
|
---
|
|
402
549
|
|
|
403
|
-
##
|
|
550
|
+
## Pagination
|
|
404
551
|
|
|
405
|
-
|
|
406
|
-
|
|
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:)
|
|
552
|
+
Respondo does **not** paginate data for you — your pagination library does that.
|
|
553
|
+
You build the pagination hash yourself and pass it via `pagination:`. This keeps the gem simple, dependency-free, and works with any library.
|
|
414
554
|
|
|
415
|
-
|
|
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:)
|
|
555
|
+
### Kaminari
|
|
431
556
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
render_bad_gateway(message, code:)
|
|
436
|
-
render_service_unavailable(message, code:)
|
|
437
|
-
render_gateway_timeout(message, code:)
|
|
438
|
-
```
|
|
557
|
+
```ruby
|
|
558
|
+
def index
|
|
559
|
+
@users = User.page(params[:page]).per(25)
|
|
439
560
|
|
|
440
|
-
|
|
561
|
+
render_ok(
|
|
562
|
+
data: @users,
|
|
563
|
+
message: "Users fetched",
|
|
564
|
+
pagination: {
|
|
565
|
+
current_page: @users.current_page,
|
|
566
|
+
per_page: @users.limit_value,
|
|
567
|
+
total_pages: @users.total_pages,
|
|
568
|
+
total_count: @users.total_count,
|
|
569
|
+
next_page: @users.next_page,
|
|
570
|
+
prev_page: @users.prev_page
|
|
571
|
+
}
|
|
572
|
+
)
|
|
573
|
+
end
|
|
574
|
+
```
|
|
441
575
|
|
|
442
|
-
|
|
576
|
+
### Pagy
|
|
443
577
|
|
|
444
|
-
|
|
578
|
+
```ruby
|
|
579
|
+
def index
|
|
580
|
+
@pagy, @users = pagy(User.all, items: 25)
|
|
445
581
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
582
|
+
render_ok(
|
|
583
|
+
data: @users,
|
|
584
|
+
message: "Users fetched",
|
|
585
|
+
pagination: {
|
|
586
|
+
current_page: @pagy.page,
|
|
587
|
+
per_page: @pagy.items,
|
|
588
|
+
total_pages: @pagy.pages,
|
|
589
|
+
total_count: @pagy.count,
|
|
590
|
+
next_page: @pagy.next,
|
|
591
|
+
prev_page: @pagy.prev
|
|
592
|
+
}
|
|
593
|
+
)
|
|
594
|
+
end
|
|
595
|
+
```
|
|
457
596
|
|
|
458
|
-
###
|
|
597
|
+
### WillPaginate
|
|
459
598
|
|
|
460
599
|
```ruby
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
600
|
+
def index
|
|
601
|
+
@users = User.paginate(page: params[:page], per_page: 25)
|
|
602
|
+
|
|
603
|
+
render_ok(
|
|
604
|
+
data: @users,
|
|
605
|
+
message: "Users fetched",
|
|
606
|
+
pagination: {
|
|
607
|
+
current_page: @users.current_page,
|
|
608
|
+
per_page: @users.per_page,
|
|
609
|
+
total_pages: @users.total_pages,
|
|
610
|
+
total_count: @users.total_entries,
|
|
611
|
+
next_page: @users.next_page,
|
|
612
|
+
prev_page: @users.previous_page
|
|
613
|
+
}
|
|
614
|
+
)
|
|
464
615
|
end
|
|
465
616
|
```
|
|
466
617
|
|
|
467
|
-
|
|
618
|
+
### No pagination
|
|
468
619
|
|
|
469
|
-
|
|
620
|
+
```ruby
|
|
621
|
+
render_ok(data: @user, message: "User found")
|
|
622
|
+
# → meta will have no pagination key at all
|
|
623
|
+
```
|
|
470
624
|
|
|
471
|
-
|
|
625
|
+
---
|
|
472
626
|
|
|
473
|
-
|
|
627
|
+
## Quick Reference Card
|
|
474
628
|
|
|
475
629
|
```ruby
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
```
|
|
630
|
+
# Core
|
|
631
|
+
render_success(data:, message:, meta:, code:, pagination:, code:, status:)
|
|
632
|
+
render_error(message:, errors:, code:, meta:, status:)
|
|
480
633
|
|
|
481
|
-
|
|
634
|
+
# 1xx — Informational
|
|
635
|
+
render_continue(message:, meta:)
|
|
636
|
+
render_switching_protocols(message:, meta:)
|
|
637
|
+
render_processing(message:, meta:)
|
|
638
|
+
render_early_hints(message:, meta:)
|
|
482
639
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
render_ok(data
|
|
486
|
-
|
|
640
|
+
# 2xx — Success
|
|
641
|
+
render_success(data:, message:, meta:, pagination:, code: , status:)
|
|
642
|
+
render_ok(data:, message:, meta:, pagination:)
|
|
643
|
+
render_created(data:, message:, meta:, pagination:)
|
|
644
|
+
render_accepted(data:, message:, meta:, pagination:)
|
|
645
|
+
render_non_authoritative(data:, message:, meta:, pagination:)
|
|
646
|
+
render_no_content(message:, meta:, pagination:)
|
|
647
|
+
render_reset_content(message:, meta:, pagination:)
|
|
648
|
+
render_partial_content(data:, message:, meta:, pagination:)
|
|
649
|
+
render_multi_status(data:, message:, meta:, pagination:)
|
|
650
|
+
render_already_reported(data:, message:, meta:, pagination:)
|
|
651
|
+
render_im_used(data:, message:, meta:, pagination:)
|
|
652
|
+
|
|
653
|
+
# 3xx — Redirects
|
|
654
|
+
render_multiple_choices(data:, message:, meta:, pagination:)
|
|
655
|
+
render_moved_permanently(message:, meta:, pagination:)
|
|
656
|
+
render_found(message:, meta:, pagination:)
|
|
657
|
+
render_see_other(message:, meta:, pagination:)
|
|
658
|
+
render_not_modified(message:, meta:, pagination:)
|
|
659
|
+
render_temporary_redirect(message:, meta:, pagination:)
|
|
660
|
+
render_permanent_redirect(message:, meta:, pagination:)
|
|
487
661
|
|
|
488
|
-
|
|
662
|
+
# 4xx — Client Errors
|
|
663
|
+
render_bad_request(message:, errors:, meta:)
|
|
664
|
+
render_unauthorized(message:, errors:, meta:)
|
|
665
|
+
render_payment_required(message:, errors:, meta:)
|
|
666
|
+
render_forbidden(message:, errors:, meta:)
|
|
667
|
+
render_not_found(message:, errors:, meta:)
|
|
668
|
+
render_method_not_allowed(message:, errors:, meta:)
|
|
669
|
+
render_not_acceptable(message:, errors:, meta:)
|
|
670
|
+
render_proxy_auth_required(message:, errors:, meta:)
|
|
671
|
+
render_request_timeout(message:, errors:, meta:)
|
|
672
|
+
render_conflict(message:, errors:, meta:)
|
|
673
|
+
render_gone(message:, errors:, meta:)
|
|
674
|
+
render_length_required(message:, errors:, meta:)
|
|
675
|
+
render_precondition_failed(message:, errors:, meta:)
|
|
676
|
+
render_payload_too_large(message:, errors:, meta:)
|
|
677
|
+
render_uri_too_long(message:, errors:, meta:)
|
|
678
|
+
render_unsupported_media_type(message:, errors:, meta:)
|
|
679
|
+
render_range_not_satisfiable(message:, errors:, meta:)
|
|
680
|
+
render_expectation_failed(message:, errors:, meta:)
|
|
681
|
+
render_im_a_teapot(message:, errors:, meta:)
|
|
682
|
+
render_misdirected_request(message:, errors:, meta:)
|
|
683
|
+
render_unprocessable(message:, errors:, meta:)
|
|
684
|
+
render_locked(message:, errors:, meta:)
|
|
685
|
+
render_failed_dependency(message:, errors:, meta:)
|
|
686
|
+
render_too_early(message:, errors:, meta:)
|
|
687
|
+
render_upgrade_required(message:, errors:, meta:)
|
|
688
|
+
render_precondition_required(message:, errors:, meta:)
|
|
689
|
+
render_too_many_requests(message:, errors:, meta:)
|
|
690
|
+
render_request_header_fields_too_large(message:, errors:, meta:)
|
|
691
|
+
render_unavailable_for_legal_reasons(message:, errors:, meta:)
|
|
489
692
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
693
|
+
# 5xx — Server Errors
|
|
694
|
+
render_server_error(message:, errors:, meta:)
|
|
695
|
+
render_not_implemented(message:, errors:, meta:)
|
|
696
|
+
render_bad_gateway(message:, errors:, meta:)
|
|
697
|
+
render_service_unavailable(message:, errors:, meta:)
|
|
698
|
+
render_gateway_timeout(message:, errors:, meta:)
|
|
699
|
+
render_http_version_not_supported(message:, errors:, meta:)
|
|
700
|
+
render_variant_also_negotiates(message:, errors:, meta:)
|
|
701
|
+
render_insufficient_storage(message:, errors:, meta:)
|
|
702
|
+
render_loop_detected(message:, errors:, meta:)
|
|
703
|
+
render_not_extended(message:, errors:, meta:)
|
|
704
|
+
render_network_authentication_required(message:, errors:, meta:)
|
|
493
705
|
```
|
|
494
706
|
|
|
495
|
-
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## Auto-Serialization
|
|
710
|
+
|
|
711
|
+
Respondo automatically handles:
|
|
712
|
+
|
|
713
|
+
| Input type | Output |
|
|
714
|
+
|----------------------------------|-------------------------------------|
|
|
715
|
+
| `ActiveRecord::Base` instance | `record.as_json` |
|
|
716
|
+
| `ActiveRecord::Relation` | Array of `as_json` records |
|
|
717
|
+
| `ActiveModel::Errors` | `{ field: ["message", ...] }` |
|
|
718
|
+
| `Hash` | Passed through (values serialized) |
|
|
719
|
+
| `Array` | Each element serialized recursively |
|
|
720
|
+
| `Exception` | `{ message: e.message }` |
|
|
721
|
+
| Anything with `#as_json` | `.as_json` |
|
|
722
|
+
| Anything with `#to_h` | `.to_h` |
|
|
723
|
+
| Primitives (String, Integer...) | As-is |
|
|
724
|
+
|
|
725
|
+
### Custom serializer
|
|
496
726
|
|
|
497
727
|
```ruby
|
|
498
|
-
|
|
499
|
-
|
|
728
|
+
Respondo.configure do |config|
|
|
729
|
+
# Use ActiveModelSerializers, Blueprinter, Panko, etc.
|
|
730
|
+
config.serializer = ->(obj) { UserSerializer.new(obj).as_json }
|
|
731
|
+
end
|
|
500
732
|
```
|
|
501
733
|
|
|
502
734
|
---
|
|
@@ -508,7 +740,7 @@ Respondo.configure { |c| c.camelize_keys = true }
|
|
|
508
740
|
```
|
|
509
741
|
|
|
510
742
|
All keys in the response — including nested `meta.pagination` — are camelized:
|
|
511
|
-
`current_page` → `currentPage`, `total_count` → `totalCount`, `error_code` → `errorCode`.
|
|
743
|
+
`current_page` → `currentPage`, `total_count` → `totalCount`, `next_page` → `nextPage`, `error_code` → `errorCode`.
|
|
512
744
|
|
|
513
745
|
### Flutter Integration
|
|
514
746
|
|
|
@@ -542,9 +774,8 @@ lib/
|
|
|
542
774
|
├── version.rb # VERSION
|
|
543
775
|
├── configuration.rb # Config with defaults
|
|
544
776
|
├── serializer.rb # Auto-detects and serializes any object
|
|
545
|
-
├── pagination.rb # Kaminari / Pagy / WillPaginate extractor
|
|
546
777
|
├── response_builder.rb # Assembles the final Hash
|
|
547
|
-
├── controller_helpers.rb # All render_* helpers (
|
|
778
|
+
├── controller_helpers.rb # All render_* helpers (1xx–5xx)
|
|
548
779
|
└── railtie.rb # Auto-includes into Rails controllers
|
|
549
780
|
```
|
|
550
781
|
|