fast_serializer 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9aa9a0f0945690089e7b2811fc8a7a2bd7d5d391a44d6ffd5da46fde8cde178a
4
- data.tar.gz: 5c7767c57d03ec466b8b0b8a241ecaaf1190b04115db5bbe63a04884e302320e
3
+ metadata.gz: 01d2bc87adca177550f7f64de96b9b0429686f2c1dde32551e651a3cea327a78
4
+ data.tar.gz: 5cef7d23fd40ec13f732cf6c5bb0426ccd70179cbce967c426bd6e5714e1e690
5
5
  SHA512:
6
- metadata.gz: 6b385a08ca0c432e8f8660e19f95ad0f33b43372084dcd9910a10410f3f7cd168158e1138bfae101a76a216e210b50dfdea7a8207864182ab96ef65f099cef53
7
- data.tar.gz: 3b39610d1a17bece32360aac268b5fd508e0fe46bb2fd2b29fa22f6e422e93fa2c8ae7b11bdd926f848b30e8c40f68d508e6ffe57c04eeea8cbbeb77e06fcceb
6
+ metadata.gz: b3256245ddafd1c12a22b753c2adca878cfabafea1b454030bee2eeb983fc9060241d07bd6f6befc12bf8c9bd9e09f468d39f848b72de10c4f6b6fe2efc68c63
7
+ data.tar.gz: e8b27a9a15222a5203f6c2db8d07851a74a004fe643a6abe0974354653f30afc89653e0ada6dee3e462ec1c09e9b779b9221d83c0dca586084d8d77aa03f3919
data/HISTORY.md CHANGED
@@ -1,3 +1,13 @@
1
+ ### 1.1.0
2
+
3
+ * Add helper method for scope option.
4
+
5
+ * Pass serialization options to child serializers.
6
+
7
+ * Add `if` option to conditionally include fields.
8
+
9
+ * Better cache keys handling for more complex objects.
10
+
1
11
  ### 1.0.2
2
12
 
3
13
  * Better integration with ActiveSupport caching.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- This gem provides a highly optimized framework for serializing Ruby objects into hashes suitable for serialization to some other format (i.e. JSON). It provides many of the same features as other serialization frameworks like active_model_serializers, but it is designed to emphasize code efficiency over feature set.
1
+ This gem provides a highly optimized framework for serializing Ruby objects into hashes suitable for serialization to some other format (i.e. JSON). It provides many of the same features as other serialization frameworks like active_model_serializers, but it is designed to emphasize code efficiency over feature set and syntactic surgar.
2
2
 
3
3
  ## Examples
4
4
 
