alba 1.0.1 → 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: 3fc2741e03be6373aa324768358a1acf4b713ec43bba5daf80d133a08d0cc12b
4
- data.tar.gz: 7228dc09384d5d4d6e99456a6f14d6e0cb3d678483cd59a7439d6a150f463e4b
3
+ metadata.gz: 36890dfa4b9b73b60f1d6a09cf674de7ff7a6dd495ff4b74bbb3d048f9cf5a85
4
+ data.tar.gz: 3e3d49619f646be866262e14e21e5fba11e2caafbc24064d57eb9c549cb64e4d
5
5
  SHA512:
6
- metadata.gz: 0fb68a3a1c786aa1b776246d08fd23bada8d4c7fcff0ea2ba33a49a1410d9b1e839407450ff2d1137f531a8e80738977369a4f857199d3bb65c98b140d7cd6b9
7
- data.tar.gz: fcfb93544bab6b48e6ae24946992231a6023df52763d807dad276f1468c1ce77d255fe637de3a72efacffe2d20915d203ddc1f247908c4f6e4d69ae6ea4c97da
6
+ metadata.gz: fc7e025a035b41dadaab5300096cf920eb3557a4be900d31b514dfc66e8ae803b0fb63d77ec06174d72d70ad2e07da81c491502c8462ad306f2379720222fc65
7
+ data.tar.gz: 741f5c1c69b2809aec51d2a2a139d4d8e00060c9aa174bf5fb68d5dbcec7996096aced5fb377d77fa147d7b6086a65191c8c764802c65bbd88d3806751ec0eb4
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /tmp/
9
9
 
10
10
  Gemfile.lock
11
+ /gemfiles/*.lock
data/.rubocop.yml CHANGED
@@ -30,17 +30,23 @@ Layout/MultilineAssignmentLayout:
30
30
  Lint/ConstantResolution:
31
31
  Enabled: false
32
32
 
33
- Metrics/ClassLength:
33
+ # In test code we don't care about the metrics!
34
+ Metrics:
34
35
  Exclude:
35
- - 'test/alba_test.rb'
36
+ - 'test/**/*.rb'
36
37
 
37
38
  Metrics/MethodLength:
38
39
  Max: 15
39
40
 
41
+ # `Resource` module is a core module and its length tends to be long...
42
+ Metrics/ModuleLength:
43
+ Max: 150
44
+
40
45
  # Resource class includes DSLs, which tend to accept long list of parameters
41
46
  Metrics/ParameterLists:
42
47
  Exclude:
43
48
  - 'lib/alba/resource.rb'
49
+ - 'test/**/*.rb'
44
50
 
45
51
  # We need to eval resource code to test errors on resource classes
46
52
  Security/Eval:
data/CHANGELOG.md CHANGED
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.0] - 2021-04-23
10
+
11
+ - [Feat] Implement circular associations control [71e1543]
12
+ - [Feat] Support :oj_rails backend [76e519e]
13
+
9
14
  ## [1.0.1] - 2021-04-15
10
15
 
11
16
  - [Fix] Don't cache resource class for `Alba.serialize` [9ed5253]
data/Gemfile CHANGED
@@ -4,11 +4,12 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem 'activesupport', require: false # For backend
7
+ gem 'ffaker', require: false # For testing
7
8
  gem 'minitest', '~> 5.14' # For test
8
9
  gem 'rake', '~> 13.0' # For test and automation
9
10
  gem 'rubocop', '>= 0.79.0', require: false # For lint
10
11
  gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
11
- gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
12
+ gem 'rubocop-performance', '~> 1.11.0', require: false # For lint
12
13
  gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
13
14
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
14
15
  gem 'simplecov', '~> 0.21.0', require: false # For test coverage
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  # Alba
9
9
 
10
- `Alba` is the fastest JSON serializer for Ruby, JRuby an TruffleRuby.
10
+ Alba is the fastest JSON serializer for Ruby, JRuby, and TruffleRuby.
11
11
 
12
12
  ## Discussions
13
13
 
@@ -70,6 +70,7 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
70
70
  * Root key inference
71
71
  * Error handling
72
72
  * Resource name inflection based on association name
73
+ * Circular associations control
73
74
  * No runtime dependencies
74
75
 
75
76
  ## Anti features
@@ -450,14 +451,15 @@ Alba.on_error do |error, object, key, attribute, resource_class|
450
451
  end
