jpie 1.0.0 → 1.0.2

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/release.mdc +62 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +82 -38
  5. data/Gemfile +13 -10
  6. data/Gemfile.lock +18 -1
  7. data/README.md +675 -1235
  8. data/Rakefile +22 -0
  9. data/jpie.gemspec +15 -15
  10. data/kiln/app/resources/user_message_resource.rb +2 -0
  11. data/lib/jpie.rb +0 -1
  12. data/lib/json_api/active_storage/deserialization.rb +32 -22
  13. data/lib/json_api/active_storage/detection.rb +36 -41
  14. data/lib/json_api/active_storage/serialization.rb +13 -11
  15. data/lib/json_api/configuration.rb +4 -5
  16. data/lib/json_api/controllers/base_controller.rb +3 -3
  17. data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
  18. data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
  19. data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
  20. data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
  21. data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
  22. data/lib/json_api/controllers/concerns/controller_helpers.rb +11 -215
  23. data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
  24. data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
  25. data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
  26. data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
  27. data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
  28. data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
  29. data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
  30. data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
  31. data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
  32. data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
  33. data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
  34. data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
  35. data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
  36. data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
  37. data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
  38. data/lib/json_api/controllers/concerns/resource_actions.rb +51 -602
  39. data/lib/json_api/controllers/relationships_controller.rb +26 -422
  40. data/lib/json_api/errors/parameter_not_allowed.rb +1 -1
  41. data/lib/json_api/railtie.rb +46 -9
  42. data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
  43. data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
  44. data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
  45. data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
  46. data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
  47. data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
  48. data/lib/json_api/resources/resource.rb +13 -219
  49. data/lib/json_api/routing.rb +56 -47
  50. data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
  51. data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
  52. data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
  53. data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
  54. data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
  55. data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
  56. data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
  57. data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
  58. data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
  59. data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
  60. data/lib/json_api/serialization/deserializer.rb +10 -346
  61. data/lib/json_api/serialization/serializer.rb +17 -260
  62. data/lib/json_api/support/active_storage_support.rb +10 -13
  63. data/lib/json_api/support/collection_query.rb +14 -370
  64. data/lib/json_api/support/concerns/condition_building.rb +57 -0
  65. data/lib/json_api/support/concerns/nested_filters.rb +130 -0
  66. data/lib/json_api/support/concerns/pagination.rb +30 -0
  67. data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
  68. data/lib/json_api/support/concerns/regular_filters.rb +81 -0
  69. data/lib/json_api/support/concerns/sorting.rb +88 -0
  70. data/lib/json_api/support/instrumentation.rb +13 -12
  71. data/lib/json_api/support/param_helpers.rb +9 -6
  72. data/lib/json_api/support/relationship_helpers.rb +4 -2
  73. data/lib/json_api/support/resource_identifier.rb +29 -29
  74. data/lib/json_api/support/responders.rb +5 -5
  75. data/lib/json_api/version.rb +1 -1
  76. metadata +44 -1
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Resources
5
+ module MetaDsl
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def meta(hash = nil, &block)
10
+ @meta = hash || block
11
+ end
12
+
13
+ def resource_meta
14
+ if instance_variable_defined?(:@meta)
15
+ @meta
16
+ elsif superclass != JSONAPI::Resource && superclass.respond_to?(:resource_meta)
17
+ superclass.resource_meta
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Resources
5
+ module ModelClassHelpers
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def resource_for_model(model_class)
10
+ resource_const = "#{model_class.name}Resource"
11
+ resource_const.safe_constantize if resource_const.respond_to?(:safe_constantize) || defined?(ActiveSupport)
12
+ rescue NameError
13
+ nil
14
+ end
15
+
16
+ def model_class
17
+ name.sub(/Resource$/, "").classify.constantize
18
+ end
19
+
20
+ def safe_model_class
21
+ return nil unless respond_to?(:name) && name
22
+ return nil unless defined?(ActiveSupport)
23
+
24
+ name.sub(/Resource$/, "").classify.safe_constantize
25
+ rescue NoMethodError
26
+ nil
27
+ end
28
+
29
+ def reflection_model_class
30
+ model_class
31
+ rescue StandardError
32
+ safe_model_class
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Resources
5
+ module RelationshipsDsl
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def has_one(name, meta: nil, **options)
10
+ @relationships ||= []
11
+ detect_polymorphic(name, options)
12
+ @relationships << { name: name.to_sym, type: :has_one, meta:, options: }
13
+ end
14
+
15
+ def has_many(name, meta: nil, **options)
16
+ @relationships ||= []
17
+ validate_append_only_options!(options)
18
+ detect_polymorphic(name, options)
19
+ @relationships << { name: name.to_sym, type: :has_many, meta:, options: }
20
+ end
21
+
22
+ def belongs_to(name, meta: nil, **options)
23
+ @relationships ||= []
24
+ detect_polymorphic(name, options)
25
+ @relationships << { name: name.to_sym, type: :belongs_to, meta:, options: }
26
+ end
27
+
28
+ def relationship_definitions
29
+ declared_relationships = instance_variable_defined?(:@relationships)
30
+ rels = @relationships || []
31
+ rels = superclass.relationship_definitions + rels if should_inherit_relationships?(declared_relationships)
32
+ rels.uniq { |r| r[:name] }
33
+ end
34
+
35
+ def relationship_names
36
+ relationship_definitions.map { |r| r[:name] }
37
+ end
38
+ end
39
+
40
+ module RelationshipHelperMethods
41
+ def validate_append_only_options!(options)
42
+ if options[:append_only] && options[:purge_on_nil] == true
43
+ raise ArgumentError, "Cannot use append_only: true with purge_on_nil: true"
44
+ end
45
+
46
+ options[:purge_on_nil] = false if options[:append_only] && !options.key?(:purge_on_nil)
47
+ end
48
+
49
+ def detect_polymorphic(name, options)
50
+ return if options.key?(:polymorphic)
51
+
52
+ model_klass = reflection_model_class
53
+ return unless model_klass.respond_to?(:reflect_on_association)
54
+
55
+ reflection = model_klass.reflect_on_association(name)
56
+ options[:polymorphic] = reflection&.polymorphic?
57
+ end
58
+
59
+ def should_inherit_relationships?(declared_relationships)
60
+ !declared_relationships &&
61
+ superclass != JSONAPI::Resource &&
62
+ superclass.respond_to?(:relationship_definitions)
63
+ end
64
+ end
65
+
66
+ included do
67
+ extend RelationshipHelperMethods
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Resources
5
+ module SortableFieldsDsl
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def sortable_fields(*field_names)
10
+ @sortable_fields ||= []
11
+ @sortable_fields.concat(field_names.map(&:to_sym))
12
+ @sortable_fields.uniq!
13
+ end
14
+
15
+ def permitted_sortable_fields
16
+ sort_fields = @sortable_fields || []
17
+ sort_fields = inherited_sort_only_fields + sort_fields if should_inherit_sortable_fields?
18
+ (permitted_attributes + sort_fields).uniq
19
+ end
20
+
21
+ def should_inherit_sortable_fields?
22
+ !instance_variable_defined?(:@sortable_fields) &&
23
+ !instance_variable_defined?(:@attributes) &&
24
+ superclass != JSONAPI::Resource &&
25
+ superclass.respond_to?(:permitted_sortable_fields)
26
+ end
27
+
28
+ def inherited_sort_only_fields
29
+ parent_sort_fields = superclass.permitted_sortable_fields
30
+ parent_attributes = superclass.permitted_attributes
31
+ parent_sort_fields - parent_attributes
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,236 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "concerns/attributes_dsl"
4
+ require_relative "concerns/filters_dsl"
5
+ require_relative "concerns/sortable_fields_dsl"
6
+ require_relative "concerns/relationships_dsl"
7
+ require_relative "concerns/meta_dsl"
8
+ require_relative "concerns/model_class_helpers"
9
+
3
10
  module JSONAPI
