alba 0.12.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
- t.test_files = FileList["test/**/*_test.rb"]
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.7')
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.
@@ -0,0 +1,315 @@
1
+ # Benchmark script to run varieties of JSON serializers
2
+ # Fetch Alba from local, otherwise fetch latest from RubyGems
3
+
4
+ # --- Bundle dependencies ---
5
+
6
+ require "bundler/inline"
7
+
8
+ gemfile(true) do
9
+ source "https://rubygems.org"
10
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
11
+
12
+ gem "active_model_serializers"
13
+ gem "activerecord", "6.1.3"
14
+ gem "alba", path: '../'
15
+ gem "benchmark-ips"
16
+ gem "blueprinter"
17
+ gem "jbuilder"
18
+ gem "jsonapi-serializer" # successor of fast_jsonapi
19
+ gem "multi_json"
20
+ gem "oj"
21
+ gem "representable"
22
+ gem "sqlite3"
23
+ end
24
+
25
+ # --- Test data model setup ---
26
+
27
+ require "active_record"
28
+ require "logger"
29
+ require "oj"
30
+ require "sqlite3"
31
+ Oj.optimize_rails
32
+
33
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
34
+ # ActiveRecord::Base.logger = Logger.new($stdout)
35
+
36
+ ActiveRecord::Schema.define do
37
+ create_table :posts, force: true do |t|
38
+ t.string :body
39
+ end
40
+
41
+ create_table :comments, force: true do |t|
42
+ t.integer :post_id
43
+ t.string :body
44
+ t.integer :commenter_id
45
+ end
46
+
47
+ create_table :users, force: true do |t|
48
+ t.string :name
49
+ end
50
+ end
51
+
52
+ class Post < ActiveRecord::Base
53
+ has_many :comments
54
+ has_many :commenters, through: :comments, class_name: 'User', source: :commenter
55
+
56
+ def attributes
57
+ {id: nil, body: nil, commenter_names: commenter_names}
58
+ end
59
+
60
+ def commenter_names
61
+ commenters.pluck(:name)
62
+ end
63
+ end
64
+
65
+ class Comment < ActiveRecord::Base
66
+ belongs_to :post
67
+ belongs_to :commenter, class_name: 'User'
68
+
69
+ def attributes
70
+ {id: nil, body: nil}
71
+ end
72
+ end
73
+
74
+ class User < ActiveRecord::Base
75
+ has_many :comments
76
+ end
77
+
78
+ # --- Alba serializers ---
79
+
80
+ require "alba"
81
+
82
+ class AlbaCommentResource
83
+ include ::Alba::Resource
84
+ attributes :id, :body
85
+ end
86
+
87
+ class AlbaPostResource
88
+ include ::Alba::Resource
89
+ attributes :id, :body
90
+ attribute :commenter_names do |post|
91
+ post.commenters.pluck(:name)
92
+ end
93
+ many :comments, resource: AlbaCommentResource
94
+ end
95
+
96
+ # --- ActiveModelSerializer serializers ---
97
+
98
+ require "active_model_serializers"
99
+
100
+ class AMSCommentSerializer < ActiveModel::Serializer
101
+ attributes :id, :body
102
+ end
103
+
104
+ class AMSPostSerializer < ActiveModel::Serializer
105
+ attributes :id, :body
106
+ attribute :commenter_names
107
+ has_many :comments, serializer: AMSCommentSerializer
108
+
109
+ def commenter_names
110
+ object.commenters.pluck(:name)
111
+ end
112
+ end
113
+
114
+ # --- Blueprint serializers ---
115
+
116
+ require "blueprinter"
117
+
118
+ class CommentBlueprint < Blueprinter::Base
119
+ fields :id, :body
120
+ end
121
+
122
+ class PostBlueprint < Blueprinter::Base
123
+ fields :id, :body, :commenter_names
124
+ association :comments, blueprint: CommentBlueprint
125
+
126
+ def commenter_names
127
+ commenters.pluck(:name)
128
+ end
129
+ end
130
+
131
+ # --- JBuilder serializers ---
132
+
133
+ require "jbuilder"
134
+
135
+ class Post
136
+ def to_builder
137
+ Jbuilder.new do |post|
138
+ post.call(self, :id, :body, :commenter_names, :comments)
139
+ end
140
+ end
141
+
142
+ def commenter_names
143
+ commenters.pluck(:name)
144
+ end
145
+ end
146
+
147
+ class Comment
148
+ def to_builder
149
+ Jbuilder.new do |comment|
150
+ comment.call(self, :id, :body)
151
+ end
152
+ end
153
+ end
154
+
155
+ # --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
156
+
157
+ class JsonApiStandardCommentSerializer
158
+ include JSONAPI::Serializer
159
+
160
+ attribute :id
161
+ attribute :body
162
+ end
163
+
164
+ class JsonApiStandardPostSerializer
165
+ include JSONAPI::Serializer
166
+
167
+ # set_type :post # optional
168
+ attribute :id
169
+ attribute :body
170
+ attribute :commenter_names
171
+
172
+ attribute :comments do |post|
173
+ post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
174
+ end
175
+ end
176
+
177
+ # --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
178
+
179
+ # code to convert from JSON:API output to "flat" JSON, like the other serializers build
180
+ class JsonApiSameFormatSerializer
181
+ include JSONAPI::Serializer
182
+
183
+ def as_json(*_options)
184
+ hash = serializable_hash
185
+
186
+ if hash[:data].is_a? Hash
187
+ hash[:data][:attributes]
188
+
189
+ elsif hash[:data].is_a? Array
190
+ hash[:data].pluck(:attributes)
191
+
192
+ elsif hash[:data].nil?
193
+ { }
194
+
195
+ else
196
+ raise "unexpected data type #{hash[:data].class}"
197
+ end
198
+ end
199
+ end
200
+
201
+ class JsonApiSameFormatCommentSerializer < JsonApiSameFormatSerializer
202
+ attribute :id
203
+ attribute :body
204
+ end
205
+
206
+ class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
207
+ attribute :id
208
+ attribute :body
209
+ attribute :commenter_names
210
+
211
+ attribute :comments do |post|
212
+ post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
213
+ end
214
+ end
215
+
216
+ # --- Representable serializers ---
217
+
218
+ require "representable"
219
+
220
+ class CommentRepresenter < Representable::Decorator
221
+ include Representable::JSON
222
+
223
+ property :id
224
+ property :body
225
+ end
226
+
227
+ class PostRepresenter < Representable::Decorator
228
+ include Representable::JSON
229
+
230
+ property :id
231
+ property :body
232
+ property :commenter_names
233
+ collection :comments
234
+
235
+ def commenter_names
236
+ commenters.pluck(:name)
237
+ end
238
+ end
239
+
240
+ # --- Test data creation ---
241
+
242
+ post = Post.create!(body: 'post')
243
+ user1 = User.create!(name: 'John')
244
+ user2 = User.create!(name: 'Jane')
245
+ post.comments.create!(commenter: user1, body: 'Comment1')
246
+ post.comments.create!(commenter: user2, body: 'Comment2')
247
+ post.reload
248
+
249
+ # --- Store the serializers in procs ---
250
+
251
+ alba = Proc.new { AlbaPostResource.new(post).serialize }
252
+ alba_inline = Proc.new do
253
+ Alba.serialize(post) do
254
+ attributes :id, :body
255
+ attribute :commenter_names do |post|
256
+ post.commenters.pluck(:name)
257
+ end
258
+ many :comments do
259
+ attributes :id, :body
260
+ end
261
+ end
262
+ end
263
+ ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
264
+ blueprinter = Proc.new { PostBlueprint.render(post) }
265
+ jbuilder = Proc.new { post.to_builder.target! }
266
+ jsonapi = proc { JsonApiStandardPostSerializer.new(post).to_json }
267
+ jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(post).to_json }
268
+ rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
269
+ representable = Proc.new { PostRepresenter.new(post).to_json }
270
+
271
+ # --- Execute the serializers to check their output ---
272
+
273
+ puts "Serializer outputs ----------------------------------"
274
+ {
275
+ alba: alba,
276
+ alba_inline: alba_inline,
277
+ ams: ams,
278
+ blueprinter: blueprinter,
279
+ jbuilder: jbuilder, # different order
280
+ jsonapi: jsonapi, # nested JSON:API format
281
+ jsonapi_same_format: jsonapi_same_format,
282
+ rails: rails,
283
+ representable: representable
284
+ }.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
285
+
286
+ # --- Run the benchmarks ---
287
+
288
+ require 'benchmark'
289
+ time = 1000
290
+ Benchmark.bmbm do |x|
291
+ x.report(:alba) { time.times(&alba) }
292
+ x.report(:alba_inline) { time.times(&alba_inline) }
293
+ x.report(:ams) { time.times(&ams) }
294
+ x.report(:blueprinter) { time.times(&blueprinter) }
295
+ x.report(:jbuilder) { time.times(&jbuilder) }
296
+ x.report(:jsonapi) { time.times(&jsonapi) }
297
+ x.report(:jsonapi_same_format) { time.times(&jsonapi_same_format) }
298
+ x.report(:rails) { time.times(&rails) }
299
+ x.report(:representable) { time.times(&representable) }
300
+ end
301
+
302
+ require 'benchmark/ips'
303
+ Benchmark.ips do |x|
304
+ x.report(:alba, &alba)
305
+ x.report(:alba_inline, &alba_inline)
306
+ x.report(:ams, &ams)
307
+ x.report(:blueprinter, &blueprinter)
308
+ x.report(:jbuilder, &jbuilder)
309
+ x.report(:jsonapi, &jsonapi)
310
+ x.report(:jsonapi_same_format, &jsonapi_same_format)
311
+ x.report(:rails, &rails)
312
+ x.report(:representable, &representable)
313
+
314
+ x.compare!
315
+ end
data/codecov.yml ADDED
@@ -0,0 +1,5 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ informational: true
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', require: false # For backend
4
+ gem 'ffaker', require: false # For testing
5
+ gem 'minitest', '~> 5.14' # For test
6
+ gem 'rake', '~> 13.0' # For test and automation
7
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
8
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
9
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
10
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
11
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
12
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
13
+ gem 'simplecov-cobertura', require: false # For test coverage
14
+ gem 'yard', require: false
15
+
16
+ platforms :ruby do
17
+ gem 'oj', '~> 3.11', require: false # For backend
18
+ gem 'ruby-prof', require: false # For performance profiling
19
+ 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,5 +1,4 @@
1
1
  require_relative 'alba/version'
