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,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,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
|