jsonapi-serializer 2.0.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 +7 -0
- data/LICENSE.txt +201 -0
- data/README.md +668 -0
- data/lib/extensions/has_one.rb +18 -0
- data/lib/fast_jsonapi.rb +10 -0
- data/lib/fast_jsonapi/attribute.rb +5 -0
- data/lib/fast_jsonapi/helpers.rb +12 -0
- data/lib/fast_jsonapi/instrumentation.rb +2 -0
- data/lib/fast_jsonapi/instrumentation/serializable_hash.rb +13 -0
- data/lib/fast_jsonapi/instrumentation/serialized_json.rb +13 -0
- data/lib/fast_jsonapi/instrumentation/skylight.rb +2 -0
- data/lib/fast_jsonapi/instrumentation/skylight/normalizers/base.rb +7 -0
- data/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb +20 -0
- data/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb +20 -0
- data/lib/fast_jsonapi/link.rb +5 -0
- data/lib/fast_jsonapi/object_serializer.rb +358 -0
- data/lib/fast_jsonapi/railtie.rb +11 -0
- data/lib/fast_jsonapi/relationship.rb +224 -0
- data/lib/fast_jsonapi/scalar.rb +29 -0
- data/lib/fast_jsonapi/serialization_core.rb +154 -0
- data/lib/fast_jsonapi/version.rb +3 -0
- data/lib/generators/serializer/USAGE +8 -0
- data/lib/generators/serializer/serializer_generator.rb +19 -0
- data/lib/generators/serializer/templates/serializer.rb.tt +6 -0
- data/lib/jsonapi/serializer.rb +12 -0
- data/lib/jsonapi/serializer/version.rb +5 -0
- metadata +252 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
::ActiveRecord::Associations::Builder::HasOne.class_eval do
|
4
|
+
# Based on
|
5
|
+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
|
6
|
+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11
|
7
|
+
def self.define_accessors(mixin, reflection)
|
8
|
+
super
|
9
|
+
name = reflection.name
|
10
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
11
|
+
def #{name}_id
|
12
|
+
# if an attribute is already defined with this methods name we should just use it
|
13
|
+
return read_attribute(__method__) if has_attribute?(__method__)
|
14
|
+
association(:#{name}).reader.try(:id)
|
15
|
+
end
|
16
|
+
CODE
|
17
|
+
end
|
18
|
+
end
|
data/lib/fast_jsonapi.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module FastJsonapi
|
2
|
+
class << self
|
3
|
+
# Calls either a Proc or a Lambda, making sure to never pass more parameters to it than it can receive
|
4
|
+
#
|
5
|
+
# @param [Proc] proc the Proc or Lambda to call
|
6
|
+
# @param [Array<Object>] *params any number of parameters to be passed to the Proc
|
7
|
+
# @return [Object] the result of the Proc call with the supplied parameters
|
8
|
+
def call_proc(proc, *params)
|
9
|
+
proc.call(*params.take(proc.parameters.length))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support/notifications'
|
2
|
+
|
3
|
+
module FastJsonapi
|
4
|
+
module ObjectSerializer
|
5
|
+
alias serializable_hash_without_instrumentation serializable_hash
|
6
|
+
|
7
|
+
def serializable_hash
|
8
|
+
ActiveSupport::Notifications.instrument(SERIALIZABLE_HASH_NOTIFICATION, { name: self.class.name }) do
|
9
|
+
serializable_hash_without_instrumentation
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support/notifications'
|
2
|
+
|
3
|
+
module FastJsonapi
|
4
|
+
module ObjectSerializer
|
5
|
+
alias serialized_json_without_instrumentation serialized_json
|
6
|
+
|
7
|
+
def serialized_json
|
8
|
+
ActiveSupport::Notifications.instrument(SERIALIZED_JSON_NOTIFICATION, { name: self.class.name }) do
|
9
|
+
serialized_json_without_instrumentation
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'fast_jsonapi/instrumentation/skylight/normalizers/base'
|
2
|
+
require 'fast_jsonapi/instrumentation/serializable_hash'
|
3
|
+
|
4
|
+
module FastJsonapi
|
5
|
+
module Instrumentation
|
6
|
+
module Skylight
|
7
|
+
module Normalizers
|
8
|
+
class SerializableHash < SKYLIGHT_NORMALIZER_BASE_CLASS
|
9
|
+
register FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION
|
10
|
+
|
11
|
+
CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION}".freeze
|
12
|
+
|
13
|
+
def normalize(_trace, _name, payload)
|
14
|
+
[CAT, payload[:name], nil]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'fast_jsonapi/instrumentation/skylight/normalizers/base'
|
2
|
+
require 'fast_jsonapi/instrumentation/serializable_hash'
|
3
|
+
|
4
|
+
module FastJsonapi
|
5
|
+
module Instrumentation
|
6
|
+
module Skylight
|
7
|
+
module Normalizers
|
8
|
+
class SerializedJson < SKYLIGHT_NORMALIZER_BASE_CLASS
|
9
|
+
register FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION
|
10
|
+
|
11
|
+
CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION}".freeze
|
12
|
+
|
13
|
+
def normalize(_trace, _name, payload)
|
14
|
+
[CAT, payload[:name], nil]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,358 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/time'
|
4
|
+
require 'active_support/concern'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
require 'active_support/core_ext/numeric/time'
|
7
|
+
require 'fast_jsonapi/helpers'
|
8
|
+
require 'fast_jsonapi/attribute'
|
9
|
+
require 'fast_jsonapi/relationship'
|
10
|
+
require 'fast_jsonapi/link'
|
11
|
+
require 'fast_jsonapi/serialization_core'
|
12
|
+
|
13
|
+
module FastJsonapi
|
14
|
+
module ObjectSerializer
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
include SerializationCore
|
17
|
+
|
18
|
+
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
|
19
|
+
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
|
20
|
+
TRANSFORMS_MAPPING = {
|
21
|
+
camel: :camelize,
|
22
|
+
camel_lower: [:camelize, :lower],
|
23
|
+
dash: :dasherize,
|
24
|
+
underscore: :underscore
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
included do
|
28
|
+
# Set record_type based on the name of the serializer class
|
29
|
+
set_type(reflected_record_type) if reflected_record_type
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(resource, options = {})
|
33
|
+
process_options(options)
|
34
|
+
|
35
|
+
@resource = resource
|
36
|
+
end
|
37
|
+
|
38
|
+
def serializable_hash
|
39
|
+
return hash_for_collection if is_collection?(@resource, @is_collection)
|
40
|
+
|
41
|
+
hash_for_one_record
|
42
|
+
end
|
43
|
+
alias to_hash serializable_hash
|
44
|
+
|
45
|
+
def hash_for_one_record
|
46
|
+
serializable_hash = { data: nil }
|
47
|
+
serializable_hash[:meta] = @meta if @meta.present?
|
48
|
+
serializable_hash[:links] = @links if @links.present?
|
49
|
+
|
50
|
+
return serializable_hash unless @resource
|
51
|
+
|
52
|
+
serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @includes, @params)
|
53
|
+
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
|
54
|
+
serializable_hash
|
55
|
+
end
|
56
|
+
|
57
|
+
def hash_for_collection
|
58
|
+
serializable_hash = {}
|
59
|
+
|
60
|
+
data = []
|
61
|
+
included = []
|
62
|
+
fieldset = @fieldsets[self.class.record_type.to_sym]
|
63
|
+
@resource.each do |record|
|
64
|
+
data << self.class.record_hash(record, fieldset, @includes, @params)
|
65
|
+
included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
|
66
|
+
end
|
67
|
+
|
68
|
+
serializable_hash[:data] = data
|
69
|
+
serializable_hash[:included] = included if @includes.present?
|
70
|
+
serializable_hash[:meta] = @meta if @meta.present?
|
71
|
+
serializable_hash[:links] = @links if @links.present?
|
72
|
+
serializable_hash
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def process_options(options)
|
78
|
+
@fieldsets = deep_symbolize(options[:fields].presence || {})
|
79
|
+
@params = {}
|
80
|
+
|
81
|
+
return if options.blank?
|
82
|
+
|
83
|
+
@known_included_objects = {}
|
84
|
+
@meta = options[:meta]
|
85
|
+
@links = options[:links]
|
86
|
+
@is_collection = options[:is_collection]
|
87
|
+
@params = options[:params] || {}
|
88
|
+
raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash)
|
89
|
+
|
90
|
+
if options[:include].present?
|
91
|
+
@includes = options[:include].reject(&:blank?).map(&:to_sym)
|
92
|
+
self.class.validate_includes!(@includes)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def deep_symbolize(collection)
|
97
|
+
if collection.is_a? Hash
|
98
|
+
collection.each_with_object({}) do |(k, v), hsh|
|
99
|
+
hsh[k.to_sym] = deep_symbolize(v)
|
100
|
+
end
|
101
|
+
elsif collection.is_a? Array
|
102
|
+
collection.map { |i| deep_symbolize(i) }
|
103
|
+
else
|
104
|
+
collection.to_sym
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def is_collection?(resource, force_is_collection = nil)
|
109
|
+
return force_is_collection unless force_is_collection.nil?
|
110
|
+
|
111
|
+
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
|
112
|
+
end
|
113
|
+
|
114
|
+
class_methods do
|
115
|
+
def inherited(subclass)
|
116
|
+
super(subclass)
|
117
|
+
subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
|
118
|
+
subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
|
119
|
+
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
|
120
|
+
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
|
121
|
+
subclass.transform_method = transform_method
|
122
|
+
subclass.data_links = data_links.dup if data_links.present?
|
123
|
+
subclass.cache_store_instance = cache_store_instance
|
124
|
+
subclass.cache_store_options = cache_store_options
|
125
|
+
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
|
126
|
+
subclass.meta_to_serialize = meta_to_serialize
|
127
|
+
subclass.record_id = record_id
|
128
|
+
end
|
129
|
+
|
130
|
+
def reflected_record_type
|
131
|
+
return @reflected_record_type if defined?(@reflected_record_type)
|
132
|
+
|
133
|
+
@reflected_record_type ||= begin
|
134
|
+
name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_key_transform(transform_name)
|
139
|
+
self.transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
|
140
|
+
|
141
|
+
# ensure that the record type is correctly transformed
|
142
|
+
if record_type
|
143
|
+
set_type(record_type)
|
144
|
+
# TODO: Remove dead code
|
145
|
+
elsif reflected_record_type
|
146
|
+
set_type(reflected_record_type)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def run_key_transform(input)
|
151
|
+
if transform_method.present?
|
152
|
+
input.to_s.send(*@transform_method).to_sym
|
153
|
+
else
|
154
|
+
input.to_sym
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def use_hyphen
|
159
|
+
warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
|
160
|
+
set_key_transform :dash
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_type(type_name)
|
164
|
+
self.record_type = run_key_transform(type_name)
|
165
|
+
end
|
166
|
+
|
167
|
+
def set_id(id_name = nil, &block)
|
168
|
+
self.record_id = block || id_name
|
169
|
+
end
|
170
|
+
|
171
|
+
def cache_options(cache_options)
|
172
|
+
# FIXME: remove this if block once deprecated cache_options are not supported anymore
|
173
|
+
unless cache_options.key?(:store)
|
174
|
+
# fall back to old, deprecated behaviour because no store was passed.
|
175
|
+
# we assume the user explicitly wants new behaviour if he passed a
|
176
|
+
# store because this is the new syntax.
|
177
|
+
deprecated_cache_options(cache_options)
|
178
|
+
return
|
179
|
+
end
|
180
|
+
|
181
|
+
self.cache_store_instance = cache_options[:store]
|
182
|
+
self.cache_store_options = cache_options.except(:store)
|
183
|
+
end
|
184
|
+
|
185
|
+
# FIXME: remove this method once deprecated cache_options are not supported anymore
|
186
|
+
def deprecated_cache_options(cache_options)
|
187
|
+
warn('DEPRECATION WARNING: `store:` is a required cache option, we will default to `Rails.cache` for now. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.')
|
188
|
+
|
189
|
+
%i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
|
190
|
+
warn("DEPRECATION WARNING: `#{key}` is a deprecated cache option and will have no effect soon. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.")
|
191
|
+
end
|
192
|
+
|
193
|
+
self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
|
194
|
+
self.cache_store_options = {
|
195
|
+
expires_in: cache_options[:cache_length] || 5.minutes,
|
196
|
+
race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
def attributes(*attributes_list, &block)
|
201
|
+
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
|
202
|
+
options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
|
203
|
+
self.attributes_to_serialize = {} if attributes_to_serialize.nil?
|
204
|
+
|
205
|
+
# to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
|
206
|
+
block = attributes_list.pop if attributes_list.last.is_a?(Proc)
|
207
|
+
|
208
|
+
attributes_list.each do |attr_name|
|
209
|
+
method_name = attr_name
|
210
|
+
key = run_key_transform(method_name)
|
211
|
+
attributes_to_serialize[key] = Attribute.new(
|
212
|
+
key: key,
|
213
|
+
method: block || method_name,
|
214
|
+
options: options
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
alias_method :attribute, :attributes
|
220
|
+
|
221
|
+
def add_relationship(relationship)
|
222
|
+
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
|
223
|
+
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
|
224
|
+
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
|
225
|
+
|
226
|
+
# TODO: Remove this undocumented option.
|
227
|
+
# Delegate the caching to the serializer exclusively.
|
228
|
+
if !relationship.cached
|
229
|
+
uncachable_relationships_to_serialize[relationship.name] = relationship
|
230
|
+
else
|
231
|
+
cachable_relationships_to_serialize[relationship.name] = relationship
|
232
|
+
end
|
233
|
+
relationships_to_serialize[relationship.name] = relationship
|
234
|
+
end
|
235
|
+
|
236
|
+
def has_many(relationship_name, options = {}, &block)
|
237
|
+
relationship = create_relationship(relationship_name, :has_many, options, block)
|
238
|
+
add_relationship(relationship)
|
239
|
+
end
|
240
|
+
|
241
|
+
def has_one(relationship_name, options = {}, &block)
|
242
|
+
relationship = create_relationship(relationship_name, :has_one, options, block)
|
243
|
+
add_relationship(relationship)
|
244
|
+
end
|
245
|
+
|
246
|
+
def belongs_to(relationship_name, options = {}, &block)
|
247
|
+
relationship = create_relationship(relationship_name, :belongs_to, options, block)
|
248
|
+
add_relationship(relationship)
|
249
|
+
end
|
250
|
+
|
251
|
+
def meta(meta_name = nil, &block)
|
252
|
+
self.meta_to_serialize = block || meta_name
|
253
|
+
end
|
254
|
+
|
255
|
+
def create_relationship(base_key, relationship_type, options, block)
|
256
|
+
name = base_key.to_sym
|
257
|
+
if relationship_type == :has_many
|
258
|
+
base_serialization_key = base_key.to_s.singularize
|
259
|
+
id_postfix = '_ids'
|
260
|
+
else
|
261
|
+
base_serialization_key = base_key
|
262
|
+
id_postfix = '_id'
|
263
|
+
end
|
264
|
+
polymorphic = fetch_polymorphic_option(options)
|
265
|
+
|
266
|
+
Relationship.new(
|
267
|
+
owner: self,
|
268
|
+
key: options[:key] || run_key_transform(base_key),
|
269
|
+
name: name,
|
270
|
+
id_method_name: compute_id_method_name(
|
271
|
+
options[:id_method_name],
|
272
|
+
"#{base_serialization_key}#{id_postfix}".to_sym,
|
273
|
+
polymorphic,
|
274
|
+
options[:serializer],
|
275
|
+
block
|
276
|
+
),
|
277
|
+
record_type: options[:record_type],
|
278
|
+
object_method_name: options[:object_method_name] || name,
|
279
|
+
object_block: block,
|
280
|
+
serializer: options[:serializer],
|
281
|
+
relationship_type: relationship_type,
|
282
|
+
cached: options[:cached],
|
283
|
+
polymorphic: polymorphic,
|
284
|
+
conditional_proc: options[:if],
|
285
|
+
transform_method: @transform_method,
|
286
|
+
links: options[:links],
|
287
|
+
lazy_load_data: options[:lazy_load_data]
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
|
292
|
+
if block.present? || serializer.is_a?(Proc) || polymorphic
|
293
|
+
custom_id_method_name || :id
|
294
|
+
else
|
295
|
+
custom_id_method_name || id_method_name_from_relationship
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def serializer_for(name)
|
300
|
+
namespace = self.name.gsub(/()?\w+Serializer$/, '')
|
301
|
+
serializer_name = name.to_s.demodulize.classify + 'Serializer'
|
302
|
+
serializer_class_name = namespace + serializer_name
|
303
|
+
begin
|
304
|
+
serializer_class_name.constantize
|
305
|
+
rescue NameError
|
306
|
+
raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \
|
307
|
+
"Attempted to find '#{serializer_class_name}'. " \
|
308
|
+
'Consider specifying the serializer directly through options[:serializer].'
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def fetch_polymorphic_option(options)
|
313
|
+
option = options[:polymorphic]
|
314
|
+
return false unless option.present?
|
315
|
+
return option if option.respond_to? :keys
|
316
|
+
|
317
|
+
{}
|
318
|
+
end
|
319
|
+
|
320
|
+
# def link(link_name, link_method_name = nil, &block)
|
321
|
+
def link(*params, &block)
|
322
|
+
self.data_links = {} if data_links.nil?
|
323
|
+
|
324
|
+
options = params.last.is_a?(Hash) ? params.pop : {}
|
325
|
+
link_name = params.first
|
326
|
+
link_method_name = params[-1]
|
327
|
+
key = run_key_transform(link_name)
|
328
|
+
|
329
|
+
data_links[key] = Link.new(
|
330
|
+
key: key,
|
331
|
+
method: block || link_method_name,
|
332
|
+
options: options
|
333
|
+
)
|
334
|
+
end
|
335
|
+
|
336
|
+
def validate_includes!(includes)
|
337
|
+
return if includes.blank?
|
338
|
+
|
339
|
+
includes.each do |include_item|
|
340
|
+
klass = self
|
341
|
+
parse_include_item(include_item).each do |parsed_include|
|
342
|
+
relationships_to_serialize = klass.relationships_to_serialize || {}
|
343
|
+
relationship_to_include = relationships_to_serialize[parsed_include]
|
344
|
+
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
|
345
|
+
|
346
|
+
if relationship_to_include.static_serializer
|
347
|
+
klass = relationship_to_include.static_serializer
|
348
|
+
else
|
349
|
+
# the serializer may change based on the object (e.g. polymorphic relationships),
|
350
|
+
# so inner relationships cannot be validated
|
351
|
+
break
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|