jsonapi-serializer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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