fast_serializer 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/HISTORY.txt +3 -0
- data/MIT_LICENSE +20 -0
- data/README.md +148 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/fast_serializer.gemspec +23 -0
- data/lib/fast_serializer.rb +37 -0
- data/lib/fast_serializer/array_serializer.rb +92 -0
- data/lib/fast_serializer/cache.rb +22 -0
- data/lib/fast_serializer/cache/active_support_cache.rb +21 -0
- data/lib/fast_serializer/serialization_context.rb +70 -0
- data/lib/fast_serializer/serialized_field.rb +78 -0
- data/lib/fast_serializer/serializer.rb +356 -0
- data/spec/array_serializer_spec.rb +59 -0
- data/spec/fast_serializer_spec.rb +28 -0
- data/spec/serialization_context_spec.rb +32 -0
- data/spec/serialized_field_spec.rb +71 -0
- data/spec/serializer_spec.rb +146 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/test_models.rb +64 -0
- metadata +117 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# Data structure used internally for maintaining a field to be serialized.
|
3
|
+
class SerializedField
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name, optional: false, serializer: nil, serializer_options: nil, enumerable: false)
|
7
|
+
@name = name
|
8
|
+
@optional = !!optional
|
9
|
+
if serializer
|
10
|
+
@serializer = serializer
|
11
|
+
@serializer_options = serializer_options
|
12
|
+
@enumerable = enumerable
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def optional?
|
17
|
+
@optional
|
18
|
+
end
|
19
|
+
|
20
|
+
# Wrap a value in the serializer if one has been set. Otherwise just returns the raw value.
|
21
|
+
def serialize(value)
|
22
|
+
if value && @serializer
|
23
|
+
serializer = nil
|
24
|
+
if @enumerable
|
25
|
+
serializer = ArraySerializer.new(value, :serializer => @serializer, :serializer_options => @serializer_options)
|
26
|
+
else
|
27
|
+
serializer = @serializer.new(value, @serializer_options)
|
28
|
+
end
|
29
|
+
context = SerializationContext.current
|
30
|
+
if context
|
31
|
+
context.with_reference(value){ serializer.as_json }
|
32
|
+
else
|
33
|
+
serializer.as_json
|
34
|
+
end
|
35
|
+
else
|
36
|
+
serialize_value(value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Convert the value to primitive data types: string, number, boolean, symbol, time, date, array, hash.
|
43
|
+
def serialize_value(value)
|
44
|
+
if value.is_a?(String) || value.is_a?(Numeric) || value == nil || value == true || value == false || value.is_a?(Time) || value.is_a?(Date) || value.is_a?(Symbol)
|
45
|
+
value
|
46
|
+
elsif value.is_a?(Hash)
|
47
|
+
hash = nil
|
48
|
+
value.each do |k, v|
|
49
|
+
val = serialize_value(v)
|
50
|
+
if val.object_id != v.object_id
|
51
|
+
hash = value.dup unless hash
|
52
|
+
hash[k] = val
|
53
|
+
end
|
54
|
+
end
|
55
|
+
hash || value
|
56
|
+
elsif value.is_a?(Enumerable)
|
57
|
+
array = nil
|
58
|
+
value.each_with_index do |v, i|
|
59
|
+
val = serialize_value(v)
|
60
|
+
if val.object_id != v.object_id
|
61
|
+
array = value.dup unless array
|
62
|
+
array[i] = val
|
63
|
+
end
|
64
|
+
end
|
65
|
+
array || value
|
66
|
+
elsif value.respond_to?(:as_json)
|
67
|
+
value.as_json
|
68
|
+
elsif value.respond_to?(:to_hash)
|
69
|
+
value.to_hash
|
70
|
+
elsif value.respond_to?(:to_h)
|
71
|
+
value.to_h
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,356 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# Models can include this module to define themselves as serializers. A serializer is used to wrap
|
3
|
+
# an object and output a hash version of that object suitable for serialization to JSON or other formats.
|
4
|
+
#
|
5
|
+
# To define what fields to serialize on the wrapped object, the serializer class must call the +serialize+
|
6
|
+
# class method:
|
7
|
+
#
|
8
|
+
# class PersonSerializer
|
9
|
+
# include FastSerializer::Serializer
|
10
|
+
# serialize :id, :name
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# This sample serializer will output an object as a hash with keys {:id, :name}. The values for each field
|
14
|
+
# is gotten by calling the corresponding method on the serializer object. By default, each serialized field
|
15
|
+
# will automatically define a method that simply delegates to the wrapped object. So if you need provide special
|
16
|
+
# handling for a field or serialize a virtual field that doesn't exist on the parent object, you just need to
|
17
|
+
# implement the method on the serializer.
|
18
|
+
#
|
19
|
+
# class PersonSerializer
|
20
|
+
# include FastSerializer::Serializer
|
21
|
+
# serialize :id, :name
|
22
|
+
#
|
23
|
+
# def name
|
24
|
+
# "#{object.first_name} #{object.last_name}"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Serializers can implement their own options for controlling details about how to serialize the object.
|
29
|
+
#
|
30
|
+
# class PersonSerializer
|
31
|
+
# include FastSerializer::Serializer
|
32
|
+
# serialize :id, :name
|
33
|
+
#
|
34
|
+
# def name
|
35
|
+
# if option(:last_first)
|
36
|
+
# "#{object.last_name}, #{object.first_name}"
|
37
|
+
# else
|
38
|
+
# "#{object.first_name} #{object.last_name}"
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# serializer = PersonSerializer.new(person, :last_first => true)
|
44
|
+
#
|
45
|
+
# All serializers will honor options for :include (include optional fields), :exclude (exclude fields).
|
46
|
+
#
|
47
|
+
# Serializer can also be specified as cacheable. Cacheable serializer will store and fetch the serialized value
|
48
|
+
# from a cache. In order to user caching you must set the cache implementation. This can either be done on a
|
49
|
+
# global level (FastSerializer.cache) class level, or instance level (:cache option). Then you can specify
|
50
|
+
# serializers to be cacheable. This can be done on a class level with the +cacheable+ directive or on an
|
51
|
+
# instance level with the :cacheable option. A time to live can also be set at the same levels using +cache_ttl+.
|
52
|
+
#
|
53
|
+
# Serializers are designed to be reusable and must never have any internal state associated with them. Calling
|
54
|
+
# +as_json+ on a serializer multiple times must always return the same value.
|
55
|
+
#
|
56
|
+
# Serializing a nil object will result in nil rather than an empty hash.
|
57
|
+
module Serializer
|
58
|
+
|
59
|
+
def self.included(base)
|
60
|
+
base.extend(ClassMethods)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the wrapped object that is being serialized.
|
64
|
+
attr_reader :object
|
65
|
+
|
66
|
+
# Return the options hash (if any) that specified optional details about how to serialize the object.
|
67
|
+
attr_reader :options
|
68
|
+
|
69
|
+
module ClassMethods
|
70
|
+
# Define one or more fields to include in the serialized object. Field values will be gotten
|
71
|
+
# by calling the method of the same name on class including this module.
|
72
|
+
#
|
73
|
+
# Several options can be specified to control how the field is serialized.
|
74
|
+
#
|
75
|
+
# * as: Name to call the field in the serialized hash. Defaults to the same as the field name
|
76
|
+
# (withany ? stripped off the end for boolean fields). This option can only be specified
|
77
|
+
# for a single field.
|
78
|
+
#
|
79
|
+
# * optional: Boolean flag indicating if the field is optional in the serialized value (defaults to false).
|
80
|
+
# Optional fields are only included if the :include option to the +as_json+ method includes the field name.
|
81
|
+
#
|
82
|
+
# * delegate: Boolean flag indicating if the field call should be delegated to the wrapped object (defaults to true).
|
83
|
+
# When this is supplied, a method will be automatically defined on the serializer with the name of the field
|
84
|
+
# that simply then calls the same method on the wrapped object.
|
85
|
+
#
|
86
|
+
# * serializer: Class that should be used to serialize the field. If this option is specified, the field value will
|
87
|
+
# be serialized using the specified serializer class which should include this module. Otherwise, the +as_json+
|
88
|
+
# method will be called on the field class.
|
89
|
+
#
|
90
|
+
# * serializer_options: Options that should be used for serializing the field for when the :serializer option
|
91
|
+
# has been specified.
|
92
|
+
#
|
93
|
+
# * enumerable: Boolean flag indicating if the field is enumerable (defaults to false). This option is only
|
94
|
+
# used if the :serializer option has been set. If the field is marked as enumerable, then the value will be
|
95
|
+
# serialized as an array with each element wrapped in the specified serializer.
|
96
|
+
#
|
97
|
+
# Subclasses will inherit all of their parent classes serialized fields. Subclasses can override fields
|
98
|
+
# defined on the parent class by simply defining them again.
|
99
|
+
def serialize(*fields, as: nil, optional: false, delegate: true, serializer: nil, serializer_options: nil, enumerable: false)
|
100
|
+
if as && fields.size > 1
|
101
|
+
raise ArgumentError.new("Cannot specify :as argument with multiple fields to serialize")
|
102
|
+
end
|
103
|
+
|
104
|
+
fields.each do |field|
|
105
|
+
name = as
|
106
|
+
if name.nil? && field.to_s.end_with?("?".freeze)
|
107
|
+
name = field.to_s.chomp("?".freeze)
|
108
|
+
end
|
109
|
+
|
110
|
+
field = field.to_sym
|
111
|
+
attribute = (name || field).to_sym
|
112
|
+
add_field(attribute, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable)
|
113
|
+
|
114
|
+
if delegate && !method_defined?(attribute)
|
115
|
+
define_delegate(attribute, field)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Remove a field from being serialized. This can be useful in subclasses if they need to remove a
|
121
|
+
# field defined by the parent class.
|
122
|
+
def remove(*fields)
|
123
|
+
remove_fields = fields.collect(&:to_sym)
|
124
|
+
field_list = []
|
125
|
+
serializable_fields.each do |existing_field|
|
126
|
+
field_list << existing_field unless remove_fields.include?(existing_field.name)
|
127
|
+
end
|
128
|
+
@serializable_fields = field_list.freeze
|
129
|
+
end
|
130
|
+
|
131
|
+
# Specify the cacheability of the serializer.
|
132
|
+
#
|
133
|
+
# You can specify the cacheable state (defaults to true) of the class. Subclasses will inherit the
|
134
|
+
# cacheable state of their parent class, so if you have non-cacheable serializer subclassing a
|
135
|
+
# cacheable parent class, you can call <tt>cacheable false</tt> to override the parent behavior.
|
136
|
+
#
|
137
|
+
# You can also specify the cache time to live (ttl) in seconds and the cache implementation to use.
|
138
|
+
# Both of these values are inherited on subclasses.
|
139
|
+
def cacheable(cacheable = true, ttl: nil, cache: nil)
|
140
|
+
@cacheable = cacheable
|
141
|
+
self.cache_ttl = ttl if ttl
|
142
|
+
self.cache = cache if cache
|
143
|
+
end
|
144
|
+
|
145
|
+
# Return true if the serializer class is cacheable.
|
146
|
+
def cacheable?
|
147
|
+
unless defined?(@cacheable)
|
148
|
+
@cacheable = superclass.cacheable? if superclass.respond_to?(:cacheable?)
|
149
|
+
end
|
150
|
+
!!@cacheable
|
151
|
+
end
|
152
|
+
|
153
|
+
# Return the time to live in seconds for a cacheable serializer.
|
154
|
+
def cache_ttl
|
155
|
+
if defined?(@cache_ttl)
|
156
|
+
@cache_ttl
|
157
|
+
elsif superclass.respond_to?(:cache_ttl)
|
158
|
+
superclass.cache_ttl
|
159
|
+
else
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Set the time to live on a cacheable serializer.
|
165
|
+
def cache_ttl=(value)
|
166
|
+
@cache_ttl = value
|
167
|
+
end
|
168
|
+
|
169
|
+
# Get the cache implemtation used to store cacheable serializers.
|
170
|
+
def cache
|
171
|
+
if defined?(@cache)
|
172
|
+
@cache
|
173
|
+
elsif superclass.respond_to?(:cache)
|
174
|
+
superclass.cache
|
175
|
+
else
|
176
|
+
FastSerializer.cache
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Set the cache implementation used to store cacheable serializers.
|
181
|
+
def cache=(cache)
|
182
|
+
@cache = cache
|
183
|
+
end
|
184
|
+
|
185
|
+
# :nodoc:
|
186
|
+
def new(object, options = nil)
|
187
|
+
context = SerializationContext.current
|
188
|
+
if context
|
189
|
+
# If there's a context in scope this will load duplicate entries from the context rather than creating new instances.
|
190
|
+
context.load(self, object, options)
|
191
|
+
else
|
192
|
+
super
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Return a list of the SerializedFields defined for the class.
|
197
|
+
def serializable_fields
|
198
|
+
unless defined?(@serializable_fields) && @serializable_fields
|
199
|
+
fields = superclass.send(:serializable_fields).dup if superclass.respond_to?(:serializable_fields)
|
200
|
+
fields ||= []
|
201
|
+
@serializable_fields = fields.freeze
|
202
|
+
end
|
203
|
+
@serializable_fields
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# Add a field to be serialized.
|
209
|
+
def add_field(name, optional:, serializer:, serializer_options:, enumerable:)
|
210
|
+
name = name.to_sym
|
211
|
+
field = SerializedField.new(name, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable)
|
212
|
+
|
213
|
+
# Add the field to the frozen list of fields.
|
214
|
+
field_list = []
|
215
|
+
added = false
|
216
|
+
serializable_fields.each do |existing_field|
|
217
|
+
if existing_field.name == name
|
218
|
+
field_list << field
|
219
|
+
else
|
220
|
+
field_list << existing_field
|
221
|
+
end
|
222
|
+
end
|
223
|
+
field_list << field unless added
|
224
|
+
@serializable_fields = field_list.freeze
|
225
|
+
end
|
226
|
+
|
227
|
+
# Define a delegate method name +attribute+ that invokes the +field+ method on the wrapped object.
|
228
|
+
def define_delegate(attribute, field)
|
229
|
+
define_method(attribute){ object.send(field) }
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Create a new serializer for the specified object.
|
234
|
+
#
|
235
|
+
# Options can be passed in to control how the object is serialized. Options supported by all Serializers:
|
236
|
+
#
|
237
|
+
# * :include - Field or array of optional field names that should be included in the serialized object.
|
238
|
+
# * :exclude - Field or array of field names that should be excluded from the serialized object.
|
239
|
+
# * :cacheable - Override the cacheable behavior set on the class.
|
240
|
+
# * :cache_ttl - Override the cache ttl set on the class.
|
241
|
+
# * :cache - Override the cache implementation set on the class.
|
242
|
+
def initialize(object, options = nil)
|
243
|
+
@object = object
|
244
|
+
@options = options
|
245
|
+
@_serialized = nil
|
246
|
+
end
|
247
|
+
|
248
|
+
# Serialize the wrapped object into a format suitable for passing to a JSON parser.
|
249
|
+
def as_json(*args)
|
250
|
+
return nil unless object
|
251
|
+
unless @_serialized
|
252
|
+
@_serialized = (cacheable? ? load_from_cache : load_hash).freeze
|
253
|
+
end
|
254
|
+
@_serialized
|
255
|
+
end
|
256
|
+
|
257
|
+
alias :to_hash :as_json
|
258
|
+
alias :to_h :as_json
|
259
|
+
|
260
|
+
# Convert the wrapped object to JSON format.
|
261
|
+
def to_json(options = nil)
|
262
|
+
if defined?(MultiJson)
|
263
|
+
MultiJson.dump(as_json)
|
264
|
+
else
|
265
|
+
JSON.dump(as_json)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Fetch the specified option from the options hash.
|
270
|
+
def option(name)
|
271
|
+
@options[name] if @options
|
272
|
+
end
|
273
|
+
|
274
|
+
# Return true if this serializer is cacheable.
|
275
|
+
def cacheable?
|
276
|
+
option(:cacheable) || self.class.cacheable?
|
277
|
+
end
|
278
|
+
|
279
|
+
# Return the cache implementation where this serializer can be stored.
|
280
|
+
def cache
|
281
|
+
option(:cache) || self.class.cache
|
282
|
+
end
|
283
|
+
|
284
|
+
# Return the time to live in seconds this serializer can be cached for.
|
285
|
+
def cache_ttl
|
286
|
+
option(:cache_ttl) || self.class.cache_ttl
|
287
|
+
end
|
288
|
+
|
289
|
+
# Returns a array of the elements that make this serializer unique. The
|
290
|
+
# key is an array made up of the serializer class name, wrapped object, and
|
291
|
+
# serialization options hash.
|
292
|
+
def cache_key
|
293
|
+
[self.class.name, object, options]
|
294
|
+
end
|
295
|
+
|
296
|
+
# :nodoc:
|
297
|
+
def ==(other)
|
298
|
+
other.instance_of?(self.class) && @object == other.object && @options == other.options
|
299
|
+
end
|
300
|
+
alias_method :eql?, :==
|
301
|
+
|
302
|
+
protected
|
303
|
+
|
304
|
+
# Load the hash that will represent the wrapped object as a serialized object.
|
305
|
+
def load_hash
|
306
|
+
hash = {}
|
307
|
+
include_fields = included_optional_fields
|
308
|
+
excluded_fields = excluded_regular_fields
|
309
|
+
SerializationContext.use do
|
310
|
+
self.class.serializable_fields.each do |field|
|
311
|
+
name = field.name
|
312
|
+
if field.optional?
|
313
|
+
next unless include_fields && include_fields.include?(name)
|
314
|
+
end
|
315
|
+
next if excluded_fields && excluded_fields.include?(name)
|
316
|
+
value = field.serialize(send(name))
|
317
|
+
hash[name] = value
|
318
|
+
end
|
319
|
+
end
|
320
|
+
hash
|
321
|
+
end
|
322
|
+
|
323
|
+
# Load the hash that will represent the wrapped object as a serialized object from a cache.
|
324
|
+
def load_from_cache
|
325
|
+
if cache
|
326
|
+
cache.fetch(self, cache_ttl) do
|
327
|
+
load_hash
|
328
|
+
end
|
329
|
+
else
|
330
|
+
load_hash
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
# Return a list of optional fields to be included in the output from the :include option.
|
337
|
+
def included_optional_fields
|
338
|
+
included_fields = option(:include)
|
339
|
+
if included_fields
|
340
|
+
Array(included_fields).collect(&:to_sym)
|
341
|
+
else
|
342
|
+
nil
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Return a list of fields to be excluded from the output from the :exclude option.
|
347
|
+
def excluded_regular_fields
|
348
|
+
excluded_fields = option(:exclude)
|
349
|
+
if excluded_fields
|
350
|
+
Array(excluded_fields).collect(&:to_sym)
|
351
|
+
else
|
352
|
+
nil
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FastSerializer::ArraySerializer do
|
4
|
+
|
5
|
+
it "should serialize an array of regular objects" do
|
6
|
+
array = [1, 2, 3]
|
7
|
+
serializer = FastSerializer::ArraySerializer.new(array)
|
8
|
+
expect(serializer.as_json).to eq array
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should serialize any Enumerable" do
|
12
|
+
hash = {:a => 1, :b => 2}
|
13
|
+
serializer = FastSerializer::ArraySerializer.new(hash)
|
14
|
+
expect(serializer.as_json).to eq hash.to_a
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should serializer an array of objects using a specific serializer" do
|
18
|
+
model_1 = SimpleModel.new(:id => 1, :name => "foo")
|
19
|
+
model_2 = SimpleModel.new(:id => 2, :name => "bar")
|
20
|
+
serializer = FastSerializer::ArraySerializer.new([model_1, model_2], :serializer => SimpleSerializer)
|
21
|
+
expect(JSON.load(serializer.to_json)).to eq [
|
22
|
+
{"id" => 1, "name" => "foo", "validated" => false},
|
23
|
+
{"id" => 2, "name" => "bar", "validated" => false}
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should serializer an array of objects using a specific serializer with options" do
|
28
|
+
model_1 = SimpleModel.new(:id => 1, :name => "foo")
|
29
|
+
model_2 = SimpleModel.new(:id => 2, :name => "bar")
|
30
|
+
serializer = FastSerializer::ArraySerializer.new([model_1, model_2], :serializer => SimpleSerializer, :serializer_options => {:include => :description})
|
31
|
+
expect(JSON.load(serializer.to_json)).to eq [
|
32
|
+
{"id" => 1, "name" => "foo", "validated" => false, "description" => nil},
|
33
|
+
{"id" => 2, "name" => "bar", "validated" => false, "description" => nil}
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should not respond to_hash methods" do
|
38
|
+
array = [1, 2, 3]
|
39
|
+
serializer = FastSerializer::ArraySerializer.new(array)
|
40
|
+
expect(serializer.respond_to?(:to_hash)).to eq false
|
41
|
+
expect(serializer.respond_to?(:to_h)).to eq false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should respond to to_a" do
|
45
|
+
array = [1, 2, 3]
|
46
|
+
serializer = FastSerializer::ArraySerializer.new(array)
|
47
|
+
expect(serializer.to_a).to eq array
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should pull cacheable serializers from a cache" do
|
51
|
+
model_1 = SimpleModel.new(:id => 1, :name => "foo")
|
52
|
+
model_2 = SimpleModel.new(:id => 2, :name => "bar")
|
53
|
+
serializer = FastSerializer::ArraySerializer.new([model_1, model_2], serializer: CachedSerializer)
|
54
|
+
expect(serializer.cacheable?).to eq true
|
55
|
+
already_cached_json = CachedSerializer.new(model_1).as_json
|
56
|
+
expect(serializer.as_json.collect(&:object_id)).to eq [already_cached_json.object_id, CachedSerializer.new(model_2).as_json.object_id]
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|