alba 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
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?
4
+ ENV["BUNDLE_GEMFILE"] = File.expand_path("Gemfile") if ENV["BUNDLE_GEMFILE"] == File.expand_path("Gemfile") || ENV["BUNDLE_GEMFILE"].empty? || ENV["BUNDLE_GEMFILE"].nil?
5
5
 
6
6
  Rake::TestTask.new(:test) do |t|
7
7
  t.libs << "test"
8
8
  t.libs << "lib"
9
- file_list = ENV["BUNDLE_GEMFILE"] == File.expand_path("gemfiles/all.gemfile") ? FileList["test/**/*_test.rb"] : FileList["test/dependencies/test_dependencies.rb"]
9
+ file_list = ENV["BUNDLE_GEMFILE"] == File.expand_path("Gemfile") ? FileList["test/**/*_test.rb"] : FileList["test/dependencies/test_dependencies.rb"]
10
10
  t.test_files = file_list
11
11
  end
12
12
 
data/alba.gemspec CHANGED
@@ -10,11 +10,15 @@ Gem::Specification.new do |spec|
10
10
  spec.description = "Alba is the fastest JSON serializer for Ruby. It focuses on performance, flexibility and usability."
11
11
  spec.homepage = 'https://github.com/okuramasafumi/alba'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
14
14
 
15
- spec.metadata['homepage_uri'] = spec.homepage
16
- spec.metadata['source_code_uri'] = 'https://github.com/okuramasafumi/alba'
17
- spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md'
15
+ spec.metadata = {
16
+ 'bug_tracker_uri' => 'https://github.com/okuramasafumi/issues',
17
+ 'changelog_uri' => 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md',
18
+ 'documentation_uri' => 'https://rubydoc.info/github/okuramasafumi/alba',
19
+ 'source_code_uri' => 'https://github.com/okuramasafumi/alba',
20
+ 'rubygems_mfa_required' => 'true'
21
+ }
18
22
 
19
23
  # Specify which files should be added to the gem when it is released.
