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