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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '066548a9ebf27a83035e88ae033d84a8aee310cb6ee13fb4e789498ebdfa5d72'
4
- data.tar.gz: 8714c314df3e5125f56cfd79119dc5acbdf1aaa66583dce4ed6e74faa13ca25c
3
+ metadata.gz: 11bbed404d793a44a7b6919b8983aec8906607a0002aa7c4836c302560fbde42
4
+ data.tar.gz: 1fd4af80b2c031485c183f731831da1a701a09bb56f9ef9f91cff828b563214a
5
5
  SHA512:
6
- metadata.gz: d2c2603d5d6f1cf28211af7742f22954562b84283d2818f1bfab517e305b5aa86bf2a7f5214e2b9c8e1d558371f8d440b138a58086faa4d814d4cfd22f5e476a
7
- data.tar.gz: db9adb8eb6635f91fd5d18595285d7d42dc1f4aac13be52d4471f572a5404defe6abde66d816b51f68357ff78587241026a2509d5ae86a864848d00a5ead5869
6
+ metadata.gz: 9374e197eeedc859252d9a00a430db4f991a29961571cd5371e58337cc1ce49796b3c97b4dacd336ee259594a1e3f68e583234537d7c74c37fa76cc24c2c0651
7
+ data.tar.gz: 95d89c45870cbbea7a021a1479205cd30684386c0881313adde2f425635befa04eb86d86ab030a7f5530997312619f1247f971777891cc0c1e4bf7c467a1b7b2
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.6
1
+ ruby-4.0.1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,45 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [1.1.0] - 2025-11-21
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 custom actions with specialized response logic, use **one Responder class per controller** (similar to Pundit's policy pattern). This keeps your codebase organized and maintainable.
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
- ### Why Use Responders?
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
- - **Separation of Concerns**: Keep response logic out of controllers
386
- - **One Class Per Controller**: Similar to Pundit policies - easy to find and maintain
387
- - **Testability**: Test response logic independently
388
- - **Scalability**: Add new actions without creating new files
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] || "#{controller_name.singularize.camelize}Serializer".constantize
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
 
@@ -10,7 +10,7 @@ module JsonapiResponses
10
10
  end
11
11
 
12
12
  def serialize_item(item, serializer_class, context = {})
13
- serializer_class.new(item, context).serializable_hash
13
+ serializer_class.new(item, context || {}).serializable_hash
14
14
  end
15
15
  end
16
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonapiResponses
4
- VERSION = '1.1.1'.freeze
4
+ VERSION = '1.2.0'.freeze
5
5
  end
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.1.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: 2025-11-22 00:00:00.000000000 Z
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: 3.0.3.1
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: []