4
11
  class Resource
5
- class << self
6
- def resource_for_model(model_class)
7
- resource_const = "#{model_class.name}Resource"
8
- resource_const.safe_constantize if resource_const.respond_to?(:safe_constantize) || defined?(ActiveSupport)
9
- rescue NameError
10
- nil
11
- end
12
-
13
- def attributes(*attrs)
14
- @attributes ||= []
15
- @attributes.concat(attrs.map(&:to_sym))
16
- @attributes.uniq!
17
- end
18
-
19
- def creatable_fields(*fields)
20
- @creatable_fields ||= []
21
- @creatable_fields.concat(fields.map(&:to_sym))
22
- @creatable_fields.uniq!
23
- end
24
-
25
- def updatable_fields(*fields)
26
- @updatable_fields ||= []
27
- @updatable_fields.concat(fields.map(&:to_sym))
28
- @updatable_fields.uniq!
29
- end
30
-
31
- def filters(*filter_names)
32
- @filters ||= []
33
- @filters.concat(filter_names.map(&:to_sym))
34
- @filters.uniq!
35
- end
36
-
37
- def permitted_filters_through
38
- relationship_names
39
- end
40
-
41
- def sortable_fields(*field_names)
42
- @sortable_fields ||= []
43
- @sortable_fields.concat(field_names.map(&:to_sym))
44
- @sortable_fields.uniq!
45
- end
46
-
47
- def meta(hash = nil, &block)
48
- @meta = hash || block
49
- end
50
-
51
- def resource_meta
52
- if instance_variable_defined?(:@meta)
53
- @meta
54
- elsif superclass != JSONAPI::Resource && superclass.respond_to?(:resource_meta)
55
- superclass.resource_meta
56
- end
57
- end
58
-
59
- def has_one(name, meta: nil, **options)
60
- @relationships ||= []
61
-
62
- unless options.key?(:polymorphic)
63
- model_klass = reflection_model_class
64
- if model_klass.respond_to?(:reflect_on_association)
65
- reflection = model_klass.reflect_on_association(name)
66
- options[:polymorphic] = reflection&.polymorphic?
67
- end
68
- end
69
-
70
- @relationships << { name: name.to_sym, type: :has_one, meta:, options: }
71
- end
72
-
73
- def has_many(name, meta: nil, **options)
74
- @relationships ||= []
75
-
76
- if options[:append_only] && options[:purge_on_nil] == true
77
- raise ArgumentError, "Cannot use append_only: true with purge_on_nil: true"
78
- end
79
-
80
- options[:purge_on_nil] = false if options[:append_only] && !options.key?(:purge_on_nil)
81
-
82
- unless options.key?(:polymorphic)
83
- model_klass = reflection_model_class
84
- if model_klass.respond_to?(:reflect_on_association)
85
- reflection = model_klass.reflect_on_association(name)
86
- options[:polymorphic] = reflection&.polymorphic?
87
- end
88
- end
89
-
90
- @relationships << { name: name.to_sym, type: :has_many, meta:, options: }
91
- end
92
-
93
- def belongs_to(name, meta: nil, **options)
94
- @relationships ||= []
95
-
96
- unless options.key?(:polymorphic)
97
- model_klass = reflection_model_class
98
- if model_klass.respond_to?(:reflect_on_association)
99
- reflection = model_klass.reflect_on_association(name)
100
- options[:polymorphic] = reflection&.polymorphic?
101
- end
102
- end
103
-
104
- @relationships << { name: name.to_sym, type: :belongs_to, meta:, options: }
105
- end
106
-
107
- def permitted_attributes
108
- # For STI subclasses, merge with parent class attributes unless the subclass declares its own
109
- declared_attributes = instance_variable_defined?(:@attributes)
110
- attrs = @attributes || []
111
- if !declared_attributes &&
112
- superclass != JSONAPI::Resource &&
113
- superclass.respond_to?(:permitted_attributes)
114
- attrs = superclass.permitted_attributes + attrs
115
- end
116
- attrs.uniq
117
- end
118
-
119
- def permitted_creatable_fields
120
- if instance_variable_defined?(:@creatable_fields)
121
- fields = @creatable_fields || []
122
- elsif superclass != JSONAPI::Resource &&
123
- superclass.respond_to?(:permitted_creatable_fields) &&
124
- superclass.instance_variable_defined?(:@creatable_fields)
125
- parent_fields = superclass.permitted_creatable_fields
126
- fields = parent_fields
127
- else
128
- fields = permitted_attributes
129
- end
130
-
131
- fields.uniq
132
- end
133
-
134
- def permitted_updatable_fields
135
- if instance_variable_defined?(:@updatable_fields)
136
- fields = @updatable_fields || []
137
- elsif superclass != JSONAPI::Resource &&
138
- superclass.respond_to?(:permitted_updatable_fields) &&
139
- superclass.instance_variable_defined?(:@updatable_fields)
140
- parent_fields = superclass.permitted_updatable_fields
141
- fields = parent_fields
142
- else
143
- fields = permitted_attributes
144
- end
145
-
146
- fields.uniq
147
- end
148
-
149
- def permitted_filters
150
- # For STI subclasses, merge with parent class filters unless the subclass declares its own
151
- declared_filters = instance_variable_defined?(:@filters)
152
- filter_list = @filters || []
153
- if !declared_filters &&
154
- superclass != JSONAPI::Resource &&
155
- superclass.respond_to?(:permitted_filters)
156
- filter_list = superclass.permitted_filters + filter_list
157
- end
158
- filter_list.uniq
159
- end
160
-
161
- def permitted_sortable_fields
162
- # Include both attributes (which are sortable) and sort-only fields
163
- # For STI subclasses, merge with parent class sortable fields when no subclass DSL is declared
164
- declared_sortable = instance_variable_defined?(:@sortable_fields)
165
- declared_attributes = instance_variable_defined?(:@attributes)
166
- sort_fields = @sortable_fields || []
167
- if !declared_sortable &&
168
- !declared_attributes &&
169
- superclass != JSONAPI::Resource &&
170
- superclass.respond_to?(:permitted_sortable_fields)
171
- parent_sort_fields = superclass.permitted_sortable_fields
172
- # Only merge parent's sort-only fields, not attributes (attributes are already included via permitted_attributes)
173
- parent_attributes = superclass.permitted_attributes
174
- parent_sort_only = parent_sort_fields - parent_attributes
175
- sort_fields = parent_sort_only + sort_fields
176
- end
177
- # Combine with attributes (all attributes are sortable)
178
- (permitted_attributes + sort_fields).uniq
179
- end
180
-
181
- def relationship_definitions
182
- # For STI subclasses, merge with parent class relationships
183
- declared_relationships = instance_variable_defined?(:@relationships)
184
- rels = @relationships || []
185
- if !declared_relationships &&
186
- superclass != JSONAPI::Resource &&
187
- superclass.respond_to?(:relationship_definitions)
188
- rels = superclass.relationship_definitions + rels
189
- end
190
- rels.uniq { |r| r[:name] }
191
- end
192
-
193
- def relationship_names
194
- relationship_definitions.map { |r| r[:name] }
195
- end
196
-
197
- def model_class
198
- name.sub(/Resource$/, "").classify.constantize
199
- end
200
-
201
- def safe_model_class
202
- return nil unless respond_to?(:name) && name
203
- return nil unless defined?(ActiveSupport)
204
-
205
- name.sub(/Resource$/, "").classify.safe_constantize
206
- rescue NoMethodError
207
- nil
208
- end
209
-
210
- def reflection_model_class
211
- model_class
212
- rescue NameError
213
- safe_model_class
214
- rescue StandardError
215
- safe_model_class
216
- end
217
- end
12
+ include Resources::AttributesDsl
13
+ include Resources::FiltersDsl
14
+ include Resources::SortableFieldsDsl
15
+ include Resources::RelationshipsDsl
16
+ include Resources::MetaDsl
17
+ include Resources::ModelClassHelpers
218
18
 