20
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -0,0 +1,81 @@
1
+ ## Benchmark for json serializers
2
+
3
+ This directory contains a few different benchmark scripts. They all use inline Bundler definitions so you can run them by `ruby benchmark/collection.rb` for instance.
4
+
5
+ ## Result
6
+
7
+ As a reference, here's the benchmark result run in my (@okuramasafumi) machine.
8
+
9
+ Machine spec:
10
+
11
+ |Key|Value|
12
+ |---|---|
13
+ |OS|macOS 12.2.1|
14
+ |CPU|Intel Corei7 Quad Core 2.3Ghz|
15
+ |RAM|32GB|
16
+ |Ruby|ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]|
17
+
18
+ `benchmark-ips` with `Oj.optimize_rails`:
19
+
20
+ ```
21
+ Comparison:
22
+ panko: 267.6 i/s
23
+ rails: 111.2 i/s - 2.41x (± 0.00) slower
24
+ jserializer: 106.2 i/s - 2.52x (± 0.00) slower
25
+ alba: 102.8 i/s - 2.60x (± 0.00) slower
26
+ turbostreamer: 99.9 i/s - 2.68x (± 0.00) slower
27
+ jbuilder: 90.7 i/s - 2.95x (± 0.00) slower
28
+ alba_inline: 90.0 i/s - 2.97x (± 0.00) slower
29
+ primalize: 82.1 i/s - 3.26x (± 0.00) slower
30
+ fast_serializer: 62.7 i/s - 4.27x (± 0.00) slower
31
+ jsonapi_same_format: 59.5 i/s - 4.50x (± 0.00) slower
32
+ jsonapi: 55.3 i/s - 4.84x (± 0.00) slower
33
+ blueprinter: 54.1 i/s - 4.95x (± 0.00) slower
34
+ representable: 35.9 i/s - 7.46x (± 0.00) slower
35
+ simple_ams: 25.4 i/s - 10.53x (± 0.00) slower
36
+ ams: 9.1 i/s - 29.39x (± 0.00) slower
37
+ ```
38
+
39
+ `benchmark-ips` without `Oj.optimize_rails`:
40
+
41
+ ```
42
+ Comparison:
43
+ panko: 283.8 i/s
44
+ turbostreamer: 102.9 i/s - 2.76x (± 0.00) slower
45
+ alba: 102.4 i/s - 2.77x (± 0.00) slower
46
+ alba_inline: 98.7 i/s - 2.87x (± 0.00) slower
47
+ jserializer: 93.3 i/s - 3.04x (± 0.00) slower
48
+ fast_serializer: 60.1 i/s - 4.73x (± 0.00) slower
49
+ blueprinter: 53.8 i/s - 5.28x (± 0.00) slower
50
+ rails: 37.1 i/s - 7.65x (± 0.00) slower
51
+ jbuilder: 37.1 i/s - 7.66x (± 0.00) slower
52
+ primalize: 31.2 i/s - 9.08x (± 0.00) slower
53
+ jsonapi_same_format: 28.3 i/s - 10.03x (± 0.00) slower
54
+ representable: 27.8 i/s - 10.23x (± 0.00) slower
55
+ jsonapi: 27.5 i/s - 10.34x (± 0.00) slower
56
+ simple_ams: 16.9 i/s - 16.75x (± 0.00) slower
57
+ ams: 8.3 i/s - 34.36x (± 0.00) slower
58
+ ```
59
+
60
+ `benchmark-memory`:
61
+
62
+ ```
63
+ Comparison:
64
+ panko: 230418 allocated
65
+ alba: 733217 allocated - 3.18x more
66
+ alba_inline: 748297 allocated - 3.25x more
67
+ turbostreamer: 781008 allocated - 3.39x more
68
+ jserializer: 819705 allocated - 3.56x more
69
+ primalize: 1195163 allocated - 5.19x more
70
+ fast_serializer: 1232385 allocated - 5.35x more
71
+ rails: 1236761 allocated - 5.37x more
72
+ blueprinter: 1588937 allocated - 6.90x more
73
+ jbuilder: 1774157 allocated - 7.70x more
74
+ jsonapi_same_format: 2132489 allocated - 9.25x more
75
+ jsonapi: 2279958 allocated - 9.89x more
76
+ representable: 2869166 allocated - 12.45x more
77
+ ams: 4473161 allocated - 19.41x more
78
+ simple_ams: 7868345 allocated - 34.15x more
79
+ ```
80
+
81
+ Conclusion: panko is extremely fast but it's a C extension gem. As pure Ruby gems, Alba, turbostreamer and jserializer are notably faster than others. With `Oj.optimize_rails` jbuilder and Rails standard serialization are also fast.
@@ -15,9 +15,10 @@ gemfile(true) do
15
15
  gem "benchmark-ips"
16
16
  gem "benchmark-memory"
17
17
  gem "blueprinter"
18
+ gem "fast_serializer_ruby"
18
19
  gem "jbuilder"
20
+ gem 'turbostreamer'
19
21
  gem "jserializer"
20
- gem "jsonapi-serializer" # successor of fast_jsonapi
21
22
  gem "multi_json"
22
23
  gem "panko_serializer"
23
24
  gem "pg"
@@ -138,6 +139,27 @@ class PostBlueprint < Blueprinter::Base
138
139
  end
139
140
  end
140
141
 
142
+ # --- Fast Serializer Ruby
143
+
144
+ require "fast_serializer"
145
+
146
+ class FastSerializerCommentResource
147
+ include ::FastSerializer::Schema::Mixin
148
+ attributes :id, :body
149
+ end
150
+
151
+ class FastSerializerPostResource
152
+ include ::FastSerializer::Schema::Mixin
153
+
154
+ attributes :id, :body
155
+
156
+ attribute :commenter_names do
157
+ object.commenters.pluck(:name)
158
+ end
159
+
160
+ has_many :comments, serializer: FastSerializerCommentResource
161
+ end
162
+
141
163
  # --- JBuilder serializers ---
