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
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json_api/support/relationship_guard"
4
+ require_relative "concerns/attributes_deserialization"
5
+ require_relative "concerns/relationships_deserialization"
6
+ require_relative "concerns/model_attributes_transformation"
7
+ require_relative "concerns/relationship_processing"
8
+ require_relative "concerns/deserialization_helpers"
4
9
 
5
10
  module JSONAPI
6
11
  class Deserializer
7
12
  include ActiveStorageSupport
13
+ include Serialization::AttributesDeserialization
14
+ include Serialization::RelationshipsDeserialization
15
+ include Serialization::ModelAttributesTransformation
16
+ include Serialization::RelationshipProcessing
17
+ include Serialization::DeserializationHelpers
8
18
 
9
19
  def initialize(params, model_class:, action: :create)
10
20
  @params = ParamHelpers.deep_symbolize_params(params)
@@ -12,351 +22,5 @@ module JSONAPI
12
22
  @definition = ResourceLoader.find_for_model(model_class)
13
23
  @action = action.to_sym
14
24
  end
15
-
16
- def attributes
17
- attrs = extract_attributes_from_params
18
- attrs = attrs.transform_keys(&:to_sym) if attrs.respond_to?(:transform_keys)
19
- permitted_attrs = permitted_attributes_for_action
20
- attrs.slice(*permitted_attrs)
21
- end
22
-
23
- def extract_attributes_from_params
24
- @params.dig(:data, :attributes) || @params[:attributes] || {}
25
- end
26
-
27
- def permitted_attributes_for_action
28
- fields = if @action == :create
29
- @definition.permitted_creatable_fields
30
- else
31
- @definition.permitted_updatable_fields
32
- end
33
- fields.map(&:to_sym)
34
- end
35
-
36
- def relationships
37
- # Handle both nested (:data => {:relationships => ...}) and flat (:relationships => ...) structures
38
- rels = @params.dig(:data, :relationships) || @params[:relationships] || {}
39
- rels = rels.to_h if rels.respond_to?(:to_h)
40
- rels.is_a?(Hash) ? rels : {}
41
- end
42
-
43
- def relationship_ids(relationship_name)
44
- relationship = find_relationship(relationship_name)
45
- return [] unless relationship
46
-
47
- data = extract_relationship_data(relationship)
48
- return [] unless data
49
-
50
- extract_ids_from_data(data)
51
- end
52
-
53
- def find_relationship(relationship_name)
54
- relationships[relationship_name.to_sym]
55
- end
56
-
57
- def extract_relationship_data(relationship)
58
- relationship[:data]
59
- end
60
-
61
- def extract_ids_from_data(data)
62
- if data.is_a?(Array)
63
- data.map { |r| extract_id_from_identifier(r) }
64
- else
65
- [extract_id_from_identifier(data)]
66
- end
67
- end
68
-
69
- def extract_id_from_identifier(identifier)
70
- RelationshipHelpers.extract_id_from_identifier(identifier)
71
- end
72
-
73
- def relationship_id(relationship_name)
74
- relationship_ids(relationship_name).first
75
- end
76
-
77
- def to_model_attributes
78
- attrs = attributes.dup
79
- attrs = apply_virtual_attribute_transformers(attrs)
80
- attrs = process_relationships(attrs)
81
- attrs.transform_keys(&:to_s)
82
- end
83
-
84
- def apply_virtual_attribute_transformers(attrs)
85
- transformed_params, attributes_with_setters = invoke_setter_methods(attrs)
86
- attributes_with_setters.each { |attr_sym| attrs.delete(attr_sym) }
87
- merge_transformed_params(attrs, transformed_params)
88
- attrs
89
- end
90
-
91
- def merge_transformed_params(attrs, transformed_params)
92
- return attrs unless transformed_params.is_a?(Hash) && transformed_params.any?
93
-
94
- transformed_params_symbolized = transformed_params.transform_keys(&:to_sym)
95
- attrs.merge!(transformed_params_symbolized)
96
- attrs
97
- end
98
-
99
- def process_relationships(attrs)
100
- permitted_relationships = @definition.relationship_names.map(&:to_s)
101
-
102
- relationships.each do |key, value|
103
- association_name = key.to_s
104
- next unless permitted_relationships.include?(association_name)
105
-
106
- process_relationship(attrs, association_name, value)
107
- end
108
-
109
- attrs
110
- end
111
-
112
- def invoke_setter_methods(attrs)
113
- definition_instance = create_definition_instance_for_setters
114
- return [{}, []] unless definition_instance.respond_to?(:transformed_params, true)
115
-
116
- attributes_with_setters = call_setters(attrs, definition_instance)
117
- [definition_instance.transformed_params, attributes_with_setters]
118
- end
119
-
120
- def create_definition_instance_for_setters
121
- @definition.new(nil, {})
122
- end
123
-
124
- def call_setters(attrs, definition_instance)
125
- attributes_with_setters = []
126
- attrs.each do |attr_sym, attr_value|
127
- next unless has_setter?(definition_instance, attr_sym)
128
-
129
- definition_instance.public_send(:"#{attr_sym}=", attr_value)
130
- attributes_with_setters << attr_sym
131
- end
132
- attributes_with_setters
133
- end
134
-
135
- def has_setter?(definition_instance, attr_sym)
136
- definition_instance.respond_to?(:"#{attr_sym}=", false)
137
- end
138
-
139
- def process_relationship(attrs, association_name, value)
140
- value_hash = normalize_relationship_value(value)
141
- data = extract_data_from_value(value_hash)
142
- param_name = association_param_name(association_name)
143
-
144
- ensure_relationship_writable!(association_name)
145
-
146
- return handle_null_relationship(attrs, param_name, association_name) if data.nil?
147
- return handle_empty_array_relationship(attrs, param_name, association_name) if empty_array?(data)
148
-
149
- validate_relationship_data_format!(data, association_name)
150
- process_relationship_data(attrs, association_name, param_name, data)
151
- end
152
-
153
- def normalize_relationship_value(value)
154
- value.is_a?(Hash) ? value : value.to_h
155
- end
156
-
157
- def extract_data_from_value(value_hash)
158
- value_hash[:data]
159
- end
160
-
161
- def empty_array?(data)
162
- data.is_a?(Array) && data.empty?
163
- end
164
-
165
- def handle_null_relationship(attrs, param_name, association_name)
166
- # Handle ActiveStorage attachments specially
167
- if active_storage_attachment?(association_name)
168
- attrs[association_name.to_s] = nil
169
- else
170
- attrs["#{param_name}_id"] = nil
171
- attrs["#{param_name}_type"] = nil if polymorphic_association?(association_name)
172
- end
173
- end
174
-
175
- def handle_empty_array_relationship(attrs, param_name, association_name)
176
- # Check if this is an ActiveStorage attachment
177
- if active_storage_attachment?(association_name)
178
- attrs[association_name.to_s] = []
179
- else
180
- attrs["#{param_name.singularize}_ids"] = []
181
- end
182
- end
183
-
184
- def validate_relationship_data_format!(data, association_name)
185
- return if valid_relationship_data?(data)
186
-
187
- raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or id"
188
- end
189
-
190
- def process_relationship_data(attrs, association_name, param_name, data)
191
- if data.is_a?(Array)
192
- process_to_many_relationship(attrs, association_name, param_name, data)
193
- else
194
- process_to_one_relationship(attrs, association_name, param_name, data)
195
- end
196
- end
197
-
198
- def process_to_many_relationship(attrs, association_name, param_name, data)
199
- ids = data.map { |r| extract_id(r) }
200
- types = data.map { |r| extract_type(r) }
201
-
202
- # Check if this is an ActiveStorage attachment
203
- if types.any? && self.class.active_storage_blob_type?(types.first)
204
- process_active_storage_attachment(attrs, association_name, ids, singular: false)
205
- return
206
- end
207
-
208
- validate_relationship_type(association_name, types.first) unless polymorphic_association?(association_name)
209
- attrs["#{param_name.singularize}_ids"] = ids
210
- end
211
-
212
- def process_to_one_relationship(attrs, association_name, param_name, data)
213
- id = extract_id(data)
214
- type = extract_type(data)
215
-
216
- if self.class.active_storage_blob_type?(type)
217
- return process_active_storage_attachment(attrs, association_name, id, singular: true)
218
- end
219
-
220
- process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
221
- end
222
-
223
- def process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
224
- if polymorphic_association?(association_name)
225
- process_polymorphic_relationship(attrs, association_name, param_name, id, type)
226
- else
227
- process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
228
- end
229
- end
230
-
231
- def process_polymorphic_relationship(attrs, association_name, param_name, id, type)
232
- class_name = validate_and_get_class_name(type, association_name)
233
- attrs["#{param_name}_id"] = id
234
- attrs["#{param_name}_type"] = class_name
235
- end
236
-
237
- def validate_and_get_class_name(type, association_name)
238
- class_name = RelationshipHelpers.type_to_class_name(type)
239
- class_name.constantize
240
- class_name
241
- rescue NameError
242
- raise ArgumentError,
243
- "Invalid relationship type for #{association_name}: " \
244
- "'#{type}' does not correspond to a valid model class"
245
- end
246
-
247
- def process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
248
- validate_relationship_type(association_name, type)
249
- attrs["#{param_name}_id"] = id
250
- end
251
-
252
- private
253
-
254
- def association_param_name(association_name)
255
- return association_name.singularize unless @model_class
256
-
257
- association = @model_class.reflect_on_association(association_name.to_sym)
258
- # If association doesn't exist, return the singularized name as fallback
259
- return association_name.singularize unless association
260
-
261
- # Use the actual association name (which is already singular for belongs_to, singular for has_many)
262
- association.name.to_s
263
- end
264
-
265
- def polymorphic_association?(association_name)
266
- RelationshipHelpers.polymorphic_association?(@definition, association_name)
267
- end
268
-
269
- def validate_relationship_type(association_name, type)
270
- relationship_def = find_relationship_definition(association_name)
271
- return unless relationship_def
272
-
273
- association = @model_class.reflect_on_association(association_name.to_sym)
274
- return unless association
275
-
276
- if relationship_def[:options][:polymorphic]
277
- validate_polymorphic_type(association_name, type)
278
- else
279
- validate_non_polymorphic_type(association_name, type, association)
280
- end
281
- end
282
-
283
- def find_relationship_definition(association_name)
284
- RelationshipHelpers.find_relationship_definition(@definition, association_name)
285
- end
286
-
287
- def validate_polymorphic_type(association_name, type)
288
- ResourceLoader.find(type)
289
- rescue ResourceLoader::MissingResourceClass
290
- raise ArgumentError,
291
- "Invalid relationship type for #{association_name}: '#{type}' does not have a resource class defined"
292
- end
293
-
294
- def validate_non_polymorphic_type(association_name, type, association)
295
- expected_type = RelationshipHelpers.model_type_name(association.klass)
296
- return if type == expected_type
297
-
298
- raise ArgumentError, "Invalid relationship type for #{association_name}: expected #{expected_type}, got #{type}"
299
- end
300
-
301
- def extract_id(resource_identifier)
302
- id = RelationshipHelpers.extract_id_from_identifier(resource_identifier)
303
- raise ArgumentError, "Missing id in relationship data" unless id
304
-
305
- id
306
- end
307
-
308
- def extract_type(resource_identifier)
309
- type = RelationshipHelpers.extract_type_from_identifier(resource_identifier)
310
- raise ArgumentError, "Missing type in relationship data" unless type
311
-
312
- type
313
- end
314
-
315
- def valid_relationship_data?(data)
316
- if data.is_a?(Array)
317
- data.all? { |r| valid_resource_identifier?(r) }
318
- else
319
- valid_resource_identifier?(data)
320
- end
321
- end
322
-
323
- def valid_resource_identifier?(identifier)
324
- return false unless identifier.is_a?(Hash)
325
-
326
- has_id?(identifier) && has_type?(identifier)
327
- end
328
-
329
- def has_id?(identifier)
330
- identifier[:id]
331
- end
332
-
333
- def has_type?(identifier)
334
- identifier[:type]
335
- end
336
-
337
- def resource_model_class
338
- @model_class
339
- end
340
-
341
- def active_storage_attachment?(association_name)
342
- return false unless @model_class
343
-
344
- self.class.active_storage_attachment?(association_name, @model_class)
345
- end
346
-
347
- def ensure_relationship_writable!(association_name)
348
- return if active_storage_attachment?(association_name)
349
-
350
- association = @model_class.reflect_on_association(association_name.to_sym)
351
- readonly = relationship_options_for(association_name)[:readonly] == true
352
- JSONAPI::RelationshipGuard.ensure_writable!(association, association_name, readonly:) if association
353
- end
354
-
355
- def relationship_options_for(association_name)
356
- relationship_def = RelationshipHelpers.find_relationship_definition(@definition, association_name)
357
- return {} unless relationship_def
358
-
359
- relationship_def[:options] || {}
360
- end
361
25
  end