219
- # Instance method to initialize resource with model instance
220
- # This allows the serializer to instantiate resources directly
221
19
  def initialize(record = nil, context = {})
222
20
  @record = record
223
21
  @context = context
224
22
  @transformed_params = {}
225
23
  end
226
24
 
227
- # Accessor for the underlying model instance
228
- # @record is the preferred internal name, but :resource is kept for backward compatibility
229
25
  attr_reader :record
230
26
  alias resource record
231
27
 
232
- # Returns transformed params accumulated by resource setters during deserialization
233
- # Resources can override this method or use the default implementation
234
28
  def transformed_params
235
29
  @transformed_params || {}
236
30
  end
@@ -2,71 +2,80 @@
2
2
 
3
3
  module JSONAPI
4
4
  module Routing
5
- def jsonapi_resources(resource, controller: nil, defaults: {}, sti: false, **options, &block)
5
+ def jsonapi_resources(resource, controller: nil, defaults: {}, sti: false, **options, &)
6
6
  resource_name = resource.to_s
7
+ controller = detect_controller(resource_name) if controller.nil?
7
8
 
8
- # Smart Controller Detection:
9
- # If no controller is specified, try to find one in the current scope.
10
- # If we find "Trust::VendorsController", use it (by passing nil to 'resources').
11
- # If NOT found, default to the generic "json_api/resources".
12
- if controller.nil?
13
- scoped_module = @scope[:module]
14
- potential_controller_name = if scoped_module
15
- "#{scoped_module.to_s.camelize}::#{resource_name.pluralize.camelize}Controller"
16
- else
17
- "#{resource_name.pluralize.camelize}Controller"
18
- end
9
+ JSONAPI::ResourceLoader.find(resource_name)
10
+ defaults = defaults.merge(format: :jsonapi, resource_type: resource_name)
11
+ options[:only] = :index if sti
19
12
 
