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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5f74dbde94fa39ac31bdc30753fe69645952f58e
|
4
|
+
data.tar.gz: bdce8bb4428da1e1a4ce866635c3a22a636d26d2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 442fa957a7d296934849581cd5dd8aa777c6cfda8ea5e52d889f0b2e27f11ce953e88a0b58ada157ce0cb89f4451dd8168d76ed43211ff7214480f3313bc1aaf
|
7
|
+
data.tar.gz: 07157e747b2af7c9f3e6636cbc61f2f48dd0c075530151e897a7a232714a2e12dc23666db046f5172d487b30ceb6917a7d4d555a5dd711e05940ced9ce97d707
|
data/.gitignore
ADDED
data/HISTORY.txt
ADDED
data/MIT_LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 We Heart It
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
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.
|
2
|
+
|
3
|
+
## Examples
|
4
|
+
|
5
|
+
For these examples we'll assume we have a simple Person class.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class Person
|
9
|
+
attr_accessor :id, :first_name, :last_name, :parents, :children
|
10
|
+
|
11
|
+
def intitialize(attributes = {})
|
12
|
+
@id = attributes[:id]
|
13
|
+
@first_name = attributes[:first_name]
|
14
|
+
@last_name = attributes[:last_name]
|
15
|
+
@gender = attributes(:gender)
|
16
|
+
@parent = attributes[:parents]
|
17
|
+
@children = attributes[:children] || {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
other.instance_of?(self.class) && other.id == id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
person = Person.new(:id => 1, :first_name => "John", :last_name => "Doe", :gender => "M")
|
26
|
+
```
|
27
|
+
|
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
|
+
|
30
|
+
ruby```
|
31
|
+
class PersonSerializer
|
32
|
+
include FastSerializer::Serializer
|
33
|
+
serialize :id, :name
|
34
|
+
|
35
|
+
def name
|
36
|
+
"#{object.first_name} #{object.last_name}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
PersonSerializer.new(person).as_json # => {:id => 1, :name => "John Doe"}
|
41
|
+
```
|
42
|
+
|
43
|
+
You can alias fields so the serialized field name is different than the internal field name. You can also turn off creating the delegation method if it isn't needed for a field.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class PersonSerializer
|
47
|
+
include FastSerializer::Serializer
|
48
|
+
serialize :id, as: :person_id
|
49
|
+
serialize :name, :delegate => false
|
50
|
+
|
51
|
+
def name
|
52
|
+
"#{object.first_name} #{object.last_name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
PersonSerializer.new(person).as_json # => {:person_id => 1, :name => "John Doe"}
|
57
|
+
```
|
58
|
+
|
59
|
+
You can specify a serializer to use on fields that return complex objects.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class PersonSerializer
|
63
|
+
include FastSerializer::Serializer
|
64
|
+
serialize :id
|
65
|
+
serialize :name, :delegate => false
|
66
|
+
serialize :parent, serializer: PersonSerializer
|
67
|
+
serialize :children, serializer: PersonSerializer, enumerable: true
|
68
|
+
|
69
|
+
def name
|
70
|
+
"#{object.first_name} #{object.last_name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
person.parent = Person.new(:id => 2, :first_name => "Sally", :last_name => "Smith")
|
75
|
+
person.children << Person.new(:id => 3, :first_name => "Jane", :last_name => "Doe")
|
76
|
+
PersonSerializer.new(person).as_json # => {
|
77
|
+
# :id => 1,
|
78
|
+
# :name => "John Doe",
|
79
|
+
# :parent => {:id => 2, :name => "Sally Smith"},
|
80
|
+
# :children => [{:id => 3, :name => "Jane Doe"}]
|
81
|
+
# }
|
82
|
+
```
|
83
|
+
|
84
|
+
Serializer can have optional fields. You can also specify fields to exclude.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class PersonSerializer
|
88
|
+
include FastSerializer::Serializer
|
89
|
+
serialize :id
|
90
|
+
serialize :name, :delegate => false
|
91
|
+
serialize :gender, optional: true
|
92
|
+
|
93
|
+
def name
|
94
|
+
"#{object.first_name} #{object.last_name}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
PersonSerializer.new(person).as_json # => {:id => 1, :name => "John Doe"}
|
99
|
+
PersonSerializer.new(person, :include => [:gender]).as_json # => {:id => 1, :name => "John Doe", :gender => "M"}
|
100
|
+
PersonSerializer.new(person, :exclude => [:id]).as_json # => {:name => "John Doe"}
|
101
|
+
```
|
102
|
+
|
103
|
+
You can specify custom options that control how the object is serialized.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class PersonSerializer
|
107
|
+
include FastSerializer::Serializer
|
108
|
+
serialize :id
|
109
|
+
serialize :name, :delegate => false
|
110
|
+
|
111
|
+
def name
|
112
|
+
if option(:last_first)
|
113
|
+
"#{object.last_name}, #{object.first_name}"
|
114
|
+
else
|
115
|
+
"#{object.first_name} #{object.last_name}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
PersonSerializer.new(person).as_json # => {:id => 1, :name => "John Doe"}
|
121
|
+
PersonSerializer.new(person, :last_first).as_json # => {:id => 1, :name => "Doe, John"}
|
122
|
+
```
|
123
|
+
|
124
|
+
You can make serializers cacheable so that the serialized value can be stored and fetched from a cache.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class PersonSerializer
|
128
|
+
include FastSerializer::Serializer
|
129
|
+
serialize :id
|
130
|
+
serialize :name, :delegate => false
|
131
|
+
|
132
|
+
cacheable true, ttl: 60
|
133
|
+
|
134
|
+
def name
|
135
|
+
if option(:last_first)
|
136
|
+
"#{object.last_name}, #{object.first_name}"
|
137
|
+
else
|
138
|
+
"#{object.first_name} #{object.last_name}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
FastSerializer.cache = MyCache.new # Must be an implementation of FastSerializer::Cache
|
144
|
+
```
|
145
|
+
|
146
|
+
## Performance
|
147
|
+
|
148
|
+
Your mileage may vary. In many cases the performance of the serialization code doesn't particularly matter and this gem performs just about as well as other solutions. However, if you do have high throughput API or can utilize the caching features or have heavily nested models in your JSON responses, then the performance increase may be noticeable.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc 'Default: run unit tests.'
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
desc 'RVM likes to call it tests'
|
7
|
+
task :tests => :test
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'rspec'
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
desc 'Run the unit tests'
|
13
|
+
RSpec::Core::RakeTask.new(:test)
|
14
|
+
rescue LoadError
|
15
|
+
task :test do
|
16
|
+
STDERR.puts "You must have rspec >= 2.0 installed to run the tests"
|
17
|
+
end
|
18
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "fast_serializer"
|
7
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).chomp
|
8
|
+
spec.authors = ["We Heart It", "Brian Durand"]
|
9
|
+
spec.email = ["dev@weheartit.com", "bbdurand@gmail.com"]
|
10
|
+
spec.description = %q{Super fast object serialization for API's combining a simple DSL with many optimizations under the hood.}
|
11
|
+
spec.summary = %q{Super fast object serialization for API's.}
|
12
|
+
spec.homepage = "https://github.com/weheartit/fast_serializer"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~>1.3"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rspec", "~>3.0"
|
23
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module FastSerializer
|
6
|
+
require_relative 'fast_serializer/cache'
|
7
|
+
require_relative 'fast_serializer/cache/active_support_cache'
|
8
|
+
require_relative 'fast_serializer/serialization_context'
|
9
|
+
require_relative 'fast_serializer/serialized_field'
|
10
|
+
require_relative 'fast_serializer/serializer'
|
11
|
+
require_relative 'fast_serializer/array_serializer'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Get the global cache implementation used for storing cacheable serializers.
|
15
|
+
def cache
|
16
|
+
@cache if defined?(@cache)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set the global cache implementation used for storing cacheable serializers.
|
20
|
+
# The cache implementation should implement the +fetch+ method as defined in
|
21
|
+
# FastSerializer::Cache. By default no cache is set so caching won't do anything.
|
22
|
+
#
|
23
|
+
# In a Rails app, you can initialize the cache by simply passing in the value :rails
|
24
|
+
# to use the default Rails.cache.
|
25
|
+
def cache=(cache)
|
26
|
+
cache = Cache::ActiveSupportCache.new(Rails.cache) if cache == :rails
|
27
|
+
@cache = cache
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Exception raised when there is a circular reference serializing a model dependent on itself.
|
32
|
+
class CircularReferenceError < StandardError
|
33
|
+
def initialize(model)
|
34
|
+
super("Circular refernce on #{model.inspect}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# Serializer implementation for serializing an array of objects.
|
3
|
+
# This class allows taking advantage of a single SerializationContext
|
4
|
+
# for caching duplicate serializers.
|
5
|
+
class ArraySerializer
|
6
|
+
include Serializer
|
7
|
+
|
8
|
+
serialize :array
|
9
|
+
|
10
|
+
def initialize(object, options = nil)
|
11
|
+
super(Array(object), options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def cache_key
|
15
|
+
if option(:serializer)
|
16
|
+
array.collect(&:cache_key)
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def cacheable?
|
23
|
+
if option(:cacheable) || self.class.cacheable?
|
24
|
+
true
|
25
|
+
elsif option(:serializer)
|
26
|
+
option(:serializer).cacheable?
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def cache_ttl
|
33
|
+
if option(:cache_ttl)
|
34
|
+
true
|
35
|
+
elsif option(:serializer)
|
36
|
+
option(:serializer).cache_ttl
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def cache
|
43
|
+
if option(:cache)
|
44
|
+
true
|
45
|
+
elsif option(:serializer)
|
46
|
+
option(:serializer).cache
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def as_json(*args)
|
53
|
+
if array.nil?
|
54
|
+
nil
|
55
|
+
elsif array.empty?
|
56
|
+
[]
|
57
|
+
else
|
58
|
+
super[:array]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
undef :to_hash
|
63
|
+
undef :to_h
|
64
|
+
alias :to_a :as_json
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def load_from_cache
|
69
|
+
if cache
|
70
|
+
values = cache.fetch_all(array, cache_ttl){|serializer| serializer.as_json}
|
71
|
+
{:array => values}
|
72
|
+
else
|
73
|
+
load_hash
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def array
|
80
|
+
unless defined?(@_array)
|
81
|
+
serializer = option(:serializer)
|
82
|
+
if serializer
|
83
|
+
serializer_options = option(:serializer_options)
|
84
|
+
@_array = object.collect{|obj| serializer.new(obj, serializer_options)}
|
85
|
+
else
|
86
|
+
@_array = object
|
87
|
+
end
|
88
|
+
end
|
89
|
+
@_array
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# Base class for cache implementations for storing cacheable serializers.
|
3
|
+
# Implementations must implement the +fetch+ method.
|
4
|
+
class Cache
|
5
|
+
def fetch(serializer, ttl, &block)
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
# Fetch multiple serializers from the cache. The default behavior is just
|
10
|
+
# to call +fetch+ with each serializer. Implementations may optimize this
|
11
|
+
# if the cache can return multiple values at once.
|
12
|
+
#
|
13
|
+
# The block to this method will be yielded to with each uncached serializer.
|
14
|
+
def fetch_all(serializers, ttl)
|
15
|
+
serializers.collect do |serializer|
|
16
|
+
fetch(serializer, ttl) do
|
17
|
+
yield(serializer)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# ActiveSupport compatible cache implementation.
|
3
|
+
class Cache::ActiveSupportCache < Cache
|
4
|
+
attr_reader :cache
|
5
|
+
|
6
|
+
def initialize(cache)
|
7
|
+
@cache = cache
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch(serializer, ttl, &block)
|
11
|
+
exists = !!@cache.read(serializer.cache_key)
|
12
|
+
@cache.fetch(serializer.cache_key, :expires_in => ttl, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_all(serializers, ttl)
|
16
|
+
@cache.fetch_multi(*serializers) do |serializer|
|
17
|
+
yield(serializer)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# This class provides a context for creating serializers that allows
|
3
|
+
# duplicate serializers to be re-used within the context. This then
|
4
|
+
# short circuits the serialization process on the duplicates.
|
5
|
+
class SerializationContext
|
6
|
+
class << self
|
7
|
+
# Use a context or create one for use within a block. Any serializers
|
8
|
+
# based on the same object with the same options within the block will be
|
9
|
+
# re-used instead of creating duplicates.
|
10
|
+
def use
|
11
|
+
if Thread.current[:fast_serializer_context]
|
12
|
+
yield
|
13
|
+
else
|
14
|
+
begin
|
15
|
+
Thread.current[:fast_serializer_context] = new
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
Thread.current[:fast_serializer_context] = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the current context or nil if none is in use.
|
24
|
+
def current
|
25
|
+
Thread.current[:fast_serializer_context]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@cache = nil
|
31
|
+
@references = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a serializer from the context cache if a duplicate has already
|
35
|
+
# been created. Otherwise creates the serializer and adds it to the
|
36
|
+
# cache.
|
37
|
+
def load(serializer_class, object, options = nil)
|
38
|
+
key = [serializer_class, object, options]
|
39
|
+
serializer = nil
|
40
|
+
if @cache
|
41
|
+
serializer = @cache[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
unless serializer
|
45
|
+
serializer = serializer_class.allocate
|
46
|
+
serializer.send(:initialize, object, options)
|
47
|
+
@cache ||= {}
|
48
|
+
@cache[key] = serializer
|
49
|
+
end
|
50
|
+
|
51
|
+
serializer
|
52
|
+
end
|
53
|
+
|
54
|
+
# Maintain reference stack to avoid circular references.
|
55
|
+
def with_reference(object)
|
56
|
+
if @references
|
57
|
+
raise CircularReferenceError.new(object) if @references.include?(object)
|
58
|
+
else
|
59
|
+
@references = []
|
60
|
+
end
|
61
|
+
|
62
|
+
begin
|
63
|
+
@references.push(object)
|
64
|
+
yield
|
65
|
+
ensure
|
66
|
+
@references.pop
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|