362
26
  end
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "concerns/attributes_serialization"
4
+ require_relative "concerns/relationships_serialization"
5
+ require_relative "concerns/links_serialization"
6
+ require_relative "concerns/includes_serialization"
7
+ require_relative "concerns/meta_serialization"
8
+
3
9
  module JSONAPI
4
10
  class Serializer
5
11
  include ActiveStorageSupport
12
+ include Serialization::AttributesSerialization
13
+ include Serialization::RelationshipsSerialization
14
+ include Serialization::LinksSerialization
15
+ include Serialization::IncludesSerialization
16
+ include Serialization::MetaSerialization
17
+
18
+ JSONAPI_VERSION = "1.1"
6
19
 
7
- # Class method for generating the JSON:API object
8
20
  def self.jsonapi_object
9
- obj = { version: JSONAPI.configuration.jsonapi_version }
21
+ obj = { version: JSONAPI_VERSION }
10
22
  obj[:meta] = JSONAPI.configuration.jsonapi_meta if JSONAPI.configuration.jsonapi_meta
11
23
  obj
12
24
  end
@@ -15,7 +27,7 @@ module JSONAPI
15
27
  @record = record
16
28
  @definition = definition || ResourceLoader.find_for_model(record.class)
17
29
  @base_definition = base_definition
