jsonapi_responses 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +40 -1
- data/README.md +157 -6
- data/lib/jsonapi_responses/respondable.rb +5 -1
- data/lib/jsonapi_responses/responder.rb +25 -2
- data/lib/jsonapi_responses/serializable.rb +1 -1
- data/lib/jsonapi_responses/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11bbed404d793a44a7b6919b8983aec8906607a0002aa7c4836c302560fbde42
|
|
4
|
+
data.tar.gz: 1fd4af80b2c031485c183f731831da1a701a09bb56f9ef9f91cff828b563214a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9374e197eeedc859252d9a00a430db4f991a29961571cd5371e58337cc1ce49796b3c97b4dacd336ee259594a1e3f68e583234537d7c74c37fa76cc24c2c0651
|
|
7
|
+
data.tar.gz: 95d89c45870cbbea7a021a1479205cd30684386c0881313adde2f425635befa04eb86d86ab030a7f5530997312619f1247f971777891cc0c1e4bf7c467a1b7b2
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ruby-4.0.1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [1.
|
|
3
|
+
## [1.2.0] - 2026-02-20
|
|
4
|
+
|
|
5
|
+
- **`serialize_for(action)` in Responder**: Serializes the record using a named method on the serializer if it exists, falling back to `serializable_hash` otherwise. Mirrors the Pundit convention — define `def confirm` in your serializer to get a custom shape for that action without duplicating logic in the responder.
|
|
6
|
+
- **Graceful serializer resolution**: `render_with` no longer raises `NameError` if the inferred serializer class does not exist. When no `serializer:` option is passed and the conventional name (e.g. `ConfirmationSerializer`) cannot be found, `serializer_class` resolves to `nil` and the responder can use `serialize_for` or `render_json` directly.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **Serializer responsibility boundary clarified**: The serializer owns the *shape* of the object (including per-action shapes via named methods); the responder owns the *envelope* of the response (`data:`, `message:`, `meta:`, etc.).
|
|
11
|
+
|
|
12
|
+
### Example
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# app/serializers/user_serializer.rb
|
|
16
|
+
class UserSerializer < ApplicationSerializer
|
|
17
|
+
def serializable_hash
|
|
18
|
+
case view
|
|
19
|
+
when :minimal then minimal_hash
|
|
20
|
+
else summary_hash
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Action-scoped shape — called by serialize_for(:confirm)
|
|
25
|
+
def confirm = auth_hash
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def auth_hash
|
|
30
|
+
{ id: resource.id, email: resource.email, confirmed: resource.confirmed? }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# app/responders/confirmation_responder.rb
|
|
35
|
+
class ConfirmationResponder < ApplicationResponder
|
|
36
|
+
def confirm
|
|
37
|
+
render_json({ message: context[:message], user: serialize_for(:confirm) })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- 2025-11-21
|
|
4
43
|
|
|
5
44
|
### Added
|
|
6
45
|
|
data/README.md
CHANGED
|
@@ -301,6 +301,42 @@ Available helpers:
|
|
|
301
301
|
- `pagination_meta(record, context)` - Extract pagination metadata hash
|
|
302
302
|
- `render_collection_with_meta(record, serializer_class, context)` - Render with automatic pagination
|
|
303
303
|
|
|
304
|
+
## Security & Authorization
|
|
305
|
+
|
|
306
|
+
The `view` parameter is a client-side suggestion and should never be trusted blindly. In a production environment, you must validate whether the `current_user` has permission to see the requested level of detail.
|
|
307
|
+
|
|
308
|
+
### Secure by Default Pattern
|
|
309
|
+
|
|
310
|
+
Use your authorization logic (like Pundit) inside the serializer to enforce security:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
class DigitalProductSerializer < ApplicationSerializer
|
|
314
|
+
def serializable_hash
|
|
315
|
+
# Validate the view against permissions
|
|
316
|
+
authorized_view = authorize_view(context[:view] || :summary)
|
|
317
|
+
|
|
318
|
+
case authorized_view
|
|
319
|
+
when :full then full_hash
|
|
320
|
+
when :summary then summary_hash
|
|
321
|
+
else minimal_hash
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
def authorize_view(requested_view)
|
|
328
|
+
return requested_view unless requested_view == :full
|
|
329
|
+
|
|
330
|
+
# Check permission for the 'full' view
|
|
331
|
+
if Pundit.policy!(context[:current_user], resource).show_full?
|
|
332
|
+
:full
|
|
333
|
+
else
|
|
334
|
+
:summary # Fallback to a safe view
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
304
340
|
## Development
|
|
305
341
|
|
|
306
342
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -378,14 +414,48 @@ end
|
|
|
378
414
|
|
|
379
415
|
## Custom Responders - Pundit-Style Pattern (Recommended)
|
|
380
416
|
|
|
381
|
-
For complex
|
|
417
|
+
For complex actions or non-standard logic (like `bulk_upload`, `export_csv`, or `dashboard_stats`), Responders provide a clean way to encapsulate response logic outside of your controllers.
|
|
418
|
+
|
|
419
|
+
### Handling Non-Standard Actions
|
|
382
420
|
|
|
383
|
-
|
|
421
|
+
If you have an action that doesn't fit the standard CRUD pattern, you can use the `action:` parameter to route the response to a specific method in your responder.
|
|
384
422
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
423
|
+
**1. Define the action in your Responder:**
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
# app/responders/product_responder.rb
|
|
427
|
+
class ProductResponder < ApplicationResponder
|
|
428
|
+
# Custom action for bulk uploads
|
|
429
|
+
def bulk_upload
|
|
430
|
+
render_json({
|
|
431
|
+
data: serialize_collection(record),
|
|
432
|
+
meta: {
|
|
433
|
+
processed_at: Time.current,
|
|
434
|
+
total_records: record.count,
|
|
435
|
+
status: 'completed'
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**2. Call it from your Controller:**
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
class Api::V1::ProductsController < ApplicationController
|
|
446
|
+
def bulk_upload
|
|
447
|
+
@products = Product.where(id: params[:ids])
|
|
448
|
+
# The 'action' parameter tells the responder which method to execute
|
|
449
|
+
render_with(@products, responder: ProductResponder, action: :bulk_upload)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Why Use Responders for "Rare" Actions?
|
|
455
|
+
|
|
456
|
+
- **Clean Controllers**: Your controller only handles business logic and fetching data.
|
|
457
|
+
- **Specific Metadata**: Non-standard actions often require unique `meta` tags that would clutter a controller.
|
|
458
|
+
- **Organization**: Even if you have "weird" actions, they remain organized within the Responder assigned to that controller.
|
|
389
459
|
|
|
390
460
|
### The Pundit-Style Approach
|
|
391
461
|
|
|
@@ -546,6 +616,9 @@ class MyCustomResponder < JsonapiResponses::Responder
|
|
|
546
616
|
serialize_collection(record) # For collections
|
|
547
617
|
serialize_item(record) # For single items
|
|
548
618
|
|
|
619
|
+
# Serialize using a named method on the serializer (falls back to serializable_hash)
|
|
620
|
+
serialize_for(:my_action) # Calls serializer.my_action if defined
|
|
621
|
+
|
|
549
622
|
# Check record type
|
|
550
623
|
collection? # true if record is a collection
|
|
551
624
|
single_item? # true if record is a single item
|
|
@@ -556,6 +629,84 @@ class MyCustomResponder < JsonapiResponses::Responder
|
|
|
556
629
|
end
|
|
557
630
|
```
|
|
558
631
|
|
|
632
|
+
## Action-Scoped Serialization with `serialize_for`
|
|
633
|
+
|
|
634
|
+
For actions with a custom response envelope (like an email confirmation that returns `{ message:, user: }` instead of the standard `{ data: }`), you can define a named method on the serializer to describe the exact shape of the object for that action.
|
|
635
|
+
|
|
636
|
+
This mirrors the Pundit convention: just as `PostPolicy#publish?` handles the `publish` action, `UserSerializer#confirm` handles the `confirm` action.
|
|
637
|
+
|
|
638
|
+
**Rule:** the serializer owns the *shape* of the object; the responder owns the *envelope*.
|
|
639
|
+
|
|
640
|
+
### How it works
|
|
641
|
+
|
|
642
|
+
`serialize_for(:action)` instantiates the serializer and calls `.action` on it if defined, otherwise falls back to `serializable_hash`.
|
|
643
|
+
|
|
644
|
+
### Example
|
|
645
|
+
|
|
646
|
+
**1. Define the action shape in the serializer** (alongside your existing views):
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
# app/serializers/user_serializer.rb
|
|
650
|
+
class UserSerializer < ApplicationSerializer
|
|
651
|
+
def serializable_hash
|
|
652
|
+
case view
|
|
653
|
+
when :minimal then minimal_hash
|
|
654
|
+
when :profile then profile_hash
|
|
655
|
+
else summary_hash
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Custom shape for the confirm action — reuses an existing private method
|
|
660
|
+
def confirm = auth_hash
|
|
661
|
+
|
|
662
|
+
private
|
|
663
|
+
|
|
664
|
+
def summary_hash = { id: resource.id, email: resource.email }
|
|
665
|
+
def auth_hash = { id: resource.id, email: resource.email, confirmed: resource.confirmed? }
|
|
666
|
+
end
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**2. Use `serialize_for` in the responder** — no shape logic leaks in:
|
|
670
|
+
|
|
671
|
+
```ruby
|
|
672
|
+
# app/responders/confirmation_responder.rb
|
|
673
|
+
class ConfirmationResponder < ApplicationResponder
|
|
674
|
+
def confirm
|
|
675
|
+
render_json({
|
|
676
|
+
message: context[:message],
|
|
677
|
+
user: serialize_for(:confirm) # → UserSerializer#confirm
|
|
678
|
+
})
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**3. Controller stays minimal:**
|
|
684
|
+
|
|
685
|
+
```ruby
|
|
686
|
+
class Api::V1::ConfirmationsController < ApplicationController
|
|
687
|
+
def confirm
|
|
688
|
+
# ... validation logic ...
|
|
689
|
+
render_with(
|
|
690
|
+
user,
|
|
691
|
+
responder: ConfirmationResponder,
|
|
692
|
+
action: :confirm,
|
|
693
|
+
serializer: UserSerializer,
|
|
694
|
+
context: { message: I18n.t("auth.confirmation.success") }
|
|
695
|
+
)
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Fallback behaviour
|
|
701
|
+
|
|
702
|
+
If the serializer does not define the named method, `serialize_for` silently falls back to `serializable_hash`, so existing serializers require no changes.
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
serialize_for(:confirm) # → UserSerializer#confirm (if defined)
|
|
706
|
+
serialize_for(:confirm) # → UserSerializer#serializable_hash (fallback)
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
|
|
559
710
|
### Complex Example: Categorized Response
|
|
560
711
|
|
|
561
712
|
```ruby
|
|
@@ -167,7 +167,11 @@ module JsonapiResponses
|
|
|
167
167
|
context = (options[:context] || {}).merge(serialization_user)
|
|
168
168
|
# Only use params[:view] if view is not already provided in context
|
|
169
169
|
context[:view] ||= params[:view]&.to_sym
|
|
170
|
-
serializer_class = options[:serializer] ||
|
|
170
|
+
serializer_class = options[:serializer] || begin
|
|
171
|
+
"#{controller_name.singularize.camelize}Serializer".constantize
|
|
172
|
+
rescue NameError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
171
175
|
|
|
172
176
|
# If a custom responder is provided, use it
|
|
173
177
|
if options[:responder]
|
|
@@ -69,6 +69,29 @@ module JsonapiResponses
|
|
|
69
69
|
controller.send(:serialize_item, item, serializer, ctx)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Serialize the record using a named method on the serializer if it exists,
|
|
73
|
+
# otherwise falls back to serializable_hash.
|
|
74
|
+
# Mirrors the Pundit convention: action name on the serializer = custom shape.
|
|
75
|
+
#
|
|
76
|
+
# @example In a serializer
|
|
77
|
+
# def confirm = auth_hash # custom shape for the confirm action
|
|
78
|
+
#
|
|
79
|
+
# @example In a responder
|
|
80
|
+
# def confirm
|
|
81
|
+
# render_json({ message: context[:message], user: serialize_for(:confirm) })
|
|
82
|
+
# end
|
|
83
|
+
#
|
|
84
|
+
# @param action [Symbol] The action name to look up on the serializer
|
|
85
|
+
# @param item [Object, nil] Record to serialize (defaults to record)
|
|
86
|
+
# @param custom_serializer [Class, nil] Override serializer class
|
|
87
|
+
# @param custom_context [Hash, nil] Override context
|
|
88
|
+
# @return [Hash] Serialized representation
|
|
89
|
+
def serialize_for(action, item = nil, custom_serializer = nil, custom_context = nil)
|
|
90
|
+
item ||= record
|
|
91
|
+
serializer = (custom_serializer || serializer_class).new(item, custom_context || context)
|
|
92
|
+
serializer.respond_to?(action) ? serializer.public_send(action) : serializer.serializable_hash
|
|
93
|
+
end
|
|
94
|
+
|
|
72
95
|
# Access to params from the controller
|
|
73
96
|
# @return [ActionController::Parameters]
|
|
74
97
|
def params
|
|
@@ -91,8 +114,8 @@ module JsonapiResponses
|
|
|
91
114
|
# Helper to check if record is a collection
|
|
92
115
|
# @return [Boolean]
|
|
93
116
|
def collection?
|
|
94
|
-
record.is_a?(Array) ||
|
|
95
|
-
record.is_a?(ActiveRecord::Relation) ||
|
|
117
|
+
record.is_a?(Array) ||
|
|
118
|
+
(defined?(ActiveRecord::Relation) && record.is_a?(ActiveRecord::Relation)) ||
|
|
96
119
|
(record.respond_to?(:to_a) && !record.is_a?(Hash))
|
|
97
120
|
end
|
|
98
121
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jsonapi_responses
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oscar Ortega
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: JsonapiResponses simplifies API response handling by allowing multiple
|
|
14
13
|
response formats from a single endpoint, improving performance and reducing endpoint
|
|
@@ -49,7 +48,6 @@ metadata:
|
|
|
49
48
|
documentation_uri: https://github.com/oortega14/jsonapi_responses#readme
|
|
50
49
|
changelog_uri: https://github.com/oortega14/jsonapi_responses/blob/main/CHANGELOG.md
|
|
51
50
|
rubygems_mfa_required: 'true'
|
|
52
|
-
post_install_message:
|
|
53
51
|
rdoc_options: []
|
|
54
52
|
require_paths:
|
|
55
53
|
- lib
|
|
@@ -64,8 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
64
62
|
- !ruby/object:Gem::Version
|
|
65
63
|
version: '0'
|
|
66
64
|
requirements: []
|
|
67
|
-
rubygems_version:
|
|
68
|
-
signing_key:
|
|
65
|
+
rubygems_version: 4.0.3
|
|
69
66
|
specification_version: 4
|
|
70
67
|
summary: A simple way to handle multiple JSON response formats in Rails APIs
|
|
71
68
|
test_files: []
|