20
- begin
21
- # Check if the controller exists constant
22
- potential_controller_name.constantize
23
- # It exists! Leave controller as nil so Rails uses standard routing conventions
24
- rescue NameError
25
- # It doesn't exist, fallback to generic controller
26
- controller = "json_api/resources"
27
- end
28
- end
13
+ define_resource_routes(resource, controller, defaults, options, &)
14
+ define_sti_routes(resource, resource_name, defaults, sti)
15
+ end
29
16
 
30
- # Validate that resource class exists at boot time
31
- JSONAPI::ResourceLoader.find(resource_name)
17
+ private
32
18
 
33
- defaults = defaults.merge(format: :jsonapi, resource_type: resource_name)
19
+ def detect_controller(resource_name)
20
+ potential_controller_name = build_controller_name(resource_name)
21
+ potential_controller_name.constantize
22
+ nil
23
+ rescue NameError
24
+ "json_api/resources"
25
+ end
34
26
 
35
- options[:only] = :index if sti
27
+ def build_controller_name(resource_name)
28
+ scoped_module = @scope[:module]
29
+ base_name = "#{resource_name.pluralize.camelize}Controller"
30
+ return base_name unless scoped_module
36
31
 
37
- resources(resource, controller:, defaults:, **options) do
38
- member do
39
- get "relationships/:relationship_name", to: "json_api/relationships#show", as: :relationship
40
- patch "relationships/:relationship_name", to: "json_api/relationships#update"
41
- delete "relationships/:relationship_name", to: "json_api/relationships#destroy"
42
- end
32
+ "#{scoped_module.to_s.camelize}::#{base_name}"
33
+ end
43
34
 