@@ -27,7 +27,7 @@ person = Person.new(:id => 1, :first_name => "John", :last_name => "Doe", :gende
27
27
 
28
28
  Serializers are classes that include `FastSerializer::Serializer`. Call the `serialize` method to specify which fields to include in the serialized object. Field values are gotten by calling the corresponding method on the serializer. By default each serialized field will define a method that delegates to the wrapped object.
29
29
 
30
- ruby```
30
+ ```ruby
31
31
  class PersonSerializer
32
32
  include FastSerializer::Serializer
33
33
  serialize :id, :name
@@ -81,6 +81,25 @@ PersonSerializer.new(person).as_json # => {
81
81
  # }
82
82
  ```
83
83
 
84
+ Subclasses of serializers inherit all attributes. You can add or remove additional attributes in a subclass.
85
+
86
+ ```ruby
87
+ class PersonSerializer
88
+ include FastSerializer::Serializer
89
+ serialize :id
90
+ serialize :name
91
+ serialize :phone
92
+ end
93
+
94
+ class EmployeeSerializer < PersonSerializer
95
+ serialize :email
96
+ remove :phone
97
+ end
98
+
99
+ PersonSerializer.new(person).as_json # => {:id => 1, :name => "John Doe", :phone => "222-555-1212"}
100
+ EmployeeSerializer.new(person).as_json # => {:id => 1, :name => "John Doe", :email => "john@example.com"}
101
+ ```
102
+
84
103
  ### Optional and excluding fields
85
104
 
86
105
  Serializer can have optional fields. You can also specify fields to exclude.
@@ -102,6 +121,34 @@ PersonSerializer.new(person, :include => [:gender]).as_json # => {:id => 1, :nam
102
121
  PersonSerializer.new(person, :exclude => [:id]).as_json # => {:name => "John Doe"}
103
122
  ```
104
123
 
124
+ You can also pass the `:include` and `:exclude` options as hashes if you want to have them apply to associated records.
125
+
126
+ ```ruby
127
+ class PersonSerializer
128
+ include FastSerializer::Serializer
129
+ serialize :id
130
+ serialize :name
131
+ serialize :company, serializer: CompanySerializer
132
+ end
133
+
134
+ PersonSerializer.new(person, :exclude => {:company => :address}).as_json
135
+ ```
136
+
137
+ You can also specify fields to be optional with an `:if` block in the definition with the name of a method from the serializer. It can also be a `Proc` that will be executed with the binding of an instance of the serializer. The field will only be included if the method returns a truthy value.
138
+
139
+ ```ruby
140
+ class PersonSerializer
141
+ include FastSerializer::Serializer
142
+ serialize :id
143
+ serialize :name, if: -> { scope && scope.id == id }
144
+ serialize :role, if: :staff?
145
+
146
+ def staff?
147
+ object.staff?
148
+ end
149
+ end
150
+ ```
151
+
105
152
  ### Serializer options
106
153
 
107
154
  You can specify custom options that control how the object is serialized.
@@ -125,6 +172,17 @@ PersonSerializer.new(person).as_json # => {:id => 1, :name => "John Doe"}
125
172
  PersonSerializer.new(person, :last_first => true).as_json # => {:id => 1, :name => "Doe, John"}
126
173
  ```
127
174
 
175
+ The options hash is passed to all nested serializers. The special option name `:scope` is available as a method within the serializer and is used by convention to enforce various data restrictions.
176
+
177
+ ```ruby
178
+ class PersonSerializer
179
+ include FastSerializer::Serializer
180
+ serialize :id
181
+ serialize :name
182
+ serialize :email, if: -> { scope && scope.id == object.id }
183
+ end
184
+ ```
185
+
128
186
  ### Caching
129
187
 
130
188
  You can make serializers cacheable so that the serialized value can be stored and fetched from a cache.
@@ -162,6 +220,7 @@ You can also pass a cache to a serializer using the `:cache` option.
162
220
  If you have a collection of objects to serialize, you can use the `FastSerializer::ArraySerializer` to serialize an enumeration of objects.
163
221
 
164
222
  ```ruby
223
+ FastSerializer::ArraySerializer.new([a, b, c, d], :serializer => MyObjectSerializer)
165
224
  ```
166
225
 
167
226
  ## Performance
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.2
1
+ 1.1.0
@@ -20,5 +20,5 @@ Gem::Specification.new do |spec|
20
20
  spec.add_development_dependency "bundler", "~>1.3"
21
21
  spec.add_development_dependency "rake"
22
22
  spec.add_development_dependency "rspec", "~>3.0"
23
- spec.add_development_dependency "active_support", "~>4.0"
23
+ spec.add_development_dependency "active_support", ">=4.0"
24
24
  end
@@ -1,11 +1,12 @@
1
1
  module FastSerializer
2
2
  # Data structure used internally for maintaining a field to be serialized.
3
3
  class SerializedField
4
- attr_reader :name
4
+ attr_reader :name, :condition
5
5
 
6
- def initialize(name, optional: false, serializer: nil, serializer_options: nil, enumerable: false)
6
+ def initialize(name, optional: false, serializer: nil, serializer_options: nil, enumerable: false, condition: nil)
7
7
  @name = name
8
8
  @optional = !!optional
9
+ @condition = condition
9
10
  if serializer
10
11
  @serializer = serializer
11
12
  @serializer_options = serializer_options
@@ -18,13 +19,13 @@ module FastSerializer
18
19
  end
19
20
 
20
21
  # Wrap a value in the serializer if one has been set. Otherwise just returns the raw value.
21
- def serialize(value)
22
+ def serialize(value, options = nil)
22
23
  if value && @serializer
23
24
  serializer = nil
24
25
  if @enumerable
25
- serializer = ArraySerializer.new(value, :serializer => @serializer, :serializer_options => @serializer_options)
26
+ serializer = ArraySerializer.new(value, :serializer => @serializer, :serializer_options => serializer_options(options))
26
27
  else
27
- serializer = @serializer.new(value, @serializer_options)
28
+ serializer = @serializer.new(value, serializer_options(options))
28
29
  end
29
30
  context = SerializationContext.current
30
31
  if context
@@ -39,30 +40,39 @@ module FastSerializer
39
40
 
40
41
  private
41
42
 
43
+ def serializer_options(options)
44
+ if options
45
+ if @serializer_options
46
+ deep_merge(@serializer_options, options)
47
+ else
48
+ options
49
+ end
50
+ else
51
+ @serializer_options
52
+ end
53
+ end
54
+
55
+ def deep_merge(hash, merge_hash)
56
+ retval = {}
57
+ merge_hash.each do |key, merge_value|
58
+ value = hash[key]
59
+ if value.is_a?(Hash) && merge_value.is_a?(Hash)
60
+ retval[key] = deep_merge(value, merge_value)
61
+ else
62
+ retval[key] = merge_value
63
+ end
64
+ end
65
+ retval
66
+ end
67
+
42
68
  # Convert the value to primitive data types: string, number, boolean, symbol, time, date, array, hash.
43
69
  def serialize_value(value)
44
70
  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
71
  value
46
72
  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
73
+ serialize_hash(value)
56
74
  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
75
+ serialize_enumerable(value)
66
76
  elsif value.respond_to?(:as_json)
67
77
  value.as_json
68
78
  elsif value.respond_to?(:to_hash)
@@ -74,5 +84,28 @@ module FastSerializer
74
84
  end
75
85
  end
76
86
 
87
+ def serialize_hash(value)
88
+ hash = nil
89
+ value.each do |k, v|
90
+ val = serialize_value(v)
91
+ if val.object_id != v.object_id
92
+ hash = value.dup unless hash
93
+ hash[k] = val
94
+ end
95
+ end
96
+ hash || value
97
+ end
98
+
99
+ def serialize_enumerable(value)
100
+ array = nil
101
+ value.each_with_index do |v, i|
102
+ val = serialize_value(v)
103
+ if val.object_id != v.object_id
104
+ array = value.dup unless array
105
+ array[i] = val
106
+ end
107
+ end
108
+ array || value
109
+ end
77
110
  end
78
111
  end
@@ -94,9 +94,25 @@ module FastSerializer
94
94
  # used if the :serializer option has been set. If the field is marked as enumerable, then the value will be
95
95
  # serialized as an array with each element wrapped in the specified serializer.
96
96
  #
97
+ # * condition: Block or method name that will be called at runtime bound to the serializer that will
98
+ # determine if the attribute will be included or not.
99
+ #
97
100
  # Subclasses will inherit all of their parent classes serialized fields. Subclasses can override fields
98
101
  # 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)
102
+ def serialize(*fields)
103
+ options = {}
104
+ if fields.size > 1 && fields.last.is_a?(Hash)
105
+ options = fields.last
106
+ fields = fields[0, fields.size - 1]
107
+ end
108
+ as = options[:as]
109
+ optional = options.fetch(:optional, false)
110
+ delegate = options.fetch(:delegate, true)
111
+ enumerable = options.fetch(:enumerable, false)
112
+ serializer = options[:serializer]
113
+ serializer_options = options[:serializer_options]
114
+ condition = options[:if]
115
+
100
116
  if as && fields.size > 1
101
117
  raise ArgumentError.new("Cannot specify :as argument with multiple fields to serialize")
102
118
  end
@@ -109,7 +125,7 @@ module FastSerializer
109
125
 
110
126
  field = field.to_sym
111
127
  attribute = (name || field).to_sym
112
- add_field(attribute, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable)
128
+ add_field(attribute, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable, condition: condition)
113
129
 
114
130
  if delegate && !method_defined?(attribute)
115
131
  define_delegate(attribute, field)
@@ -209,9 +225,16 @@ module FastSerializer
209
225
  private
210
226
 
211
227
  # Add a field to be serialized.
212
- def add_field(name, optional:, serializer:, serializer_options:, enumerable:)
228
+ def add_field(name, optional:, serializer:, serializer_options:, enumerable:, condition:)
213
229
  name = name.to_sym
214
- field = SerializedField.new(name, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable)
230
+ if condition.is_a?(Proc)
231
+ include_method_name = "__include_#{name}?".to_sym
232
+ define_method(include_method_name, condition)
233
+ private include_method_name
234
+ condition = include_method_name
235
+ end
236
+
237
+ field = SerializedField.new(name, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable, condition: condition)
215
238
 
216
239
  # Add the field to the frozen list of fields.
217
240
  field_list = []
@@ -278,6 +301,10 @@ module FastSerializer
278
301
  @options[name] if @options
279
302
  end
280
303
 
304
+ def scope
305
+ option(:scope)
306
+ end
307
+
281
308
  # Return true if this serializer is cacheable.
282
309
  def cacheable?
283
310
  option(:cacheable) || self.class.cacheable?
@@ -297,7 +324,8 @@ module FastSerializer
297
324
  # key is an array made up of the serializer class name, wrapped object, and
298
325
  # serialization options hash.
299
326
  def cache_key
300
- [self.class.name, object, options]
327
+ object_cache_key = (object.respond_to?(:cache_key) ? object.cache_key : object)
328
+ [self.class.name, object_cache_key, options_cache_key(options)]
301
329
  end
302
330
 
303
331
  # :nodoc:
@@ -316,17 +344,40 @@ module FastSerializer
316
344
  SerializationContext.use do
317
345
  self.class.serializable_fields.each do |field|
318
346
  name = field.name
347
+
319
348
  if field.optional?
320
349
  next unless include_fields && include_fields.include?(name)
321
350
  end
322
- next if excluded_fields && excluded_fields.include?(name)
323
- value = field.serialize(send(name))
351
+ next if excluded_fields && excluded_fields[name] == true
352
+ condition = field.condition
353
+ next if condition && !send(condition)
354
+
355
+ value = field.serialize(send(name), serializer_options(name))
324
356
  hash[name] = value
325
357
  end
326
358
  end
327
359
  hash
328
360
  end
329
361
 
362
+ def serializer_options(name)
363
+ opts = options
364
+ return nil unless opts
365
+ if opts && (opts.include?(:include) || opts.include?(:exclude))
366
+ opts = opts.dup
367
+ include_options = opts[:include]
368
+ if include_options.is_a?(Hash)
369
+ include_options = include_options[name.to_sym]
370
+ opts[:include] = include_options if include_options
371
+ end
372
+ exclude_options = options[:exclude]
373
+ if exclude_options.is_a?(Hash)
374
+ exclude_options = exclude_options[name.to_sym]
375
+ opts[:exclude] = exclude_options if exclude_options
376
+ end
377
+ end
378
+ opts
379
+ end
380
+
330
381
  # Load the hash that will represent the wrapped object as a serialized object from a cache.
331
382
  def load_from_cache
332
383
  if cache
@@ -340,24 +391,52 @@ module FastSerializer
340
391
 
341
392
  private
342
393
 
343
- # Return a list of optional fields to be included in the output from the :include option.
344
- def included_optional_fields
345
- included_fields = option(:include)
346
- if included_fields
347
- Array(included_fields).collect(&:to_sym)
394
+ def options_cache_key(options)
395
+ return nil if options.nil?
396
+ if options.respond_to?(:cache_key)
397
+ options.cache_key
398
+ elsif options.is_a?(Hash)
399
+ hash_key = {}
400
+ options.each do |key, value|
401
+ hash_key[key] = options_cache_key(value)
402
+ end
403
+ hash_key
404
+ elsif options.is_a?(Enumerable)
405
+ options.collect{|option| options_cache_key(option)}
348
406
  else
349
- nil
407
+ options
350
408
  end
351
409
  end
352
410
 
411
+ # Return a list of optional fields to be included in the output from the :include option.
412
+ def included_optional_fields
413
+ normalize_field_list(option(:include))
414
+ end
415
+
353
416
  # Return a list of fields to be excluded from the output from the :exclude option.
354
417
  def excluded_regular_fields
355
- excluded_fields = option(:exclude)
356
- if excluded_fields
357
- Array(excluded_fields).collect(&:to_sym)
418
+ normalize_field_list(option(:exclude))
419
+ end
420
+
421
+ def normalize_field_list(vals)
422
+ return nil if vals.nil?
423
+ if vals.is_a?(Hash)
424
+ hash = nil
425
+ vals.each do |key, values|
426
+ if hash || !key.is_a?(Symbol)
427
+ hash ||= {}
428
+ hash[key.to_sym] = values
429
+ end
430
+ end
431
+ vals = hash if hash
358
432
  else
359
- nil
433
+ hash = {}
434
+ Array(vals).each do |key|
435
+ hash[key.to_sym] = true
436
+ end
437
+ vals = hash
360
438
  end
439
+ vals
361
440
  end
362
441
  end
363
442
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe FastSerializer::Serializer do
4
4
 
5
- let(:model){ SimpleModel.new(:id => 1, :name => "foo", :description => "foobar") }
5
+ let(:model){ SimpleModel.new(:id => 1, :name => "foo", :description => "foobar", :number => 12) }
6
6
 
7
7
  it "should serialize object to JSON compatible format" do
8
8
  serializer = SimpleSerializer.new(model)
@@ -102,6 +102,25 @@ describe FastSerializer::Serializer do
102
102
  })
103
103
  end
104
104
 
105
+ it "should pass include and exclude options to child serializers" do
106
+ other_model = SimpleModel.new(:id => 3, :name => "other")
107
+ complex = SimpleModel.new(:id => 2, :name => :complex, :parent => model, :associations => [model, other_model])
108
+ serializer = ComplexSerializer.new(complex, :serial_number => 15, :exclude => {:associations => [:validated]}, :include => {:parent => :amount})
109
+ expect(serializer.as_json).to eq({
110
+ :id => 2, :name => :complex, :validated => false, :serial_number => 15,
111
+ :associations => [
112
+ {:id => 1, :name => "foo"},
113
+ {:id => 3, :name => "other"}
114
+ ],
115
+ :parent => {:id => 1, :name => "foo", :validated => false, :amount => 12}
116
+ })
117
+ end
118
+
119
+ it "should have a scope option" do
120
+ serializer = CachedSerializer.new(model, :scope => :foo)
121
+ expect(serializer.scope).to eq :foo
122
+ end
123
+
105
124
  it "should return identical serialized values for serializers on the same object and options inside the same context" do
106
125
  other_model = SimpleModel.new(:id => 3, :name => "other")
107
126
  complex = SimpleModel.new(:id => 2, :name => "complex", :associations => [model, other_model, model])
@@ -122,7 +141,7 @@ describe FastSerializer::Serializer do
122
141
  "associations" => [
123
142
  {"id" => 1, "name" => "foo", "validated" => false}
124
143
  ],
125
- "parent" => {"id" => 1, "name" => "foo", "validated" => false}
144
+ "parent" => {"id" => 1, "name" => "foo", "validated" => false, "description"=>"foobar"}
126
145
  })
127
146
  end
128
147
 
@@ -149,4 +168,10 @@ describe FastSerializer::Serializer do
149
168
  serializer = CircularSerializer.new(model)
150
169
  expect{ serializer.as_json }.to raise_error(FastSerializer::CircularReferenceError)
151
170
  end
171
+
172
+ it "should allow conditionals on serialized fields" do
173
+ expect(ConditionalSerializer.new(model).as_json).to eq({:id => 1})
174
+ expect(ConditionalSerializer.new(model, :scope => :description).as_json).to eq({:id => 1, :description => "foobar"})
175
+ expect(ConditionalSerializer.new(model, :scope => :name).as_json).to eq({:id => 1, :name => "foo"})
176
+ end
152
177
  end
@@ -51,7 +51,7 @@ end
51
51
  class ComplexSerializer < SimpleSerializer
52
52
  serialize :serial_number, delegate: false
53
53
  serialize :associations, delegate: true, serializer: CachedSerializer, enumerable: true
54
- serialize :parent, delegate: true, serializer: SimpleSerializer
54
+ serialize :parent, delegate: true, serializer: SimpleSerializer, serializer_options: {include: :description}
55
55
 
56
56
  def serial_number
57
57
  option(:serial_number)
@@ -62,3 +62,13 @@ class CircularSerializer < SimpleSerializer
62
62
  remove :name, :validated
63
63
  serialize :parent, serializer: self
64
64
  end
65
+
66
+ class ConditionalSerializer < SimpleSerializer
67
+ remove :validated
68
+ serialize :description, if: -> { scope == :description }
69
+ serialize :name, if: :show_name?
70
+
71
+ def show_name?
72
+ scope == :name
73
+ end
74
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - We Heart It
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-08-06 00:00:00.000000000 Z
12
+ date: 2018-08-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -57,14 +57,14 @@ dependencies:
57
57
  name: active_support
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - "~>"
60
+ - - ">="
61
61
  - !ruby/object:Gem::Version
62
62
  version: '4.0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - "~>"
67
+ - - ">="
68
68
  - !ruby/object:Gem::Version
69
69
  version: '4.0'
70
70
  description: Super fast object serialization for API's combining a simple DSL with