451
452
  ```
452
453
 
453
- ### Caching
454
+ ### Circular associations control
454
455
 
455
- Currently, Alba doesn't support caching, primarily due to the behavior of `ActiveRecord::Relation`'s cache. See [the issue](https://github.com/rails/rails/issues/41784).
456
+ You can control circular associations with `within` option. `within` option is a nested Hash such as `{book: {authors: books}}`. In this example, Alba serializes a book's authors' books. This means you can reference `BookResource` from `AuthorResource` and vice versa. This is really powerful when you have a complex data structure and serialize certain parts of it.
456
457
 
457
- ## Comparison
458
+ For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/master/test/usecases/circular_association_test.rb)
458
459
 
459
- Alba is faster than alternatives.
460
- For a performance benchmark, see https://gist.github.com/okuramasafumi/4e375525bd3a28e4ca812d2a3b3e5829.
460
+ ### Caching
461
+
462
+ Currently, Alba doesn't support caching, primarily due to the behavior of `ActiveRecord::Relation`'s cache. See [the issue](https://github.com/rails/rails/issues/41784).
461
463
 
462
464
  ## Rails
463
465
 
@@ -465,6 +467,8 @@ When you use Alba in Rails, you can create an initializer file with the line bel
465
467
 
466
468
  ```ruby
467
469
  Alba.backend = :active_support