35
+ def define_resource_routes(resource, controller, defaults, options, &block)
36
+ resources(resource, controller:, defaults:, **options) do
37
+ define_relationship_routes
44
38
  instance_eval(&block) if block
45
39
  end
40
+ end
41
+
42
+ def define_relationship_routes
43
+ member do
44
+ get "relationships/:relationship_name", to: "json_api/relationships#show", as: :relationship
45
+ patch "relationships/:relationship_name", to: "json_api/relationships#update"
46
+ delete "relationships/:relationship_name", to: "json_api/relationships#destroy"
47
+ end
48
+ end
46
49
 
50
+ def define_sti_routes(resource, resource_name, defaults, sti)
47
51
  return unless sti
48
52
 
49
53
  if sti.is_a?(Array)
50
- sti.each do |sub_resource_name|
51
- jsonapi_resources(sub_resource_name, defaults:)
52
- end
54
+ define_explicit_sti_routes(sti,
55
+ defaults,)
53
56
  else
54
- begin
55
- resource_class = JSONAPI::ResourceLoader.find(resource_name)
56
- model_class = resource_class.model_class
57
+ define_auto_sti_routes(resource, resource_name,
58
+ defaults,)
59
+ end
60
+ end
61
+
62
+ def define_explicit_sti_routes(sti_resources, defaults)
63
+ sti_resources.each { |sub_resource_name| jsonapi_resources(sub_resource_name, defaults:) }
64
+ end
65
+
66
+ def define_auto_sti_routes(resource, resource_name, defaults)
67
+ resource_class = JSONAPI::ResourceLoader.find(resource_name)
68
+ model_class = resource_class.model_class
69
+ return unless model_class.respond_to?(:descendants)
57
70
 
