ruby_jsonapi 1.0.1

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.
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/time'
5
+ require 'active_support/concern'
6
+ require 'active_support/inflector'
7
+ require 'active_support/core_ext/numeric/time'
8
+ require 'fast_jsonapi/helpers'
9
+ require 'fast_jsonapi/attribute'
10
+ require 'fast_jsonapi/relationship'
11
+ require 'fast_jsonapi/link'
12
+ require 'fast_jsonapi/serialization_core'
13
+
14
+ module FastJsonapi
15
+ module ObjectSerializer
16
+ extend ActiveSupport::Concern
17
+ include SerializationCore
18
+
19
+ TRANSFORMS_MAPPING = {
20
+ camel: :camelize,
21
+ camel_lower: [:camelize, :lower],
22
+ dash: :dasherize,
23
+ underscore: :underscore
24
+ }.freeze
25
+
26
+ included do
27
+ # Set record_type based on the name of the serializer class
28
+ set_type(reflected_record_type) if reflected_record_type
29
+ end
30
+
31
+ def initialize(resource, options = {})
32
+ process_options(options)
33
+
34
+ @resource = resource
35
+ end
36
+
37
+ def serializable_hash
38
+ if self.class.is_collection?(@resource, @is_collection)
39
+ return hash_for_collection
40
+ end
41
+
42
+ hash_for_one_record
43
+ end
44
+ alias to_hash serializable_hash
45
+
46
+ def hash_for_one_record
47
+ serializable_hash = { data: nil }
48
+ serializable_hash[:meta] = @meta if @meta.present?
49
+ serializable_hash[:links] = @links if @links.present?
50
+
51
+ return serializable_hash unless @resource
52
+
53
+ serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @includes, @params)
54
+ serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
55
+ serializable_hash
56
+ end
57
+
58
+ def hash_for_collection
59
+ serializable_hash = {}
60
+
61
+ data = []
62
+ included = []
63
+ fieldset = @fieldsets[self.class.record_type.to_sym]
64
+ @resource.each do |record|
65
+ data << self.class.record_hash(record, fieldset, @includes, @params)
66
+ included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
67
+ end
68
+
69
+ serializable_hash[:data] = data
70
+ serializable_hash[:included] = included if @includes.present?
71
+ serializable_hash[:meta] = @meta if @meta.present?
72
+ serializable_hash[:links] = @links if @links.present?
73
+ serializable_hash
74
+ end
75
+
76
+ private
77
+
78
+ def process_options(options)
79
+ @fieldsets = deep_symbolize(options[:fields].presence || {})
80
+ @params = {}
81
+
82
+ return if options.blank?
83
+
84
+ @known_included_objects = Set.new
85
+ @meta = options[:meta]
86
+ @links = options[:links]
87
+ @is_collection = options[:is_collection]
88
+ @params = options[:params] || {}
89
+ raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash)
90
+
91
+ if options[:include].present?
92
+ @includes = options[:include].reject(&:blank?).map(&:to_sym)
93
+ self.class.validate_includes!(@includes)
94
+ end
95
+ end
96
+
97
+ def deep_symbolize(collection)
98
+ if collection.is_a? Hash
99
+ collection.each_with_object({}) do |(k, v), hsh|
100
+ hsh[k.to_sym] = deep_symbolize(v)
101
+ end
102
+ elsif collection.is_a? Array
103
+ collection.map { |i| deep_symbolize(i) }
104
+ else
105
+ collection.to_sym
106
+ end
107
+ end
108
+
109
+ class_methods do
110
+ # Detects a collection/enumerable
111
+ #
112
+ # @return [TrueClass] on a successful detection
113
+ def is_collection?(resource, force_is_collection = nil)
114
+ return force_is_collection unless force_is_collection.nil?
115
+
116
+ resource.is_a?(Enumerable) && !resource.respond_to?(:each_pair)
117
+ end
118
+
119
+ def inherited(subclass)
120
+ super(subclass)
121
+ subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
122
+ subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
123
+ subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
124
+ subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
125
+ subclass.transform_method = transform_method
126
+ subclass.data_links = data_links.dup if data_links.present?
127
+ subclass.cache_store_instance = cache_store_instance
128
+ subclass.cache_store_options = cache_store_options
129
+ subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
130
+ subclass.meta_to_serialize = meta_to_serialize
131
+ subclass.record_id = record_id
132
+ end
133
+
134
+ def reflected_record_type
135
+ return @reflected_record_type if defined?(@reflected_record_type)
136
+
137
+ @reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer'))
138
+ end
139
+
140
+ def set_key_transform(transform_name)
141
+ self.transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
142
+
143
+ # ensure that the record type is correctly transformed
144
+ if record_type
145
+ set_type(record_type)
146
+ # TODO: Remove dead code
147
+ elsif reflected_record_type
148
+ set_type(reflected_record_type)
149
+ end
150
+ end
151
+
152
+ def run_key_transform(input)
153
+ if transform_method.present?
154
+ input.to_s.send(*@transform_method).to_sym
155
+ else
156
+ input.to_sym
157
+ end
158
+ end
159
+
160
+ def use_hyphen
161
+ warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
162
+ set_key_transform :dash
163
+ end
164
+
165
+ def set_type(type_name)
166
+ self.record_type = run_key_transform(type_name)
167
+ end
168
+
169
+ def set_id(id_name = nil, &block)
170
+ self.record_id = block || id_name
171
+ end
172
+
173
+ def cache_options(cache_options)
174
+ # FIXME: remove this if block once deprecated cache_options are not supported anymore
175
+ unless cache_options.key?(:store)
176
+ # fall back to old, deprecated behaviour because no store was passed.
177
+ # we assume the user explicitly wants new behaviour if he passed a
178
+ # store because this is the new syntax.
179
+ deprecated_cache_options(cache_options)
180
+ return
181
+ end
182
+
183
+ self.cache_store_instance = cache_options[:store]
184
+ self.cache_store_options = cache_options.except(:store)
185
+ end
186
+
187
+ # FIXME: remove this method once deprecated cache_options are not supported anymore
188
+ def deprecated_cache_options(cache_options)
189
+ 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.')
190
+
191
+ %i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
192
+ 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.")
193
+ end
194
+
195
+ self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
196
+ self.cache_store_options = {
197
+ expires_in: cache_options[:cache_length] || 5.minutes,
198
+ race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
199
+ }
200
+ end
201
+
202
+ def attributes(*attributes_list, &block)
203
+ attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
204
+ options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
205
+ self.attributes_to_serialize = {} if attributes_to_serialize.nil?
206
+
207
+ # to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
208
+ block = attributes_list.pop if attributes_list.last.is_a?(Proc)
209
+
210
+ attributes_list.each do |attr_name|
211
+ method_name = attr_name
212
+ key = run_key_transform(method_name)
213
+ attributes_to_serialize[key] = Attribute.new(
214
+ key: key,
215
+ method: block || method_name,
216
+ options: options
217
+ )
218
+ end
219
+ end
220
+
221
+ alias_method :attribute, :attributes
222
+
223
+ def add_relationship(relationship)
224
+ self.relationships_to_serialize = {} if relationships_to_serialize.nil?
225
+ self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
226
+ self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
227
+
228
+ # TODO: Remove this undocumented option.
229
+ # Delegate the caching to the serializer exclusively.
230
+ if relationship.cached
231
+ cachable_relationships_to_serialize[relationship.name] = relationship
232
+ else
233
+ uncachable_relationships_to_serialize[relationship.name] = relationship
234
+ end
235
+ relationships_to_serialize[relationship.name] = relationship
236
+ end
237
+
238
+ def has_many(relationship_name, options = {}, &block)
239
+ relationship = create_relationship(relationship_name, :has_many, options, block)
240
+ add_relationship(relationship)
241
+ end
242
+
243
+ def has_one(relationship_name, options = {}, &block)
244
+ relationship = create_relationship(relationship_name, :has_one, options, block)
245
+ add_relationship(relationship)
246
+ end
247
+
248
+ def belongs_to(relationship_name, options = {}, &block)
249
+ relationship = create_relationship(relationship_name, :belongs_to, options, block)
250
+ add_relationship(relationship)
251
+ end
252
+
253
+ def meta(meta_name = nil, &block)
254
+ self.meta_to_serialize = block || meta_name
255
+ end
256
+
257
+ def create_relationship(base_key, relationship_type, options, block)
258
+ name = base_key.to_sym
259
+ if relationship_type == :has_many
260
+ base_serialization_key = base_key.to_s.singularize
261
+ id_postfix = '_ids'
262
+ else
263
+ base_serialization_key = base_key
264
+ id_postfix = '_id'
265
+ end
266
+ polymorphic = fetch_polymorphic_option(options)
267
+
268
+ Relationship.new(
269
+ owner: self,
270
+ key: options[:key] || run_key_transform(base_key),
271
+ name: name,
272
+ id_method_name: compute_id_method_name(
273
+ options[:id_method_name],
274
+ "#{base_serialization_key}#{id_postfix}".to_sym,
275
+ polymorphic,
276
+ options[:serializer],
277
+ block
278
+ ),
279
+ record_type: options[:record_type],
280
+ object_method_name: options[:object_method_name] || name,
281
+ object_block: block,
282
+ serializer: options[:serializer],
283
+ relationship_type: relationship_type,
284
+ cached: options[:cached],
285
+ polymorphic: polymorphic,
286
+ conditional_proc: options[:if],
287
+ transform_method: @transform_method,
288
+ meta: options[:meta],
289
+ links: options[:links],
290
+ lazy_load_data: options[:lazy_load_data]
291
+ )
292
+ end
293
+
294
+ def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
295
+ if block.present? || serializer.is_a?(Proc) || polymorphic
296
+ custom_id_method_name || :id
297
+ else
298
+ custom_id_method_name || id_method_name_from_relationship
299
+ end
300
+ end
301
+
302
+ def serializer_for(name)
303
+ namespace = self.name.gsub(/()?\w+Serializer$/, '')
304
+ serializer_name = "#{name.to_s.demodulize.classify}Serializer"
305
+ serializer_class_name = namespace + serializer_name
306
+ begin
307
+ serializer_class_name.constantize
308
+ rescue NameError
309
+ raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \
310
+ "Attempted to find '#{serializer_class_name}'. " \
311
+ 'Consider specifying the serializer directly through options[:serializer].'
312
+ end
313
+ end
314
+
315
+ def fetch_polymorphic_option(options)
316
+ option = options[:polymorphic]
317
+ return false unless option.present?
318
+ return option if option.respond_to? :keys
319
+
320
+ {}
321
+ end
322
+
323
+ # def link(link_name, link_method_name = nil, &block)
324
+ def link(*params, &block)
325
+ self.data_links = {} if data_links.nil?
326
+
327
+ options = params.last.is_a?(Hash) ? params.pop : {}
328
+ link_name = params.first
329
+ link_method_name = params[-1]
330
+ key = run_key_transform(link_name)
331
+
332
+ data_links[key] = Link.new(
333
+ key: key,
334
+ method: block || link_method_name,
335
+ options: options
336
+ )
337
+ end
338
+
339
+ def validate_includes!(includes)
340
+ return if includes.blank?
341
+
342
+ parse_includes_list(includes).each_key do |include_item|
343
+ relationship_to_include = relationships_to_serialize[include_item]
344
+ raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_include
345
+
346
+ relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ class Railtie < Rails::Railtie
6
+ initializer 'fast_jsonapi.active_record' do
7
+ ActiveSupport.on_load :active_record do
8
+ require 'extensions/has_one'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,236 @@
1
+ module FastJsonapi
2
+ class Relationship
3
+ attr_reader :owner, :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :meta, :lazy_load_data
4
+
5
+ def initialize(
6
+ owner:,
7
+ key:,
8
+ name:,
9
+ id_method_name:,
10
+ record_type:,
11
+ object_method_name:,
12
+ object_block:,
13
+ serializer:,
14
+ relationship_type:,
15
+ polymorphic:,
16
+ conditional_proc:,
17
+ transform_method:,
18
+ links:,
19
+ meta:,
20
+ cached: false,
21
+ lazy_load_data: false
22
+ )
23
+ @owner = owner
24
+ @key = key
25
+ @name = name
26
+ @id_method_name = id_method_name
27
+ @record_type = record_type
28
+ @object_method_name = object_method_name
29
+ @object_block = object_block
30
+ @serializer = serializer
31
+ @relationship_type = relationship_type
32
+ @cached = cached
33
+ @polymorphic = polymorphic
34
+ @conditional_proc = conditional_proc
35
+ @transform_method = transform_method
36
+ @links = links || {}
37
+ @meta = meta || {}
38
+ @lazy_load_data = lazy_load_data
39
+ @record_types_for = {}
40
+ @serializers_for_name = {}
41
+ end
42
+
43
+ def serialize(record, included, serialization_params, output_hash)
44
+ if include_relationship?(record, serialization_params)
45
+ empty_case = relationship_type == :has_many ? [] : nil
46
+
47
+ output_hash[key] = {}
48
+ output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case unless lazy_load_data && !included
49
+
50
+ add_meta_hash(record, serialization_params, output_hash) if meta.present?
51
+ add_links_hash(record, serialization_params, output_hash) if links.present?
52
+ end
53
+ end
54
+
55
+ def fetch_associated_object(record, params)
56
+ return FastJsonapi.call_proc(object_block, record, params) unless object_block.nil?
57
+
58
+ record.send(object_method_name)
59
+ end
60
+
61
+ def include_relationship?(record, serialization_params)
62
+ if conditional_proc.present?
63
+ FastJsonapi.call_proc(conditional_proc, record, serialization_params)
64
+ else
65
+ true
66
+ end
67
+ end
68
+
69
+ def serializer_for(record, serialization_params)
70
+ # TODO: Remove this, dead code...
71
+ if @static_serializer
72
+ @static_serializer
73
+
74
+ elsif polymorphic
75
+ name = polymorphic[record.class] if polymorphic.is_a?(Hash)
76
+ name ||= record.class.name
77
+ serializer_for_name(name)
78
+
79
+ elsif serializer.is_a?(Proc)
80
+ FastJsonapi.call_proc(serializer, record, serialization_params)
81
+
82
+ elsif object_block
83
+ serializer_for_name(record.class.name)
84
+
85
+ else
86
+ # TODO: Remove this, dead code...
87
+ raise "Unknown serializer for object #{record.inspect}"
88
+ end
89
+ end
90
+
91
+ def static_serializer
92
+ initialize_static_serializer unless @initialized_static_serializer
93
+ @static_serializer
94
+ end
95
+
96
+ def static_record_type
97
+ initialize_static_serializer unless @initialized_static_serializer
98
+ @static_record_type
99
+ end
100
+
101
+ private
102
+
103
+ def ids_hash_from_record_and_relationship(record, params = {})
104
+ initialize_static_serializer unless @initialized_static_serializer
105
+
106
+ return ids_hash(fetch_id(record, params), @static_record_type) if @static_record_type
107
+
108
+ return unless associated_object = fetch_associated_object(record, params)
109
+
110
+ if associated_object.respond_to? :map
111
+ return associated_object.map do |object|
112
+ id_hash_from_record object, params
113
+ end
114
+ end
115
+
116
+ id_hash_from_record associated_object, params
117
+ end
118
+
119
+ def id_hash_from_record(record, params)
120
+ associated_record_type = record_type_for(record, params)
121
+ id_hash(record.public_send(id_method_name), associated_record_type)
122
+ end
123
+
124
+ def ids_hash(ids, record_type)
125
+ return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
126
+
127
+ id_hash(ids, record_type) # ids variable is just a single id here
128
+ end
129
+
130
+ def id_hash(id, record_type, default_return = false)
131
+ if id.present?
132
+ { id: id.to_s, type: record_type }
133
+ else
134
+ default_return ? { id: nil, type: record_type } : nil
135
+ end
136
+ end
137
+
138
+ def fetch_id(record, params)
139
+ if object_block.present?
140
+ object = FastJsonapi.call_proc(object_block, record, params)
141
+ return object.map { |item| item.public_send(id_method_name) } if object.respond_to? :map
142
+
143
+ return object.try(id_method_name)
144
+ end
145
+ record.public_send(id_method_name)
146
+ end
147
+
148
+ def add_links_hash(record, params, output_hash)
149
+ output_hash[key][:links] = if links.is_a?(Symbol)
150
+ record.public_send(links)
151
+ else
152
+ links.each_with_object({}) do |(key, method), hash|
153
+ Link.new(key: key, method: method).serialize(record, params, hash)
154
+ end
155
+ end
156
+ end
157
+
158
+ def add_meta_hash(record, params, output_hash)
159
+ output_hash[key][:meta] = if meta.is_a?(Proc)
160
+ FastJsonapi.call_proc(meta, record, params)
161
+ else
162
+ meta
163
+ end
164
+ end
165
+
166
+ def run_key_transform(input)
167
+ if transform_method.present?
168
+ input.to_s.send(*transform_method).to_sym
169
+ else
170
+ input.to_sym
171
+ end
172
+ end
173
+
174
+ def initialize_static_serializer
175
+ return if @initialized_static_serializer
176
+
177
+ @static_serializer = compute_static_serializer
178
+ @static_record_type = compute_static_record_type
179
+ @initialized_static_serializer = true
180
+ end
181
+
182
+ def compute_static_serializer
183
+ if polymorphic
184
+ # polymorphic without a specific serializer --
185
+ # the serializer is determined on a record-by-record basis
186
+ nil
187
+
188
+ elsif serializer.is_a?(Symbol) || serializer.is_a?(String)
189
+ # a serializer was explicitly specified by name -- determine the serializer class
190
+ serializer_for_name(serializer)
191
+
192
+ elsif serializer.is_a?(Proc)
193
+ # the serializer is a Proc to be executed per object -- not static
194
+ nil
195
+
196
+ elsif serializer
197
+ # something else was specified, e.g. a specific serializer class -- return it
198
+ serializer
199
+
200
+ elsif object_block
201
+ # an object block is specified without a specific serializer --
202
+ # assume the objects might be different and infer the serializer by their class
203
+ nil
204
+
205
+ else
206
+ # no serializer information was provided -- infer it from the relationship name
207
+ serializer_name = name.to_s
208
+ serializer_name = serializer_name.singularize if relationship_type.to_sym == :has_many
209
+ serializer_for_name(serializer_name)
210
+ end
211
+ end
212
+
213
+ def serializer_for_name(name)
214
+ @serializers_for_name[name] ||= owner.serializer_for(name)
215
+ end
216
+
217
+ def record_type_for(record, serialization_params)
218
+ # if the record type is static, return it
219
+ return @static_record_type if @static_record_type
220
+
221
+ # if not, use the record type of the serializer, and memoize the transformed version
222
+ serializer = serializer_for(record, serialization_params)
223
+ @record_types_for[serializer] ||= run_key_transform(serializer.record_type)
224
+ end
225
+
226
+ def compute_static_record_type
227
+ if polymorphic
228
+ nil
229
+ elsif record_type
230
+ run_key_transform(record_type)
231
+ elsif @static_serializer
232
+ run_key_transform(@static_serializer.record_type)
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,29 @@
1
+ module FastJsonapi
2
+ class Scalar
3
+ attr_reader :key, :method, :conditional_proc
4
+
5
+ def initialize(key:, method:, options: {})
6
+ @key = key
7
+ @method = method
8
+ @conditional_proc = options[:if]
9
+ end
10
+
11
+ def serialize(record, serialization_params, output_hash)
12
+ if conditionally_allowed?(record, serialization_params)
13
+ if method.is_a?(Proc)
14
+ output_hash[key] = FastJsonapi.call_proc(method, record, serialization_params)
15
+ else
16
+ output_hash[key] = record.public_send(method)
17
+ end
18
+ end
19
+ end
20
+
21
+ def conditionally_allowed?(record, serialization_params)
22
+ if conditional_proc.present?
23
+ FastJsonapi.call_proc(conditional_proc, record, serialization_params)
24
+ else
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end