470
+ # or
471
+ Alba.backend = :oj_rails
468
472
  ```
469
473
 
470
474
  ## Why named "Alba"?
data/benchmark/local.rb CHANGED
@@ -1,32 +1,37 @@
1
1
  # Benchmark script to run varieties of JSON serializers
2
2
  # Fetch Alba from local, otherwise fetch latest from RubyGems
3
3
 
4
+ # --- Bundle dependencies ---
5
+
4
6
  require "bundler/inline"
5
7
 
6
8
  gemfile(true) do
7
9
  source "https://rubygems.org"
8
-
9
10
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
10
11
 
11
- gem "activerecord", "6.1.3"
12
- gem "sqlite3"
13
- gem "jbuilder"
14
12
  gem "active_model_serializers"
15
- gem "blueprinter"
16
- gem "representable"
13
+ gem "activerecord", "6.1.3"
17
14
  gem "alba", path: '../'
18
- gem "oj"
15
+ gem "benchmark-ips"
16
+ gem "blueprinter"
17
+ gem "jbuilder"
18
+ gem "jsonapi-serializer" # successor of fast_jsonapi
19
19
  gem "multi_json"
20
+ gem "oj"
21
+ gem "representable"
22
+ gem "sqlite3"
20
23
  end
21
24
 
25
+ # --- Test data model setup ---
26
+
22
27
  require "active_record"
23
- require "sqlite3"
24
28
  require "logger"
25
29
  require "oj"
30
+ require "sqlite3"
26
31
  Oj.optimize_rails
27
32
 
28
33
  ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
29
- # ActiveRecord::Base.logger = Logger.new(STDOUT)
34
+ # ActiveRecord::Base.logger = Logger.new($stdout)
30
35
 
31
36
  ActiveRecord::Schema.define do
32
37
  create_table :posts, force: true do |t|
@@ -70,8 +75,9 @@ class User < ActiveRecord::Base
70
75
  has_many :comments
71
76
  end
72
77
 
78
+ # --- Alba serializers ---
79
+
73
80
  require "alba"
74
- Alba.backend = :oj
75
81
 
76
82
  class AlbaCommentResource
77
83
  include ::Alba::Resource
@@ -81,17 +87,55 @@ end
81
87
  class AlbaPostResource
82
88
  include ::Alba::Resource
83
89
  attributes :id, :body
84
- many :comments, resource: AlbaCommentResource
85
90
  attribute :commenter_names do |post|
86
91
  post.commenters.pluck(:name)
87
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
88
102
  end
89
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
+
90
133
  require "jbuilder"
134
+
91
135
  class Post
92
136
  def to_builder
93
137
  Jbuilder.new do |post|
94
- post.call(self, :id, :body, :comments, :commenter_names)
138
+ post.call(self, :id, :body, :commenter_names, :comments)
95
139
  end
96
140
  end
97
141
 
@@ -108,35 +152,69 @@ class Comment
108
152
  end
109
153
  end
110
154
 
111
- require "active_model_serializers"
155
+ # --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
112
156
 
113
- class AMSCommentSerializer < ActiveModel::Serializer
114
- attributes :id, :body
157
+ class JsonApiStandardCommentSerializer
158
+ include JSONAPI::Serializer
159
+
160
+ attribute :id
161
+ attribute :body
115
162
  end
116
163
 
117
- class AMSPostSerializer < ActiveModel::Serializer
118
- attributes :id, :body
119
- has_many :comments, serializer: AMSCommentSerializer
164
+ class JsonApiStandardPostSerializer
165
+ include JSONAPI::Serializer
166
+
167
+ # set_type :post # optional
168
+ attribute :id
169
+ attribute :body
120
170
  attribute :commenter_names
121
- def commenter_names
122
- object.commenters.pluck(:name)
171
+
172
+ attribute :comments do |post|
173
+ post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
123
174
  end
124
175
  end
125
176
 
126
- require "blueprinter"
177
+ # --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
127
178
 
128
- class CommentBlueprint < Blueprinter::Base
129
- fields :id, :body
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
130
199
  end
131
200
 
132
- class PostBlueprint < Blueprinter::Base
133
- fields :id, :body, :commenter_names
134
- association :comments, blueprint: CommentBlueprint
135
- def commenter_names
136
- commenters.pluck(:name)
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) }
137
213
  end
138
214
  end
139
215
 
216
+ # --- Representable serializers ---
217
+
140
218
  require "representable"
141
219
 
142
220
  class CommentRepresenter < Representable::Decorator
@@ -159,6 +237,8 @@ class PostRepresenter < Representable::Decorator
159
237
  end
160
238
  end
161
239
 
240
+ # --- Test data creation ---
241
+
162
242
  post = Post.create!(body: 'post')
163
243
  user1 = User.create!(name: 'John')
164
244
  user2 = User.create!(name: 'Jane')
@@ -166,12 +246,9 @@ post.comments.create!(commenter: user1, body: 'Comment1')
166
246
  post.comments.create!(commenter: user2, body: 'Comment2')
167
247
  post.reload
168
248
 
249
+ # --- Store the serializers in procs ---
250
+
169
251
  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
252
  alba_inline = Proc.new do
176
253
  Alba.serialize(post) do
177
254
  attributes :id, :body
@@ -183,16 +260,56 @@ alba_inline = Proc.new do
183
260
  end
184
261
  end
185
262
  end
186
- [alba, jbuilder, ams, rails, blueprinter, representable, alba_inline].each {|x| puts x.call }
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 ---
187
287
 
188
288
  require 'benchmark'
189
289
  time = 1000
190
290
  Benchmark.bmbm do |x|
191
291
  x.report(:alba) { time.times(&alba) }
192
- x.report(:jbuilder) { time.times(&jbuilder) }
292
+ x.report(:alba_inline) { time.times(&alba_inline) }
193
293
  x.report(:ams) { time.times(&ams) }
194
- x.report(:rails) { time.times(&rails) }
195
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) }
196
299
  x.report(:representable) { time.times(&representable) }
197
- x.report(:alba_inline) { time.times(&alba_inline) }
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!
198
315
  end
data/codecov.yml ADDED
@@ -0,0 +1,5 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ informational: true
data/gemfiles/all.gemfile CHANGED
@@ -1,6 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'activesupport', require: false # For backend
4
+ gem 'ffaker', require: false # For testing
4
5
  gem 'minitest', '~> 5.14' # For test
5
6
  gem 'rake', '~> 13.0' # For test and automation
6
7
  gem 'rubocop', '>= 0.79.0', require: false # For lint
data/lib/alba.rb CHANGED
@@ -70,8 +70,10 @@ module Alba
70
70
 
71
71
  def set_encoder
72
72
  @encoder = case @backend
73
- when :oj
73
+ when :oj, :oj_strict
74
74
  try_oj
75
+ when :oj_rails
76
+ try_oj(mode: :rails)
75
77
  when :active_support
76
78
  try_active_support
77
79
  when nil, :default, :json
@@ -81,9 +83,9 @@ module Alba
81
83
  end
82
84
  end
83
85
 
84
- def try_oj
86
+ def try_oj(mode: :strict)
85
87
  require 'oj'
86
- ->(hash) { Oj.dump(hash, mode: :strict) }
88
+ ->(hash) { Oj.dump(hash, mode: mode) }
87
89
  rescue LoadError
88
90
  Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
89
91
  default_encoder
data/lib/alba/many.rb CHANGED
@@ -6,15 +6,16 @@ module Alba
6
6
  # Recursively converts objects into an Array of Hashes
7
7
  #
8
8
  # @param target [Object] the object having an association method
9
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
9
10
  # @param params [Hash] user-given Hash for arbitrary data
10
11
  # @return [Array<Hash>]
11
- def to_hash(target, params: {})
12
+ def to_hash(target, within: nil, params: {})
12
13
  @object = target.public_send(@name)
13
14
  @object = @condition.call(@object, params) if @condition
14
15
  return if @object.nil?
15
16
 
16
17
  @resource = constantize(@resource)
17
- @object.map { |o| @resource.new(o, params: params).to_hash }
18
+ @object.map { |o| @resource.new(o, params: params, within: within).to_hash }
18
19
  end
19
20
  end
20
21
  end
data/lib/alba/one.rb CHANGED
@@ -6,15 +6,16 @@ module Alba
6
6
  # Recursively converts an object into a Hash
7
7
  #
8
8
  # @param target [Object] the object having an association method
9
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
9
10
  # @param params [Hash] user-given Hash for arbitrary data
10
11
  # @return [Hash]
11
- def to_hash(target, params: {})
12
+ def to_hash(target, within: nil, params: {})
12
13
  @object = target.public_send(@name)
13
14
  @object = @condition.call(object, params) if @condition
14
15
  return if @object.nil?
15
16
 
16
17
  @resource = constantize(@resource)
17
- @resource.new(object, params: params).to_hash
18
+ @resource.new(object, params: params, within: within).to_hash
18
19
  end
19
20
  end
20
21
  end
data/lib/alba/resource.rb CHANGED
@@ -28,9 +28,11 @@ module Alba
28
28
 
29
29
  # @param object [Object] the object to be serialized
30
30
  # @param params [Hash] user-given Hash for arbitrary data
31
- def initialize(object, params: {})
31
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
32
+ def initialize(object, params: {}, within: true)
32
33
  @object = object
33
34
  @params = params.freeze
35
+ @within = within
34
36
  DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
35
37
  end
36
38
 
@@ -130,12 +132,32 @@ module Alba
130
132
  when Proc
131
133
  instance_exec(object, &attribute)
132
134
  when Alba::One, Alba::Many
133
- attribute.to_hash(object, params: params)
135
+ within = check_within
136
+ return unless within
137
+
138
+ attribute.to_hash(object, params: params, within: within)
134
139
  else
135
140
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
136
141
  end
137
142
  end
138
143
 
144
+ def check_within
145
+ case @within
146
+ when Hash # Traverse within tree
147
+ @within.fetch(_key.to_sym, nil)
148
+ when Array # within tree ends with Array
149
+ @within.find { |item| item.to_sym == _key.to_sym } # Check if at least one item in the array matches current resource
150
+ when Symbol # within tree could end with Symbol
151
+ @within == _key.to_sym # Check if the symbol matches current resource
152
+ when true # In this case, Alba serializes all associations.
153
+ true
154
+ when nil, false # In these cases, Alba stops serialization here.
155
+ false
156
+ else
157
+ raise Alba::Error, "Unknown type for within option: #{@within.class}"
158
+ end
159
+ end
160
+
139
161
  def collection?
140
162
  @object.is_a?(Enumerable)
141
163
  end
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alba
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OKURA Masafumi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-15 00:00:00.000000000 Z
11
+ date: 2021-04-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Alba is designed to be a simple, easy to use and fast alternative to
14
14
  existing JSON serializers. Its performance is better than almost all gems which
@@ -33,6 +33,7 @@ files:
33
33
  - benchmark/local.rb
34
34
  - bin/console
35
35
  - bin/setup
36
+ - codecov.yml
36
37
  - gemfiles/all.gemfile
37
38
  - gemfiles/without_active_support.gemfile
38
39
  - gemfiles/without_oj.gemfile
@@ -66,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
67
  - !ruby/object:Gem::Version
67
68
  version: '0'
68
69
  requirements: []
69
- rubygems_version: 3.2.14
70
+ rubygems_version: 3.2.16
70
71
  signing_key:
71
72
  specification_version: 4
72
73
  summary: Alba is the fastest JSON serializer for Ruby.