18
- @sti_subclass = nil # Cache for sti_subclass? check
30
+ @sti_subclass = nil
19
31
  end
20
32
 
21
33
  def to_hash(include: [], fields: {}, document_meta: nil)
@@ -23,7 +35,7 @@ module JSONAPI
23
35
  jsonapi: jsonapi_object,
24
36
  data: serialize_record(fields),
25
37
  included: serialize_included(include, fields),
26
- meta: document_meta
38
+ meta: document_meta,
27
39
  }.compact
28
40
  end
29
41
 
@@ -34,7 +46,7 @@ module JSONAPI
34
46
  attributes: serialize_attributes(fields),
35
47
  relationships: serialize_relationships,
36
48
  links: serialize_links,
37
- meta: record_meta
49
+ meta: record_meta,
38
50
  }.compact
39
51
  end
40
52
 
@@ -58,263 +70,8 @@ module JSONAPI
58
70
  record.id.to_s
59
71
  end
60
72
 
61
- def serialize_attributes(fields = {})
62
- type_fields = extract_type_fields(fields)
63
- return {} if type_fields.empty? && fields.any?
64
-
65
- attributes = build_attributes_hash
66
- return attributes if type_fields.empty?
67
-
68
- attributes.slice(*type_fields.map(&:to_sym))
69
- end
70
-
71
- def extract_type_fields(fields)
72
- fields[record_type.to_sym] || []
73
- end
74
-
75
- def build_attributes_hash
76
- permitted_attrs = definition.permitted_attributes.map(&:to_sym)
77
- attributes = {}
78
- definition_instance = definition.new(record, {})
79
-
80
- permitted_attrs.each do |attr_sym|
81
- attributes[attr_sym] = get_attribute_value(definition_instance, attr_sym)
82
- end
83
-
84
- attributes.compact
85
- end
86
-
87
- def get_attribute_value(definition_instance, attr_sym)
88
- return definition_instance.public_send(attr_sym) if definition_instance.respond_to?(attr_sym, false)
89
-
90
- attr_name = attr_sym.to_s
91
- return record.attributes[attr_name] if model_has_attribute?(attr_name)
92
-
93
- nil
94
- end
95
-
96
- def model_has_attribute?(attr_name)
97
- record.respond_to?(:attributes) &&
98
- record.attributes.is_a?(Hash) &&
99
- record.attributes.key?(attr_name)
100
- end
101
-
102
- def serialize_relationships
103
- relationships = {}
104
- relationship_definitions = definition.relationship_definitions
105
-
106
- relationship_definitions.each do |rel_def|
107
- serialize_relationship(rel_def, relationships)
108
- end
109
-
110
- relationships
111
- end
112
-
113
- def serialize_relationship(rel_def, relationships)
114
- association_name = rel_def[:name]
115
-
116
- if active_storage_attachment?(association_name, record.class)
117
- return serialize_active_storage_relationship_wrapper(rel_def, relationships, association_name)
118
- end
119
-
120
- serialize_regular_relationship(rel_def, relationships, association_name)
121
- end
122
-
123
- def serialize_active_storage_relationship_wrapper(rel_def, relationships, association_name)
124
- relationships[association_name] = {
125
- data: serialize_active_storage_relationship(association_name, record),
126
- meta: rel_def[:meta]
127
- }.compact
128
- end
129
-
130
- def serialize_regular_relationship(rel_def, relationships, association_name)
131
- association = record.class.reflect_on_association(association_name)
132
- return unless association
133
-
134
- relationships[association_name] = {
135
- data: serialize_relationship_data(association),
136
- meta: rel_def[:meta]
137
- }.compact
138
- end
139
-
140
- def serialize_relationship_data(association)
141
- related = record.public_send(association.name)
142
-
143
- if association.collection?
144
- serialize_collection_relationship(related, association)
145
- elsif related
146
- serialize_single_relationship(related, association)
147
- end
148
- end
149
-
150
- def serialize_collection_relationship(related, association)
151
- return [] if related.nil?
152
-
153
- related.map { |r| serialize_identifier_for_related(r, association) }
154
- end
155
-
156
- def serialize_single_relationship(related, association)
157
- serialize_identifier_for_related(related, association)
158
- end
159
-
160
- def serialize_identifier_for_related(related_record, association)
161
- base_def_for_related, use_instance_class = determine_sti_definition(related_record)
162
-
163
- RelationshipHelpers.serialize_resource_identifier(
164
- related_record,
165
- association:,
166
- resource_class: definition,
167
- use_instance_class:,
168
- base_resource_class: base_def_for_related
169
- )
170
- end
171
-
172
- def determine_sti_definition(_related_record)
173
- [nil, true]
174
- end
175
-
176
- def serialize_links
177
- links = { self: "/#{record_type}/#{record_id}" }
178
- add_active_storage_download_link(links) if active_storage_blob?
179
- links
180
- end
181
-
182
- def active_storage_blob?
183
- defined?(::ActiveStorage) && record.is_a?(::ActiveStorage::Blob)
184
- end
185
-
186
- def add_active_storage_download_link(links)
187
- links[:download] = rails_blob_path || fallback_blob_path
188
- rescue StandardError
189
- links[:download] = fallback_blob_path
190
- end
191
-
192
- def rails_blob_path
193
- Rails.application.routes.url_helpers.rails_blob_path(record, only_path: true)
194
- end
195
-
196
- def fallback_blob_path
197
- "/rails/active_storage/blobs/#{record.signed_id}/#{record.filename}"
198
- end
199
-
200
- def serialize_included(includes, fields = {})
201
- return [] if includes.empty?
202
-
203
- included_records = []
204
- processed = Set.new
205
-
206
- includes.each do |include_path|
207
- serialize_include_path(record, include_path, fields, included_records, processed)
208
- end
209
-
210
- included_records
211
- end
212
-
213
- def serialize_include_path(current_record, include_path, fields, included_records, processed)
214
- path_parts = include_path.split(".")
215
- association_name = path_parts.first.to_sym
216
-
217
- return unless valid_include_association?(current_record, association_name)
218
-
219
- related_array = get_related_records(current_record, association_name)
220
- related_array.each do |related_record|
221
- serialize_and_process_record(related_record, fields, included_records, processed)
222
- serialize_nested_path(related_record, path_parts, fields, included_records, processed)
223
- end
224
- end
225
-
226
- def valid_include_association?(current_record, association_name)
227
- current_definition = ResourceLoader.find_for_model(current_record.class)
228
- relationship_def = RelationshipHelpers.find_relationship_definition(current_definition, association_name)
229
- return false unless relationship_def
230
-
231
- # Check for ActiveStorage attachments
232
- return true if self.class.active_storage_attachment?(association_name, current_record.class)
233
-
234
- association = current_record.class.reflect_on_association(association_name)
235
- association.present?
236
- end
237
-
238
- def get_related_records(current_record, association_name)
239
- # Handle ActiveStorage attachments specially
240
- if self.class.active_storage_attachment?(association_name, current_record.class)
241
- attachment = current_record.public_send(association_name)
242
- return [] unless attachment.respond_to?(:attached?) && attachment.attached?
243
- return attachment.blobs.to_a if attachment.is_a?(::ActiveStorage::Attached::Many)
244
-
245
- return [attachment.blob].compact
246
-
247
- end
248
-
249
- related = current_record.public_send(association_name)
250
- Array(related)
251
- end
252
-
253
- def serialize_and_process_record(related_record, fields, included_records, processed)
254
- record_key = build_record_key(related_record)
255
- return if processed.include?(record_key)
256
-
257
- serializer = self.class.new(related_record)
258
- included_records << serializer.serialize_record(fields)
259
- processed.add(record_key)
260
- end
261
-
262
- def build_record_key(related_record)
263
- "#{related_record.class.name}-#{related_record.id}"
264
- end
265
-
266
- def serialize_nested_path(related_record, path_parts, fields, included_records, processed)
267
- return unless path_parts.length > 1
268
-
269
- nested_path = path_parts[1..].join(".")
270
- serialize_include_path(related_record, nested_path, fields, included_records, processed)
271
- end
272
-
273
73
  def jsonapi_object
