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