2
- require_relative 'alba/serializer'
3
2
  require_relative 'alba/resource'
4
3
 
5
4
  # Core module
@@ -11,8 +10,7 @@ module Alba
11
10
  class UnsupportedBackend < Error; end
12
11
 
13
12
  class << self
14
- attr_reader :backend, :encoder
15
- attr_accessor :default_serializer
13
+ attr_reader :backend, :encoder, :inferring, :_on_error
16
14
 
17
15
  # Set the backend, which actually serializes object into JSON
18
16
  #
@@ -28,25 +26,54 @@ module Alba
28
26
  # Serialize the object with inline definitions
29
27
  #
30
28
  # @param object [Object] the object to be serialized
31
- # @param with [nil, Proc, Alba::Serializer] selializer
29
+ # @param key [Symbol]
32
30
  # @param block [Block] resource block
33
31
  # @return [String] serialized JSON string
34
32
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
35
- def serialize(object, with: nil, &block)
33
+ def serialize(object, key: nil, &block)
36
34
  raise ArgumentError, 'Block required' unless block
37
35
 
38
- resource_class.class_eval(&block)
39
- resource = resource_class.new(object)
40
- with ||= @default_serializer
41
- resource.serialize(with: with)
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
42
67
  end
43
68
 
44
69
  private
