alba 0.11.1 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +21 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +10 -4
- data/README.md +262 -34
- data/Rakefile +4 -1
- data/alba.gemspec +2 -2
- data/benchmark/local.rb +198 -0
- data/gemfiles/all.gemfile +18 -0
- data/gemfiles/without_active_support.gemfile +17 -0
- data/gemfiles/without_oj.gemfile +17 -0
- data/lib/alba.rb +38 -16
- data/lib/alba/association.rb +22 -7
- data/lib/alba/key_transformer.rb +32 -0
- data/lib/alba/many.rb +6 -3
- data/lib/alba/one.rb +5 -2
- data/lib/alba/resource.rb +125 -56
- data/lib/alba/version.rb +1 -1
- data/sider.yml +1 -0
- metadata +12 -8
- data/.travis.yml +0 -10
- data/Gemfile.lock +0 -89
- data/lib/alba/serializer.rb +0 -75
data/Rakefile
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
+
ENV["BUNDLE_GEMFILE"] = File.expand_path("gemfiles/all.gemfile") if ENV["BUNDLE_GEMFILE"] == File.expand_path("Gemfile") || ENV["BUNDLE_GEMFILE"].empty? || ENV["BUNDLE_GEMFILE"].nil?
|
5
|
+
|
4
6
|
Rake::TestTask.new(:test) do |t|
|
5
7
|
t.libs << "test"
|
6
8
|
t.libs << "lib"
|
7
|
-
|
9
|
+
file_list = ENV["BUNDLE_GEMFILE"] == File.expand_path("gemfiles/all.gemfile") ? FileList["test/**/*_test.rb"] : FileList["test/dependencies/test_dependencies.rb"]
|
10
|
+
t.test_files = file_list
|
8
11
|
end
|
9
12
|
|
10
13
|
task :default => :test
|
data/alba.gemspec
CHANGED
@@ -10,11 +10,11 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.description = "Alba is designed to be a simple, easy to use and fast alternative to existing JSON serializers. Its performance is better than almost all gems which do similar things. The internal is so simple that it's easy to hack and maintain."
|
11
11
|
spec.homepage = 'https://github.com/okuramasafumi/alba'
|
12
12
|
spec.license = 'MIT'
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
14
14
|
|
15
15
|
spec.metadata['homepage_uri'] = spec.homepage
|
16
16
|
spec.metadata['source_code_uri'] = 'https://github.com/okuramasafumi/alba'
|
17
|
-
spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/CHANGELOG.md'
|
17
|
+
spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md'
|
18
18
|
|
19
19
|
# Specify which files should be added to the gem when it is released.
|
20
20
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
data/benchmark/local.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
# Benchmark script to run varieties of JSON serializers
|
2
|
+
# Fetch Alba from local, otherwise fetch latest from RubyGems
|
3
|
+
|
4
|
+
require "bundler/inline"
|
5
|
+
|
6
|
+
gemfile(true) do
|
7
|
+
source "https://rubygems.org"
|
8
|
+
|
9
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
10
|
+
|
11
|
+
gem "activerecord", "6.1.3"
|
12
|
+
gem "sqlite3"
|
13
|
+
gem "jbuilder"
|
14
|
+
gem "active_model_serializers"
|
15
|
+
gem "blueprinter"
|
16
|
+
gem "representable"
|
17
|
+
gem "alba", path: '../'
|
18
|
+
gem "oj"
|
19
|
+
gem "multi_json"
|
20
|
+
end
|
21
|
+
|
22
|
+
require "active_record"
|
23
|
+
require "sqlite3"
|
24
|
+
require "logger"
|
25
|
+
require "oj"
|
26
|
+
Oj.optimize_rails
|
27
|
+
|
28
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
29
|
+
# ActiveRecord::Base.logger = Logger.new(STDOUT)
|
30
|
+
|
31
|
+
ActiveRecord::Schema.define do
|
32
|
+
create_table :posts, force: true do |t|
|
33
|
+
t.string :body
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :comments, force: true do |t|
|
37
|
+
t.integer :post_id
|
38
|
+
t.string :body
|
39
|
+
t.integer :commenter_id
|
40
|
+
end
|
41
|
+
|
42
|
+
create_table :users, force: true do |t|
|
43
|
+
t.string :name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Post < ActiveRecord::Base
|
48
|
+
has_many :comments
|
49
|
+
has_many :commenters, through: :comments, class_name: 'User', source: :commenter
|
50
|
+
|
51
|
+
def attributes
|
52
|
+
{id: nil, body: nil, commenter_names: commenter_names}
|
53
|
+
end
|
54
|
+
|
55
|
+
def commenter_names
|
56
|
+
commenters.pluck(:name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Comment < ActiveRecord::Base
|
61
|
+
belongs_to :post
|
62
|
+
belongs_to :commenter, class_name: 'User'
|
63
|
+
|
64
|
+
def attributes
|
65
|
+
{id: nil, body: nil}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
has_many :comments
|
71
|
+
end
|
72
|
+
|
73
|
+
require "alba"
|
74
|
+
Alba.backend = :oj
|
75
|
+
|
76
|
+
class AlbaCommentResource
|
77
|
+
include ::Alba::Resource
|
78
|
+
attributes :id, :body
|
79
|
+
end
|
80
|
+
|
81
|
+
class AlbaPostResource
|
82
|
+
include ::Alba::Resource
|
83
|
+
attributes :id, :body
|
84
|
+
many :comments, resource: AlbaCommentResource
|
85
|
+
attribute :commenter_names do |post|
|
86
|
+
post.commenters.pluck(:name)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
require "jbuilder"
|
91
|
+
class Post
|
92
|
+
def to_builder
|
93
|
+
Jbuilder.new do |post|
|
94
|
+
post.call(self, :id, :body, :comments, :commenter_names)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def commenter_names
|
99
|
+
commenters.pluck(:name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class Comment
|
104
|
+
def to_builder
|
105
|
+
Jbuilder.new do |comment|
|
106
|
+
comment.call(self, :id, :body)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
require "active_model_serializers"
|
112
|
+
|
113
|
+
class AMSCommentSerializer < ActiveModel::Serializer
|
114
|
+
attributes :id, :body
|
115
|
+
end
|
116
|
+
|
117
|
+
class AMSPostSerializer < ActiveModel::Serializer
|
118
|
+
attributes :id, :body
|
119
|
+
has_many :comments, serializer: AMSCommentSerializer
|
120
|
+
attribute :commenter_names
|
121
|
+
def commenter_names
|
122
|
+
object.commenters.pluck(:name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
require "blueprinter"
|
127
|
+
|
128
|
+
class CommentBlueprint < Blueprinter::Base
|
129
|
+
fields :id, :body
|
130
|
+
end
|
131
|
+
|
132
|
+
class PostBlueprint < Blueprinter::Base
|
133
|
+
fields :id, :body, :commenter_names
|
134
|
+
association :comments, blueprint: CommentBlueprint
|
135
|
+
def commenter_names
|
136
|
+
commenters.pluck(:name)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
require "representable"
|
141
|
+
|
142
|
+
class CommentRepresenter < Representable::Decorator
|
143
|
+
include Representable::JSON
|
144
|
+
|
145
|
+
property :id
|
146
|
+
property :body
|
147
|
+
end
|
148
|
+
|
149
|
+
class PostRepresenter < Representable::Decorator
|
150
|
+
include Representable::JSON
|
151
|
+
|
152
|
+
property :id
|
153
|
+
property :body
|
154
|
+
property :commenter_names
|
155
|
+
collection :comments
|
156
|
+
|
157
|
+
def commenter_names
|
158
|
+
commenters.pluck(:name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
post = Post.create!(body: 'post')
|
163
|
+
user1 = User.create!(name: 'John')
|
164
|
+
user2 = User.create!(name: 'Jane')
|
165
|
+
post.comments.create!(commenter: user1, body: 'Comment1')
|
166
|
+
post.comments.create!(commenter: user2, body: 'Comment2')
|
167
|
+
post.reload
|
168
|
+
|
169
|
+
alba = Proc.new { AlbaPostResource.new(post).serialize }
|
170
|
+
jbuilder = Proc.new { post.to_builder.target! }
|
171
|
+
ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
|
172
|
+
rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
|
173
|
+
blueprinter = Proc.new { PostBlueprint.render(post) }
|
174
|
+
representable = Proc.new { PostRepresenter.new(post).to_json }
|
175
|
+
alba_inline = Proc.new do
|
176
|
+
Alba.serialize(post) do
|
177
|
+
attributes :id, :body
|
178
|
+
attribute :commenter_names do |post|
|
179
|
+
post.commenters.pluck(:name)
|
180
|
+
end
|
181
|
+
many :comments do
|
182
|
+
attributes :id, :body
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
[alba, jbuilder, ams, rails, blueprinter, representable, alba_inline].each {|x| puts x.call }
|
187
|
+
|
188
|
+
require 'benchmark'
|
189
|
+
time = 1000
|
190
|
+
Benchmark.bmbm do |x|
|
191
|
+
x.report(:alba) { time.times(&alba) }
|
192
|
+
x.report(:jbuilder) { time.times(&jbuilder) }
|
193
|
+
x.report(:ams) { time.times(&ams) }
|
194
|
+
x.report(:rails) { time.times(&rails) }
|
195
|
+
x.report(:blueprinter) { time.times(&blueprinter) }
|
196
|
+
x.report(:representable) { time.times(&representable) }
|
197
|
+
x.report(:alba_inline) { time.times(&alba_inline) }
|
198
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activesupport', require: false # For backend
|
4
|
+
gem 'minitest', '~> 5.14' # For test
|
5
|
+
gem 'rake', '~> 13.0' # For test and automation
|
6
|
+
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
7
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
8
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
9
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
10
|
+
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
11
|
+
gem 'simplecov', '~> 0.21.0', require: false # For test coverage
|
12
|
+
gem 'simplecov-cobertura', require: false # For test coverage
|
13
|
+
gem 'yard', require: false
|
14
|
+
|
15
|
+
platforms :ruby do
|
16
|
+
gem 'oj', '~> 3.11', require: false # For backend
|
17
|
+
gem 'ruby-prof', require: false # For performance profiling
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'minitest', '~> 5.14' # For test
|
4
|
+
gem 'rake', '~> 13.0' # For test and automation
|
5
|
+
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
6
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
7
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
8
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
9
|
+
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
10
|
+
gem 'simplecov', '~> 0.21.0', require: false # For test coverage
|
11
|
+
gem 'simplecov-cobertura', require: false # For test coverage
|
12
|
+
gem 'yard', require: false
|
13
|
+
|
14
|
+
platforms :ruby do
|
15
|
+
gem 'oj', '~> 3.11', require: false # For backend
|
16
|
+
gem 'ruby-prof', require: false # For performance profiling
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activesupport', require: false # For backend
|
4
|
+
gem 'minitest', '~> 5.14' # For test
|
5
|
+
gem 'rake', '~> 13.0' # For test and automation
|
6
|
+
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
7
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
8
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
9
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
10
|
+
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
11
|
+
gem 'simplecov', '~> 0.21.0', require: false # For test coverage
|
12
|
+
gem 'simplecov-cobertura', require: false # For test coverage
|
13
|
+
gem 'yard', require: false
|
14
|
+
|
15
|
+
platforms :ruby do
|
16
|
+
gem 'ruby-prof', require: false # For performance profiling
|
17
|
+
end
|
data/lib/alba.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
require_relative 'alba/version'
|
2
|
-
require_relative 'alba/serializer'
|
3
2
|
require_relative 'alba/resource'
|
4
3
|
|
5
4
|
# Core module
|
6
5
|
module Alba
|
7
6
|
# Base class for Errors
|
8
7
|
class Error < StandardError; end
|
8
|
+
|
9
9
|
# Error class for backend which is not supported
|
10
10
|
class UnsupportedBackend < Error; end
|
11
11
|
|
12
12
|
class << self
|
13
|
-
attr_reader :backend, :encoder
|
14
|
-
attr_accessor :default_serializer
|
13
|
+
attr_reader :backend, :encoder, :inferring, :_on_error
|
15
14
|
|
16
15
|
# Set the backend, which actually serializes object into JSON
|
17
16
|
#
|
@@ -27,17 +26,44 @@ module Alba
|
|
27
26
|
# Serialize the object with inline definitions
|
28
27
|
#
|
29
28
|
# @param object [Object] the object to be serialized
|
30
|
-
# @param
|
29
|
+
# @param key [Symbol]
|
31
30
|
# @param block [Block] resource block
|
32
31
|
# @return [String] serialized JSON string
|
33
32
|
# @raise [ArgumentError] if block is absent or `with` argument's type is wrong
|
34
|
-
def serialize(object,
|
33
|
+
def serialize(object, key: nil, &block)
|
35
34
|
raise ArgumentError, 'Block required' unless block
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
resource.
|
36
|
+
klass = Class.new
|
37
|
+
klass.include(Alba::Resource)
|
38
|
+
klass.class_eval(&block)
|
39
|
+
resource = klass.new(object)
|
40
|
+
resource.serialize(key: key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Enable inference for key and resource name
|
44
|
+
def enable_inference!
|
45
|
+
begin
|
46
|
+
require 'active_support/inflector'
|
47
|
+
rescue LoadError
|
48
|
+
raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
|
49
|
+
end
|
50
|
+
@inferring = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Disable inference for key and resource name
|
54
|
+
def disable_inference!
|
55
|
+
@inferring = false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set error handler
|
59
|
+
#
|
60
|
+
# @param [Symbol] handler
|
61
|
+
# @param [Block]
|
62
|
+
def on_error(handler = nil, &block)
|
63
|
+
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
64
|
+
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
65
|
+
|
66
|
+
@_on_error = handler || block
|
41
67
|
end
|
42
68
|
|
43
69
|
private
|
@@ -59,6 +85,7 @@ module Alba
|
|
59
85
|
require 'oj'
|
60
86
|
->(hash) { Oj.dump(hash, mode: :strict) }
|
61
87
|
rescue LoadError
|
88
|
+
Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
|
62
89
|
default_encoder
|
63
90
|
end
|
64
91
|
|
@@ -66,6 +93,7 @@ module Alba
|
|
66
93
|
require 'active_support/json'
|
67
94
|
->(hash) { ActiveSupport::JSON.encode(hash) }
|
68
95
|
rescue LoadError
|
96
|
+
Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
|
69
97
|
default_encoder
|
70
98
|
end
|
71
99
|
|
@@ -75,14 +103,8 @@ module Alba
|
|
75
103
|
JSON.dump(hash)
|
76
104
|
end
|
77
105
|
end
|
78
|
-
|
79
|
-
def resource_class
|
80
|
-
@resource_class ||= begin
|
81
|
-
klass = Class.new
|
82
|
-
klass.include(Alba::Resource)
|
83
|
-
end
|
84
|
-
end
|
85
106
|
end
|
86
107
|
|
87
108
|
@encoder = default_encoder
|
109
|
+
@_on_error = :raise
|
88
110
|
end
|
data/lib/alba/association.rb
CHANGED
@@ -2,25 +2,40 @@ module Alba
|
|
2
2
|
# Base class for `One` and `Many`
|
3
3
|
# Child class should implement `to_hash` method
|
4
4
|
class Association
|
5
|
+
attr_reader :object
|
6
|
+
|
5
7
|
# @param name [Symbol] name of the method to fetch association
|
6
8
|
# @param condition [Proc] a proc filtering data
|
7
9
|
# @param resource [Class<Alba::Resource>] a resource class for the association
|
8
10
|
# @param block [Block] used to define resource when resource arg is absent
|
9
|
-
def initialize(name:, condition: nil, resource: nil, &block)
|
11
|
+
def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
|
10
12
|
@name = name
|
11
13
|
@condition = condition
|
12
14
|
@block = block
|
13
|
-
@resource = resource
|
14
|
-
|
15
|
-
end
|
15
|
+
@resource = resource
|
16
|
+
return if @resource
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
if @block
|
19
|
+
@resource = resource_class
|
20
|
+
elsif Alba.inferring
|
21
|
+
const_parent = nesting.nil? ? Object : Object.const_get(nesting)
|
22
|
+
@resource = const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
|
23
|
+
else
|
24
|
+
raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
|
25
|
+
end
|
20
26
|
end
|
21
27
|
|
22
28
|
private
|
23
29
|
|
30
|
+
def constantize(resource)
|
31
|
+
case resource # rubocop:disable Style/MissingElse
|
32
|
+
when Class
|
33
|
+
resource
|
34
|
+
when Symbol, String
|
35
|
+
Object.const_get(resource)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
24
39
|
def resource_class
|
25
40
|
klass = Class.new
|
26
41
|
klass.include(Alba::Resource)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Alba
|
2
|
+
# Transform keys using `ActiveSupport::Inflector`
|
3
|
+
module KeyTransformer
|
4
|
+
begin
|
5
|
+
require 'active_support/inflector'
|
6
|
+
rescue LoadError
|
7
|
+
raise ::Alba::Error, 'To use transform_keys, please install `ActiveSupport` gem.'
|
8
|
+
end
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# Transform key as given transform_type
|
13
|
+
#
|
14
|
+
# @params key [String] key to be transformed
|
15
|
+
# @params transform_type [Symbol] transform type
|
16
|
+
# @return [String] transformed key
|
17
|
+
# @raise [Alba::Error] when transform_type is not supported
|
18
|
+
def transform(key, transform_type)
|
19
|
+
key = key.to_s
|
20
|
+
case transform_type
|
21
|
+
when :camel
|
22
|
+
ActiveSupport::Inflector.camelize(key)
|
23
|
+
when :lower_camel
|
24
|
+
ActiveSupport::Inflector.camelize(key, false)
|
25
|
+
when :dash
|
26
|
+
ActiveSupport::Inflector.dasherize(key)
|
27
|
+
else
|
28
|
+
raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|