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.
@@ -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,224 @@
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, :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
+ cached: false,
16
+ polymorphic:,
17
+ conditional_proc:,
18
+ transform_method:,
19
+ links:,
20
+ lazy_load_data: false
21
+ )
22
+ @owner = owner
23
+ @key = key
24
+ @name = name
25
+ @id_method_name = id_method_name
26
+ @record_type = record_type
27
+ @object_method_name = object_method_name
28
+ @object_block = object_block
29
+ @serializer = serializer
30
+ @relationship_type = relationship_type
31
+ @cached = cached
32
+ @polymorphic = polymorphic
33
+ @conditional_proc = conditional_proc
34
+ @transform_method = transform_method
35
+ @links = links || {}
36
+ @lazy_load_data = lazy_load_data
37
+ @record_types_for = {}
38
+ @serializers_for_name = {}
39
+ end
40
+
41
+ def serialize(record, included, serialization_params, output_hash)
42
+ if include_relationship?(record, serialization_params)
43
+ empty_case = relationship_type == :has_many ? [] : nil
44
+
45
+ output_hash[key] = {}
46
+ output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case unless lazy_load_data && !included
47
+ add_links_hash(record, serialization_params, output_hash) if links.present?
48
+ end
49
+ end
50
+
51
+ def fetch_associated_object(record, params)
52
+ return FastJsonapi.call_proc(object_block, record, params) unless object_block.nil?
53
+
54
+ record.send(object_method_name)
55
+ end
56
+
57
+ def include_relationship?(record, serialization_params)
58
+ if conditional_proc.present?
59
+ FastJsonapi.call_proc(conditional_proc, record, serialization_params)
60
+ else
61
+ true
62
+ end
63
+ end
64
+
65
+ def serializer_for(record, serialization_params)
66
+ # TODO: Remove this, dead code...
67
+ if @static_serializer
68
+ @static_serializer
69
+
70
+ elsif polymorphic
71
+ name = polymorphic[record.class] if polymorphic.is_a?(Hash)
72
+ name ||= record.class.name
73
+ serializer_for_name(name)
74
+
75
+ elsif serializer.is_a?(Proc)
76
+ FastJsonapi.call_proc(serializer, record, serialization_params)
77
+
78
+ elsif object_block
79
+ serializer_for_name(record.class.name)
80
+
81
+ else
82
+ # TODO: Remove this, dead code...
83
+ raise "Unknown serializer for object #{record.inspect}"
84
+ end
85
+ end
86
+
87
+ def static_serializer
88
+ initialize_static_serializer unless @initialized_static_serializer
89
+ @static_serializer
90
+ end
91
+
92
+ def static_record_type
93
+ initialize_static_serializer unless @initialized_static_serializer
94
+ @static_record_type
95
+ end
96
+
97
+ private
98
+
99
+ def ids_hash_from_record_and_relationship(record, params = {})
100
+ initialize_static_serializer unless @initialized_static_serializer
101
+
102
+ return ids_hash(fetch_id(record, params), @static_record_type) if @static_record_type
103
+
104
+ return unless associated_object = fetch_associated_object(record, params)
105
+
106
+ if associated_object.respond_to? :map
107
+ return associated_object.map do |object|
108
+ id_hash_from_record object, params
109
+ end
110
+ end
111
+
112
+ id_hash_from_record associated_object, params
113
+ end
114
+
115
+ def id_hash_from_record(record, params)
116
+ associated_record_type = record_type_for(record, params)
117
+ id_hash(record.public_send(id_method_name), associated_record_type)
118
+ end
119
+
120
+ def ids_hash(ids, record_type)
121
+ return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
122
+
123
+ id_hash(ids, record_type) # ids variable is just a single id here
124
+ end
125
+
126
+ def id_hash(id, record_type, default_return = false)
127
+ if id.present?
128
+ { id: id.to_s, type: record_type }
129
+ else
130
+ default_return ? { id: nil, type: record_type } : nil
131
+ end
132
+ end
133
+
134
+ def fetch_id(record, params)
135
+ if object_block.present?
136
+ object = FastJsonapi.call_proc(object_block, record, params)
137
+ return object.map { |item| item.public_send(id_method_name) } if object.respond_to? :map
138
+
139
+ return object.try(id_method_name)
140
+ end
141
+ record.public_send(id_method_name)
142
+ end
143
+
144
+ def add_links_hash(record, params, output_hash)
145
+ output_hash[key][:links] = if links.is_a?(Symbol)
146
+ record.public_send(links)
147
+ else
148
+ links.each_with_object({}) do |(key, method), hash|
149
+ Link.new(key: key, method: method).serialize(record, params, hash)\
150
+ end
151
+ end
152
+ end
153
+
154
+ def run_key_transform(input)
155
+ if transform_method.present?
156
+ input.to_s.send(*transform_method).to_sym
157
+ else
158
+ input.to_sym
159
+ end
160
+ end
161
+
162
+ def initialize_static_serializer
163
+ return if @initialized_static_serializer
164
+
165
+ @static_serializer = compute_static_serializer
166
+ @static_record_type = compute_static_record_type
167
+ @initialized_static_serializer = true
168
+ end
169
+
170
+ def compute_static_serializer
171
+ if polymorphic
172
+ # polymorphic without a specific serializer --
173
+ # the serializer is determined on a record-by-record basis
174
+ nil
175
+
176
+ elsif serializer.is_a?(Symbol) || serializer.is_a?(String)
177
+ # a serializer was explicitly specified by name -- determine the serializer class
178
+ serializer_for_name(serializer)
179
+
180
+ elsif serializer.is_a?(Proc)
181
+ # the serializer is a Proc to be executed per object -- not static
182
+ nil
183
+
184
+ elsif serializer
185
+ # something else was specified, e.g. a specific serializer class -- return it
186
+ serializer
187
+
188
+ elsif object_block
189
+ # an object block is specified without a specific serializer --
190
+ # assume the objects might be different and infer the serializer by their class
191
+ nil
192
+
193
+ else
194
+ # no serializer information was provided -- infer it from the relationship name
195
+ serializer_name = name.to_s
196
+ serializer_name = serializer_name.singularize if relationship_type.to_sym == :has_many
197
+ serializer_for_name(serializer_name)
198
+ end
199
+ end
200
+
201
+ def serializer_for_name(name)
202
+ @serializers_for_name[name] ||= owner.serializer_for(name)
203
+ end
204
+
205
+ def record_type_for(record, serialization_params)
206
+ # if the record type is static, return it
207
+ return @static_record_type if @static_record_type
208
+
209
+ # if not, use the record type of the serializer, and memoize the transformed version
210
+ serializer = serializer_for(record, serialization_params)
211
+ @record_types_for[serializer] ||= run_key_transform(serializer.record_type)
212
+ end
213
+
214
+ def compute_static_record_type
215
+ if polymorphic
216
+ nil
217
+ elsif record_type
218
+ run_key_transform(record_type)
219
+ elsif @static_serializer
220
+ run_key_transform(@static_serializer.record_type)
221
+ end
222
+ end
223
+ end
224
+ 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
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module FastJsonapi
6
+ MandatoryField = Class.new(StandardError)
7
+
8
+ module SerializationCore
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class << self
13
+ attr_accessor :attributes_to_serialize,
14
+ :relationships_to_serialize,
15
+ :cachable_relationships_to_serialize,
16
+ :uncachable_relationships_to_serialize,
17
+ :transform_method,
18
+ :record_type,
19
+ :record_id,
20
+ :cache_store_instance,
21
+ :cache_store_options,
22
+ :data_links,
23
+ :meta_to_serialize
24
+ end
25
+ end
26
+
27
+ class_methods do
28
+ def id_hash(id, record_type, default_return = false)
29
+ if id.present?
30
+ { id: id.to_s, type: record_type }
31
+ else
32
+ default_return ? { id: nil, type: record_type } : nil
33
+ end
34
+ end
35
+
36
+ def links_hash(record, params = {})
37
+ data_links.each_with_object({}) do |(_k, link), hash|
38
+ link.serialize(record, params, hash)
39
+ end
40
+ end
41
+
42
+ def attributes_hash(record, fieldset = nil, params = {})
43
+ attributes = attributes_to_serialize
44
+ attributes = attributes.slice(*fieldset) if fieldset.present?
45
+ attributes = {} if fieldset == []
46
+
47
+ attributes.each_with_object({}) do |(_k, attribute), hash|
48
+ attribute.serialize(record, params, hash)
49
+ end
50
+ end
51
+
52
+ def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
53
+ relationships = relationships_to_serialize if relationships.nil?
54
+ relationships = relationships.slice(*fieldset) if fieldset.present?
55
+ relationships = {} if fieldset == []
56
+
57
+ relationships.each_with_object({}) do |(key, relationship), hash|
58
+ included = includes_list.present? && includes_list.include?(key)
59
+ relationship.serialize(record, included, params, hash)
60
+ end
61
+ end
62
+
63
+ def meta_hash(record, params = {})
64
+ FastJsonapi.call_proc(meta_to_serialize, record, params)
65
+ end
66
+
67
+ def record_hash(record, fieldset, includes_list, params = {})
68
+ if cache_store_instance
69
+ record_hash = cache_store_instance.fetch(record, **cache_store_options) do
70
+ temp_hash = id_hash(id_from_record(record, params), record_type, true)
71
+ temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
72
+ temp_hash[:relationships] = {}
73
+ temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
74
+ temp_hash[:links] = links_hash(record, params) if data_links.present?
75
+ temp_hash
76
+ end
77
+ record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
78
+ else
79
+ record_hash = id_hash(id_from_record(record, params), record_type, true)
80
+ record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
81
+ record_hash[:relationships] = relationships_hash(record, nil, fieldset, includes_list, params) if relationships_to_serialize.present?
82
+ record_hash[:links] = links_hash(record, params) if data_links.present?
83
+ end
84
+
85
+ record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
86
+ record_hash
87
+ end
88
+
89
+ def id_from_record(record, params)
90
+ return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc)
91
+ return record.send(record_id) if record_id
92
+ raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
93
+
94
+ record.id
95
+ end
96
+
97
+ def parse_include_item(include_item)
98
+ return [include_item.to_sym] unless include_item.to_s.include?('.')
99
+
100
+ include_item.to_s.split('.').map!(&:to_sym)
101
+ end
102
+
103
+ def remaining_items(items)
104
+ return unless items.size > 1
105
+
106
+ [items[1..-1].join('.').to_sym]
107
+ end
108
+
109
+ # includes handler
110
+ def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
111
+ return unless includes_list.present?
112
+
113
+ includes_list.sort.each_with_object([]) do |include_item, included_records|
114
+ items = parse_include_item(include_item)
115
+ remaining_items = remaining_items(items)
116
+
117
+ items.each do |item|
118
+ next unless relationships_to_serialize && relationships_to_serialize[item]
119
+
120
+ relationship_item = relationships_to_serialize[item]
121
+ next unless relationship_item.include_relationship?(record, params)
122
+
123
+ relationship_type = relationship_item.relationship_type
124
+
125
+ included_objects = relationship_item.fetch_associated_object(record, params)
126
+ next if included_objects.blank?
127
+
128
+ included_objects = [included_objects] unless relationship_type == :has_many
129
+
130
+ static_serializer = relationship_item.static_serializer
131
+ static_record_type = relationship_item.static_record_type
132
+
133
+ included_objects.each do |inc_obj|
134
+ serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
135
+ record_type = static_record_type || serializer.record_type
136
+
137
+ if remaining_items.present?
138
+ serializer_records = serializer.get_included_records(inc_obj, remaining_items, known_included_objects, fieldsets, params)
139
+ included_records.concat(serializer_records) unless serializer_records.empty?
140
+ end
141
+
142
+ code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
143
+ next if known_included_objects.key?(code)
144
+
145
+ known_included_objects[code] = inc_obj
146
+
147
+ included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,3 @@
1
+ module FastJsonapi
2
+ VERSION = JSONAPI::Serializer::VERSION
3
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a serializer for the given model.
3
+
4
+ Example:
5
+ rails generate serializer Movie
6
+
7
+ This will create:
8
+ app/serializers/movie_serializer.rb
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ class SerializerGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ argument :attributes, type: :array, default: [], banner: 'field field'
9
+
10
+ def create_serializer_file
11
+ template 'serializer.rb.tt', File.join('app', 'serializers', class_path, "#{file_name}_serializer.rb")
12
+ end
13
+
14
+ private
15
+
16
+ def attributes_names
17
+ attributes.map { |a| a.name.to_sym.inspect }
18
+ end
19
+ end