45
70
 
46
71
  def set_encoder
47
72
  @encoder = case @backend
48
- when :oj
73
+ when :oj, :oj_strict
49
74
  try_oj
75
+ when :oj_rails
76
+ try_oj(mode: :rails)
50
77
  when :active_support
51
78
  try_active_support
52
79
  when nil, :default, :json
@@ -56,10 +83,11 @@ module Alba
56
83
  end
57
84
  end
58
85
 
59
- def try_oj
86
+ def try_oj(mode: :strict)
60
87
  require 'oj'
61
- ->(hash) { Oj.dump(hash, mode: :strict) }
88
+ ->(hash) { Oj.dump(hash, mode: mode) }
62
89
  rescue LoadError
90
+ Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
63
91
  default_encoder
64
92
  end
65
93
 
@@ -67,6 +95,7 @@ module Alba
67
95
  require 'active_support/json'
68
96
  ->(hash) { ActiveSupport::JSON.encode(hash) }
69
97
  rescue LoadError
98
+ Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
70
99
  default_encoder
71
100
  end
72
101
 
@@ -76,14 +105,8 @@ module Alba
76
105
  JSON.dump(hash)
77
106
  end
78
107
  end
79
-
80
- def resource_class
81
- @resource_class ||= begin
82
- klass = Class.new
83
- klass.include(Alba::Resource)
84
- end
85
- end
86
108
  end
87
109
 
88
110
  @encoder = default_encoder
111
+ @_on_error = :raise
89
112
  end