alba 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.