142
164
 
143
165
  require "jbuilder"
@@ -178,67 +200,6 @@ class JserializerPostSerializer < Jserializer::Base
178
200
  end
179
201
  end
180
202
 
181
- # --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
182
-
183
- class JsonApiStandardCommentSerializer
184
- include JSONAPI::Serializer
185
-
186
- attribute :id
187
- attribute :body
188
- end
189
-
190
- class JsonApiStandardPostSerializer
191
- include JSONAPI::Serializer
192
-
193
- # set_type :post # optional
194
- attribute :id
195
- attribute :body
196
- attribute :commenter_names
197
-
198
- attribute :comments do |post|
199
- post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
200
- end
201
- end
202
-
203
- # --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
204
-
205
- # code to convert from JSON:API output to "flat" JSON, like the other serializers build
206
- class JsonApiSameFormatSerializer
207
- include JSONAPI::Serializer
208
-
209
- def as_json(*_options)
210
- hash = serializable_hash
211
-
212
- if hash[:data].is_a? Hash
213
- hash[:data][:attributes]
214
-
215
- elsif hash[:data].is_a? Array
216
- hash[:data].pluck(:attributes)
217
-
218
- elsif hash[:data].nil?
219
- { }
220
-
221
- else
222
- raise "unexpected data type #{hash[:data].class}"
223
- end
224
- end
225
- end
226
-
227
- class JsonApiSameFormatCommentSerializer < JsonApiSameFormatSerializer
228
- attribute :id
229
- attribute :body
230
- end
231
-
232
- class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
233
- attribute :id
234
- attribute :body
235
- attribute :commenter_names
236
-
237
- attribute :comments do |post|
238
- post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
239
- end
240
- end
241
-
242
203
  # --- Panko serializers ---
243
204
  #
244
205
 
@@ -255,7 +216,7 @@ class PankoPostSerializer < Panko::Serializer
255
216
  has_many :comments, serializer: PankoCommentSerializer
256
217
 
257
218
  def commenter_names
258
- object.comments.pluck(:name)
219
+ object.commenters.pluck(:name)
259
220
  end
260
221
  end
261
222
 
@@ -332,6 +293,31 @@ class SimpleAMSPostSerializer
332
293
  end
333
294
  end
334
295
 
296
+ require 'turbostreamer'
297
+ TurboStreamer.set_default_encoder(:json, :oj)
298
+
299
+ class TurbostreamerSerializer
300
+ def initialize(posts)
301
+ @posts = posts
302
+ end
303
+
304
+ def to_json
305
+ TurboStreamer.encode do |json|
306
+ json.array! @posts do |post|
307
+ json.object! do
308
+ json.extract! post, :id, :body, :commenter_names
309
+
310
+ json.comments post.comments do |comment|
311
+ json.object! do
312
+ json.extract! comment, :id, :body
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+
335
321
  # --- Test data creation ---
336
322
 
337
323
  100.times do |i|
@@ -344,7 +330,7 @@ end
344
330
  end
345
331
  end
346
332
 
347
- posts = Post.all.to_a
333
+ posts = Post.all.includes(:comments, :commenters)
348
334
 
349
335
  # --- Store the serializers in procs ---
350
336
 
@@ -360,8 +346,9 @@ alba_inline = Proc.new do
360
346
  end
361
347
  end
362
348
  end
363
- ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {}).as_json }
349
+ ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {each_serializer: AMSPostSerializer}).to_json }
364
350
  blueprinter = Proc.new { PostBlueprint.render(posts) }
351
+ fast_serializer = Proc.new { FastSerializerPostResource.new(posts).to_json }
365
352
  jbuilder = Proc.new do
366
353
  Jbuilder.new do |json|