274
74
  self.class.jsonapi_object
275
75
  end
276
-
277
- def record_meta
278
- custom_meta = custom_meta_from_definition
279
- default_meta = build_default_meta
280
-
281
- # Merge defaults with custom meta (custom meta can override defaults)
282
- return unless default_meta.any? || custom_meta.any?
283
-
284
- default_meta.merge(custom_meta)
285
- end
286
-
287
- def custom_meta_from_definition
288
- meta = nil
289
-
290
- # Try instance method on Resource class first (dynamic, has access to model via resource)
291
- if definition.method_defined?(:meta)
292
- # Directly instantiate the resource class - all resources inherit from ApplicationResource
293
- # which has initialize(resource = nil, context = {}) and attr_reader :resource
294
- instance = definition.new(@record, {})
295
- meta = instance.meta
296
- # Fall back to class method (static)
297
- elsif (class_meta = definition.resource_meta)
298
- meta = class_meta.is_a?(Proc) ? class_meta.call(record) : class_meta
299
- end
300
-
301
- # Convert meta to hash if it's not nil
302
- meta = meta.to_h if meta.respond_to?(:to_h) && !meta.is_a?(Hash)
303
- meta || {}
304
- end
305
-
306
- def build_default_meta
307
- default_timestamp_meta
308
- end
309
-
310
- def default_timestamp_meta
311
- meta = {}
312
-
313
- meta[:created_at] = record.created_at.iso8601 if record.respond_to?(:created_at) && record.created_at.present?
314
-
315
- meta[:updated_at] = record.updated_at.iso8601 if record.respond_to?(:updated_at) && record.updated_at.present?
316
-
317
- meta
318
- end
319
76
  end
320
77
  end