fast_serializer 1.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,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