367
354
  json.array!(posts) do |post|
@@ -370,8 +357,6 @@ jbuilder = Proc.new do
370
357
  end.target!
371
358
  end
372
359
  jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
373
- jsonapi = proc { JsonApiStandardPostSerializer.new(posts).to_json }
374
- jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(posts).to_json }
375
360
  panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
376
361
  primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
377
362
  rails = Proc.new do
@@ -379,24 +364,25 @@ rails = Proc.new do
379
364
  end
380
365
  representable = Proc.new { PostsRepresenter.new(posts).to_json }
381
366
  simple_ams = Proc.new { SimpleAMS::Renderer::Collection.new(posts, serializer: SimpleAMSPostSerializer).to_json }
367
+ turbostreamer = Proc.new { TurbostreamerSerializer.new(posts).to_json }
382
368
 
383
369
  # --- Execute the serializers to check their output ---
384
-
370
+ GC.disable
385
371
  puts "Serializer outputs ----------------------------------"
386
372
  {
387
373
  alba: alba,
388
374
  alba_inline: alba_inline,
389
375
  ams: ams,
390
376
  blueprinter: blueprinter,
377
+ fast_serializer: fast_serializer,
391
378
  jbuilder: jbuilder, # different order
392
379
  jserializer: jserializer,
393
- jsonapi: jsonapi, # nested JSON:API format
394
- jsonapi_same_format: jsonapi_same_format,
395
380
  panko: panko,
396
381
  primalize: primalize,
397
382
  rails: rails,
398
383
  representable: representable,
399
384
  simple_ams: simple_ams,
385
+ turbostreamer: turbostreamer
400
386
  }.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
401
387
 
402
388
  # --- Run the benchmarks ---
@@ -407,15 +393,15 @@ Benchmark.ips do |x|
407
393
  x.report(:alba_inline, &alba_inline)
408
394
  x.report(:ams, &ams)
409
395
  x.report(:blueprinter, &blueprinter)
396
+ x.report(:fast_serializer, &fast_serializer)
410
397
  x.report(:jbuilder, &jbuilder)
411
398
  x.report(:jserializer, &jserializer)
412
- x.report(:jsonapi, &jsonapi)
413
- x.report(:jsonapi_same_format, &jsonapi_same_format)
414
399
  x.report(:panko, &panko)
415
400
  x.report(:primalize, &primalize)
416
401
  x.report(:rails, &rails)
417
402
  x.report(:representable, &representable)
418
403
  x.report(:simple_ams, &simple_ams)
404
+ x.report(:turbostreamer, &turbostreamer)
419
405
 
420
406
  x.compare!
421
407
  end
@@ -427,15 +413,15 @@ Benchmark.memory do |x|
427
413
  x.report(:alba_inline, &alba_inline)
428
414
  x.report(:ams, &ams)
429
415
  x.report(:blueprinter, &blueprinter)
416
+ x.report(:fast_serializer, &fast_serializer)
430
417
  x.report(:jbuilder, &jbuilder)
431
418
  x.report(:jserializer, &jserializer)
432
- x.report(:jsonapi, &jsonapi)
433
- x.report(:jsonapi_same_format, &jsonapi_same_format)
434
419
  x.report(:panko, &panko)
435
420
  x.report(:primalize, &primalize)
436
421
  x.report(:rails, &rails)
437
422
  x.report(:representable, &representable)
438
423
  x.report(:simple_ams, &simple_ams)
424
+ x.report(:turbostreamer, &turbostreamer)
439
425
 
440
426
  x.compare!
441
427
  end
@@ -16,6 +16,7 @@ gemfile(true) do
16
16
  gem "benchmark-memory"
17
17
  gem "blueprinter"
18
18
  gem "jbuilder"
19
+ gem 'turbostreamer'
19
20
  gem "jserializer"
20
21
  gem "jsonapi-serializer" # successor of fast_jsonapi
