alba 1.5.0 → 2.0.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.
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