58
- if model_class.respond_to?(:descendants)
59
- model_class.descendants.each do |subclass|
60
- sub_resource_name = subclass.name.underscore.pluralize.to_sym
61
- next if sub_resource_name == resource.to_sym
71
+ model_class.descendants.each do |subclass|
72
+ sub_resource_name = subclass.name.underscore.pluralize.to_sym
73
+ next if sub_resource_name == resource.to_sym
62
74
 
63
- jsonapi_resources(sub_resource_name, defaults:)
64
- end
65
- end
66
- rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
67
- # Silently skip if model/resource not found
68
- end
75
+ jsonapi_resources(sub_resource_name, defaults:)
69
76
  end
77
+ rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
78
+ nil
70
79
  end
71
80
  end
72
81
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module AttributesDeserialization
6
+ def attributes
7
+ attrs = extract_attributes_from_params
8
+ attrs = attrs.transform_keys(&:to_sym) if attrs.respond_to?(:transform_keys)
9
+ permitted_attrs = permitted_attributes_for_action
10
+ attrs.slice(*permitted_attrs)
11
+ end
12
+
13
+ def extract_attributes_from_params
14
+ @params.dig(:data, :attributes) || @params[:attributes] || {}
15
+ end
16
+
17
+ def permitted_attributes_for_action
18
+ fields = if @action == :create
19
+ @definition.permitted_creatable_fields
20
+ else
21
+ @definition.permitted_updatable_fields
22
+ end
23
+ fields.map(&:to_sym)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module AttributesSerialization
6
+ def serialize_attributes(fields = {})
7
+ type_fields = extract_type_fields(fields)
8
+ return {} if type_fields.empty? && fields.any?
9
+
10
+ attributes = build_attributes_hash
11
+ return attributes if type_fields.empty?
12
+
13
+ attributes.slice(*type_fields.map(&:to_sym))
14
+ end
15
+
16
+ private
17
+
18
+ def extract_type_fields(fields)
19
+ fields[record_type.to_sym] || []
20
+ end
21
+
22
+ def build_attributes_hash
23
+ permitted_attrs = definition.permitted_attributes.map(&:to_sym)
24
+ attributes = {}
25
+ definition_instance = definition.new(record, {})
26
+
27
+ permitted_attrs.each do |attr_sym|
28
+ attributes[attr_sym] = get_attribute_value(definition_instance, attr_sym)
29
+ end
30
+
31
+ attributes.compact
32
+ end
33
+
34
+ def get_attribute_value(definition_instance, attr_sym)
35
+ return definition_instance.public_send(attr_sym) if definition_instance.respond_to?(attr_sym, false)
36
+
37
+ attr_name = attr_sym.to_s
38
+ return record.attributes[attr_name] if model_has_attribute?(attr_name)
39
+
40
+ nil
41
+ end
42
+
43
+ def model_has_attribute?(attr_name)
44
+ record.respond_to?(:attributes) &&
45
+ record.attributes.is_a?(Hash) &&
46
+ record.attributes.key?(attr_name)
47
+ end
48
+ end
49
+ end
50
+ end