fast_serializer 1.0.0

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