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.
- checksums.yaml +4 -4
- data/.cursor/rules/release.mdc +62 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +82 -38
- data/Gemfile +13 -10
- data/Gemfile.lock +18 -1
- data/README.md +675 -1235
- data/Rakefile +22 -0
- data/jpie.gemspec +15 -15
- data/kiln/app/resources/user_message_resource.rb +2 -0
- data/lib/jpie.rb +0 -1
- data/lib/json_api/active_storage/deserialization.rb +32 -22
- data/lib/json_api/active_storage/detection.rb +36 -41
- data/lib/json_api/active_storage/serialization.rb +13 -11
- data/lib/json_api/configuration.rb +4 -5
- data/lib/json_api/controllers/base_controller.rb +3 -3
- data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
- data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
- data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
- data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +11 -215
- data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
- data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
- data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
- data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
- data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +51 -602
- data/lib/json_api/controllers/relationships_controller.rb +26 -422
- data/lib/json_api/errors/parameter_not_allowed.rb +1 -1
- data/lib/json_api/railtie.rb +46 -9
- data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
- data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
- data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
- data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
- data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
- data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
- data/lib/json_api/resources/resource.rb +13 -219
- data/lib/json_api/routing.rb +56 -47
- data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
- data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
- data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
- data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
- data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
- data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
- data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
- data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
- data/lib/json_api/serialization/deserializer.rb +10 -346
- data/lib/json_api/serialization/serializer.rb +17 -260
- data/lib/json_api/support/active_storage_support.rb +10 -13
- data/lib/json_api/support/collection_query.rb +14 -370
- data/lib/json_api/support/concerns/condition_building.rb +57 -0
- data/lib/json_api/support/concerns/nested_filters.rb +130 -0
- data/lib/json_api/support/concerns/pagination.rb +30 -0
- data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
- data/lib/json_api/support/concerns/regular_filters.rb +81 -0
- data/lib/json_api/support/concerns/sorting.rb +88 -0
- data/lib/json_api/support/instrumentation.rb +13 -12
- data/lib/json_api/support/param_helpers.rb +9 -6
- data/lib/json_api/support/relationship_helpers.rb +4 -2
- data/lib/json_api/support/resource_identifier.rb +29 -29
- data/lib/json_api/support/responders.rb +5 -5
- data/lib/json_api/version.rb +1 -1
- 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:
|
|
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
|
|
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
|