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.
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Respondo 🎯
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/respondo.svg?icon=si%3Arubygems&icon_color=%23ce0303)](https://badge.fury.io/rb/respondo)
4
+ ![GitHub Repo Views](https://gitviews.com/repo/spatelpatidar/respondo.svg)
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 # 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
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 # "Unauthorized"
147
- render_unauthorized("Please log in to continue")
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
- Requested resource does not exist.
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
- render_not_found
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
- #### `render_method_not_allowed` — 405 Method Not Allowed
179
- The HTTP verb used is not supported for this endpoint.
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
- render_method_not_allowed
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
- #### `render_not_acceptable` — 406 Not Acceptable
187
- The server cannot produce a response matching the client's Accept header.
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
- render_not_acceptable
191
- render_not_acceptable("Only application/json is supported")
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
- #### `render_request_timeout` — 408 Request Timeout
195
- The request took too long to process.
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
- render_request_timeout
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
- #### `render_conflict` — 409 Conflict
203
- Request conflicts with the current state of the resource. Use for duplicate records, state conflicts.
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
- 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"] })
306
+ render_payload_too_large(message: "File exceeds the 10 MB upload limit")
210
307
  ```
211
308
 
212
- #### `render_gone` — 410 Gone
213
- Resource existed but has been permanently deleted.
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
- render_gone
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
- #### `render_precondition_failed` — 412 Precondition Failed
221
- Conditional request headers (If-Match, If-None-Match) did not match.
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
- render_precondition_failed
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
- #### `render_unsupported_media_type` — 415 Unsupported Media Type
229
- The Content-Type header is not supported.
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
- render_unsupported_media_type
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"], age: ["must be over 18"] })
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 Locked
245
- Resource is locked and cannot be modified.
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
- render_locked
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
- #### `render_too_many_requests` — 429 Too Many Requests
254
- Rate limit exceeded.
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
- 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")
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 Not Implemented
280
- The requested feature has not been built yet.
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
- render_not_implemented
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
- #### `render_bad_gateway` — 502 Bad Gateway
288
- An upstream service (third-party API, microservice) returned an invalid response.
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
- 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")
430
+ render_variant_also_negotiates(message: "Server content-negotiation loop detected")
294
431
  ```
295
432
 
296
- #### `render_service_unavailable` — 503 Service Unavailable
297
- Server is temporarily unable to handle the request — maintenance, overloaded.
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
- render_service_unavailable
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
- #### `render_gateway_timeout` — 504 Gateway Timeout
305
- An upstream service timed out before responding.
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
- render_gateway_timeout
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
- users = User.active.page(params[:page]).per(25)
321
- render_ok(data: users, message: "Users fetched")
322
- # → 200, with pagination meta auto-included
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: { estimated_time: "2 minutes" },
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
- ## Quick Reference Card
550
+ ## Pagination
404
551
 
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:)
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
- # 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:)
555
+ ### Kaminari
431
556
 
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
- ```
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
- ## Auto-Serialization
576
+ ### Pagy
443
577
 
444
- Respondo automatically handles:
578
+ ```ruby
579
+ def index
580
+ @pagy, @users = pagy(User.all, items: 25)
445
581
 
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 |
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
- ### Custom serializer
597
+ ### WillPaginate
459
598
 
460
599
  ```ruby
461
- Respondo.configure do |config|
462
- # Use ActiveModelSerializers, Blueprinter, Panko, etc.
463
- config.serializer = ->(obj) { UserSerializer.new(obj).as_json }
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
- ## Pagination Meta
620
+ ```ruby
621
+ render_ok(data: @user, message: "User found")
622
+ # → meta will have no pagination key at all
623
+ ```
470
624
 
471
- Automatically detected — no extra code needed.
625
+ ---
472
626
 
473
- ### Kaminari
627
+ ## Quick Reference Card
474
628
 
475
629
  ```ruby
476
- users = User.page(params[:page]).per(25)
477
- render_ok(data: users)
478
- # meta.pagination is populated automatically
479
- ```
630
+ # Core
631
+ render_success(data:, message:, meta:, code:, pagination:, code:, status:)
632
+ render_error(message:, errors:, code:, meta:, status:)
480
633
 
481
- ### Pagy
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
- ```ruby
484
- pagy, users = pagy(User.all)
485
- render_ok(data: users, pagy: pagy)
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
- ### WillPaginate
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
- ```ruby
491
- users = User.paginate(page: params[:page], per_page: 25)
492
- render_ok(data: users)
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
- ### Suppress pagination
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
- # Even if the collection is paginated, hide the meta
499
- render_ok(data: users, pagination: false)
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 (2xx, 4xx, 5xx)
778
+ ├── controller_helpers.rb # All render_* helpers (1xx–5xx)
548
779
  └── railtie.rb # Auto-includes into Rails controllers
549
780
  ```
550
781