21
22
  gem "multi_json"
@@ -253,7 +254,7 @@ class PankoPostSerializer < Panko::Serializer
253
254
  has_many :comments, serializer: PankoCommentSerializer
254
255
 
255
256
  def commenter_names
256
- object.comments.pluck(:name)
257
+ object.commenters.pluck(:name)
257
258
  end
258
259
  end
259
260
 
@@ -324,6 +325,29 @@ class SimpleAMSPostSerializer
324
325
  end
325
326
  end
326
327
 
328
+ require 'turbostreamer'
329
+ TurboStreamer.set_default_encoder(:json, :oj)
330
+
331
+ class TurbostreamerSerializer
332
+ def initialize(post)
333
+ @post = post
334
+ end
335
+
336
+ def to_json
337
+ TurboStreamer.encode do |json|
338
+ json.object! do
339
+ json.extract! @post, :id, :body, :commenter_names
340
+
341
+ json.comments @post.comments do |comment|
342
+ json.object! do
343
+ json.extract! comment, :id, :body
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
350
+
327
351
  # --- Test data creation ---
328
352
 
329
353
  post = Post.create!(body: 'post')
@@ -347,6 +371,7 @@ alba_inline = Proc.new do
347
371
  end
348
372
  end
349
373
  end
374
+
350
375
  ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
351
376
  blueprinter = Proc.new { PostBlueprint.render(post) }
352
377
  jbuilder = Proc.new { post.to_builder.target! }
@@ -358,6 +383,7 @@ primalize = proc { PrimalizePostResource.new(post).to_json }
358
383
  rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
359
384
  representable = Proc.new { PostRepresenter.new(post).to_json }
360
385
  simple_ams = Proc.new { SimpleAMS::Renderer.new(post, serializer: SimpleAMSPostSerializer).to_json }
386
+ turbostreamer = Proc.new { TurbostreamerSerializer.new(post).to_json }
361
387
 
362
388
  # --- Execute the serializers to check their output ---
363
389
 
@@ -376,7 +402,10 @@ puts "Serializer outputs ----------------------------------"
376
402
  rails: rails,
377
403
  representable: representable,
378
404
  simple_ams: simple_ams,
379
- }.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
405
+ turbostreamer: turbostreamer
406
+ }.each do |name, serializer|
407
+ puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}"
408
+ end
380
409
 
381
410
  # --- Run the benchmarks ---
382
411
 
@@ -395,6 +424,7 @@ Benchmark.ips do |x|
395
424
  x.report(:rails, &rails)
396
425
  x.report(:representable, &representable)
397
426
  x.report(:simple_ams, &simple_ams)
427
+ x.report(:turbostreamer, &turbostreamer)
398
428
 
399
429
  x.compare!
400
430
  end
@@ -415,6 +445,7 @@ Benchmark.memory do |x|
415
445
  x.report(:rails, &rails)
416
446
  x.report(:representable, &representable)
417
447
  x.report(:simple_ams, &simple_ams)
448
+ x.report(:turbostreamer, &turbostreamer)
418
449
 
419
450
  x.compare!
420
451
  end
@@ -4,11 +4,12 @@ title: Upgrading from Jbuilder
4
4
 
5
5
  <!-- @format -->
6
6
 
7
- This guide is aimed at helping Jbuilder users transition to Alba, and it consists of three parts:
7
+ This guide is aimed at helping Jbuilder users transition to Alba, and it consists of four parts:
8
8
 
9
9
  1. Basic serialization
10
10
  2. Complex serialization
11
- 3. Unsupported features
11
+ 3. Key transformation
12
+ 4. Unsupported features
12
13
 
13
14
  ## Example class
14
15
 
@@ -216,8 +217,21 @@ UserResource.new(user).serialize
216
217
  # => '{"user":{"id":id, "created_at": created_at, "updated_at": updated_at, "profile": {"email": email}, articles: [{"title": title, "body": body}]}'
217
218
  ```
218
219
 
219
- ## 3. Unsupported features
220
+ ## 3. Key transformation
221
+
222
+ See the README for more information, but it's possible to migrate the `Jbuilder.key_format!` behavior with the `transform_keys` macro.
223
+
224
+ ```rb
225
+ class UserResource
226
+ include Alba::Resource
227
+ root_key :user
228
+ attributes :id, :created_at, :updated_at
229
+
230
+ transform_keys :lower_camel
231
+ end
232
+ ```
233
+
234
+ ## 4. Unsupported features
220
235
 
221
236
  - Jbuilder#ignore_nil!
222
237
  - Jbuilder#cache!
223
- - Jbuilder.key_format! and Jbuilder.deep_format_keys!
data/docs/rails.md ADDED
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Alba for Rails
3
+ author: OKURA Masafumi
4
+ ---
5
+
6
+ # Alba for Rails
7
+
8
+ While Alba is NOT designed for Rails specifically, you can definitely use Alba with Rails. This document describes in detail how to use Alba with Rails to be more productive.
9
+
10
+ ## Initializer
11
+
12
+ You might want to add some configurations to initializer file such as `alba.rb` with something like below:
13
+
14
+ ```ruby
15
+ # alba.rb
16
+ Alba.backend = :active_support
17
+ Alba.enable_inference!(:active_support)
18
+ ```
19
+
20
+ You can also use `:oj_rails` for backend if you prefer using Oj.
21
+
22
+ ## Rendering JSON
23
+
24
+ You can render JSON with Rails in two ways. One way is to pass JSON String.
25
+
26
+ ```ruby
27
+ render json: FooResource.new(foo).serialize
28
+ ```
29
+
30
+ But you can also render JSON passing `Alba::Resource` object. Rails automatically calls `to_json` on a resource.
31
+
32
+ ```ruby
33
+ render json: FooResource.new(foo)
34
+ ```
35
+
36
+ Note that almost all options given to this `render` are ignored. The only exceptions are `layout`, `prefixes`, `template` and `status`.
37
+
38
+ ```ruby
39
+ # This `only` option is ignored
40
+ render json: FooResource.new(foo), only: [:id]
41
+
42
+ # This is OK
43
+ render json: FooResource.new(foo), status: 200
44
+ ```
@@ -1,22 +1,48 @@
1
1
  module Alba
2
- # Base class for `One` and `Many`
3
- # Child class should implement `to_hash` method
2
+ # Representing association
4
3
  class Association
4
+ @const_cache = {}
5
+ class << self
6
+ attr_reader :const_cache
7
+ end
8
+
5
9
  attr_reader :object, :name
6
10
 
7
11
  # @param name [Symbol, String] name of the method to fetch association
8
12
  # @param condition [Proc, nil] a proc filtering data
9
13
  # @param resource [Class<Alba::Resource>, nil] a resource class for the association
14
+ # @param params [Hash] params override for the association
10
15
  # @param nesting [String] a namespace where source class is inferred with
16
+ # @param key_transformation [Symbol] key transformation type
11
17
  # @param block [Block] used to define resource when resource arg is absent
12
- def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
18
+ def initialize(name:, condition: nil, resource: nil, params: {}, nesting: nil, key_transformation: :none, &block)
13
19
  @name = name
14
20
  @condition = condition
15
- @block = block
16
21
  @resource = resource
22
+ @params = params
23
+ @key_transformation = key_transformation
17
24
  return if @resource
18
25
 
19
- assign_resource(nesting)
26
+ assign_resource(nesting, block)
27
+ end
28
+
29
+ # Recursively converts an object into a Hash
30
+ #
31
+ # @param target [Object] the object having an association method
32
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
33
+ # @param params [Hash] user-given Hash for arbitrary data
34
+ # @return [Hash]
35
+ def to_h(target, within: nil, params: {})
36
+ params = params.merge(@params) unless @params.empty?
37
+ @object = target.__send__(@name)
38
+ @object = @condition.call(object, params, target) if @condition
39
+ return if @object.nil?
40
+
41
+ if @resource.is_a?(Proc) && @object.is_a?(Enumerable)
42
+ to_h_with_each_resource(within, params)
43
+ else
44
+ to_h_with_constantize_resource(within, params)
45
+ end
20
46
  end
21
47
 
22
48
  private
@@ -26,18 +52,32 @@ module Alba
26
52
  when Class
27
53
  resource
28
54
  when Symbol, String
29
- Object.const_get(resource)
55
+ self.class.const_cache.fetch(resource) do
56
+ self.class.const_cache[resource] = Object.const_get(resource)
57
+ end
30
58
  end
31
59
  end
32
60
 
33
- def assign_resource(nesting)
34
- @resource = if @block
35
- Alba.resource_class(&@block)
61
+ def assign_resource(nesting, block)
62
+ @resource = if block
63
+ Alba.resource_class(&block)
36
64
  elsif Alba.inferring
37
65
  Alba.infer_resource_class(@name, nesting: nesting)
38
66
  else
39
67
  raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
40
68
  end
41
69
  end
70
+
71
+ def to_h_with_each_resource(within, params)
72
+ @object.map do |item|
73
+ @resource.call(item).new(item, within: within, params: params).to_h
74
+ end
75
+ end
76
+
77
+ def to_h_with_constantize_resource(within, params)
78
+ @resource = constantize(@resource)
79
+ @resource.transform_keys(@key_transformation)
80
+ @resource.new(object, params: params, within: within).to_h
81
+ end
42
82
  end
43
83
  end
@@ -0,0 +1,54 @@
1
+ module Alba
2
+ # Represents attribute with `if` option
3
+ class ConditionalAttribute
4
+ CONDITION_UNMET = Object.new.freeze
5
+ public_constant :CONDITION_UNMET # It's public for use in `Alba::Resource`
6
+
7
+ # @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute] real attribute wrapped with condition
8
+ # @param condition [Symbol, Proc] condition to check
9
+ def initialize(body:, condition:)
10
+ @body = body
11
+ @condition = condition
12
+ end
13
+
14
+ # Returns attribute body if condition passes
15
+ #
16
+ # @param resource [Alba::Resource]
17
+ # @param object [Object] needed for collection, each object from collection
18
+ # @return [ConditionalAttribute::CONDITION_UNMET, Object] CONDITION_UNMET if condition is unmet, fetched attribute otherwise
19
+ def with_passing_condition(resource:, object: nil)
20
+ return CONDITION_UNMET unless condition_passes?(resource, object)
21
+
22
+ fetched_attribute = yield(@body)
23
+ return fetched_attribute if fetched_attribute.nil? || !with_two_arity_proc_condition
24
+
25
+ return CONDITION_UNMET unless resource.instance_exec(object, attribute_from_association_body_or(fetched_attribute), &@condition)
26
+
27
+ fetched_attribute
28
+ end
29
+
30
+ private
31
+
32
+ def condition_passes?(resource, object)
33
+ if @condition.is_a?(Proc)
34
+ arity = @condition.arity
35
+ # We can return early to skip fetch_attribute if arity is 1
36
+ # When arity is 2, we check the condition later
37
+ return true if arity >= 2
38
+ return false if arity <= 1 && !resource.instance_exec(object, &@condition)
39
+
40
+ true
41
+ else # Symbol
42
+ resource.__send__(@condition)
43
+ end
44
+ end
45
+
46
+ def with_two_arity_proc_condition
47
+ @condition.is_a?(Proc) && @condition.arity >= 2
48
+ end
49
+
50
+ def attribute_from_association_body_or(fetched_attribute)
51
+ @body.is_a?(Alba::Association) ? @body.object : fetched_attribute
52
+ end
53
+ end
54
+ end