alba 1.6.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.
@@ -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.
@@ -19,7 +19,6 @@ gemfile(true) do
19
19
  gem "jbuilder"
20
20
  gem 'turbostreamer'
21
21
  gem "jserializer"
22
- gem "jsonapi-serializer" # successor of fast_jsonapi
23
22
  gem "multi_json"
24
23
  gem "panko_serializer"
25
24
  gem "pg"
@@ -201,67 +200,6 @@ class JserializerPostSerializer < Jserializer::Base
201
200
  end
202
201
  end
203
202
 
204
- # --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
205
-
206
- class JsonApiStandardCommentSerializer
207
- include JSONAPI::Serializer
208
-
209
- attribute :id
210
- attribute :body
211
- end
212
-
213
- class JsonApiStandardPostSerializer
214
- include JSONAPI::Serializer
215
-
216
- # set_type :post # optional
217
- attribute :id
218
- attribute :body
219
- attribute :commenter_names
220
-
221
- attribute :comments do |post|
222
- post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
223
- end
224
- end
225
-
226
- # --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
227
-
228
- # code to convert from JSON:API output to "flat" JSON, like the other serializers build
229
- class JsonApiSameFormatSerializer
230
- include JSONAPI::Serializer
231
-
232
- def as_json(*_options)
233
- hash = serializable_hash
234
-
235
- if hash[:data].is_a? Hash
236
- hash[:data][:attributes]
237
-
238
- elsif hash[:data].is_a? Array
239
- hash[:data].pluck(:attributes)
240
-
241
- elsif hash[:data].nil?
242
- { }
243
-
244
- else
245
- raise "unexpected data type #{hash[:data].class}"
246
- end
247
- end
248
- end
249
-
250
- class JsonApiSameFormatCommentSerializer < JsonApiSameFormatSerializer
251
- attribute :id
252
- attribute :body
253
- end
254
-
255
- class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
256
- attribute :id
257
- attribute :body
258
- attribute :commenter_names
259
-
260
- attribute :comments do |post|
261
- post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
262
- end
263
- end
264
-
265
203
  # --- Panko serializers ---
266
204
  #
267
205
 
@@ -419,8 +357,6 @@ jbuilder = Proc.new do
419
357
  end.target!
420
358
  end
421
359
  jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
422
- jsonapi = proc { JsonApiStandardPostSerializer.new(posts).to_json }
423
- jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(posts).to_json }
424
360
  panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
425
361
  primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
426
362
  rails = Proc.new do
@@ -441,8 +377,6 @@ puts "Serializer outputs ----------------------------------"
441
377
  fast_serializer: fast_serializer,
442
378
  jbuilder: jbuilder, # different order
443
379
  jserializer: jserializer,
444
- jsonapi: jsonapi, # nested JSON:API format
445
- jsonapi_same_format: jsonapi_same_format,
446
380
  panko: panko,
447
381
  primalize: primalize,
448
382
  rails: rails,
@@ -462,8 +396,6 @@ Benchmark.ips do |x|
462
396
  x.report(:fast_serializer, &fast_serializer)
463
397
  x.report(:jbuilder, &jbuilder)
464
398
  x.report(:jserializer, &jserializer)
465
- x.report(:jsonapi, &jsonapi)
466
- x.report(:jsonapi_same_format, &jsonapi_same_format)
467
399
  x.report(:panko, &panko)
468
400
  x.report(:primalize, &primalize)
469
401
  x.report(:rails, &rails)
@@ -484,8 +416,6 @@ Benchmark.memory do |x|
484
416
  x.report(:fast_serializer, &fast_serializer)
485
417
  x.report(:jbuilder, &jbuilder)
486
418
  x.report(:jserializer, &jserializer)
487
- x.report(:jsonapi, &jsonapi)
488
- x.report(:jsonapi_same_format, &jsonapi_same_format)
489
419
  x.report(:panko, &panko)
490
420
  x.report(:primalize, &primalize)
491
421
  x.report(:rails, &rails)
@@ -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
+ ```
@@ -11,12 +11,16 @@ module Alba
11
11
  # @param name [Symbol, String] name of the method to fetch association
12
12
  # @param condition [Proc, nil] a proc filtering data
13
13
  # @param resource [Class<Alba::Resource>, nil] a resource class for the association
14
+ # @param params [Hash] params override for the association
14
15
  # @param nesting [String] a namespace where source class is inferred with
16
+ # @param key_transformation [Symbol] key transformation type
15
17
  # @param block [Block] used to define resource when resource arg is absent
16
- 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)
17
19
  @name = name
18
20
  @condition = condition
19
21
  @resource = resource
22
+ @params = params
23
+ @key_transformation = key_transformation
20
24
  return if @resource
21
25
 
22
26
  assign_resource(nesting, block)
@@ -29,12 +33,16 @@ module Alba
29
33
  # @param params [Hash] user-given Hash for arbitrary data
30
34
  # @return [Hash]
31
35
  def to_h(target, within: nil, params: {})
32
- @object = target.public_send(@name)
33
- @object = @condition.call(object, params) if @condition
36
+ params = params.merge(@params) unless @params.empty?
37
+ @object = target.__send__(@name)
38
+ @object = @condition.call(object, params, target) if @condition
34
39
  return if @object.nil?
35
40
 
36
- @resource = constantize(@resource)
37
- @resource.new(object, params: params, within: within).to_h
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
38
46
  end
39
47
 
40
48
  private
@@ -59,5 +67,17 @@ module Alba
59
67
  raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
60
68
  end
61
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
62
82
  end
63
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
@@ -1,54 +1,25 @@
1
+ begin
2
+ require 'active_support/inflector'
3
+ require 'active_support/core_ext/module/delegation'
4
+ rescue LoadError
5
+ raise ::Alba::Error, 'To use default inflector, please install `ActiveSupport` gem.'
6
+ end
7
+
1
8
  module Alba
2
9
  # This module has two purposes.
3
10
  # One is that we require `active_support/inflector` in this module so that we don't do that all over the place.
4
11
  # Another is that `ActiveSupport::Inflector` doesn't have `camelize_lower` method that we want it to have, so this module works as an adapter.
5
12
  module DefaultInflector
6
- begin
7
- require 'active_support/inflector'
8
- rescue LoadError
9
- raise ::Alba::Error, 'To use transform_keys, please install `ActiveSupport` gem.'
10
- end
11
-
12
- module_function
13
-
14
- # Camelizes a key
15
- #
16
- # @param key [String] key to be camelized
17
- # @return [String] camelized key
18
- def camelize(key)
19
- ActiveSupport::Inflector.camelize(key)
13
+ class << self
14
+ delegate :camelize, :dasherize, :underscore, :classify, :demodulize, :pluralize, to: ActiveSupport::Inflector
20
15
  end
21
16
 
22
17
  # Camelizes a key, 1st letter lowercase
23
18
  #
24
19
  # @param key [String] key to be camelized
25
20
  # @return [String] camelized key
26
- def camelize_lower(key)
21
+ def self.camelize_lower(key)
27
22
  ActiveSupport::Inflector.camelize(key, false)
28
23
  end
29
-
30
- # Dasherizes a key
31
- #
32
- # @param key [String] key to be dasherized
33
- # @return [String] dasherized key
34
- def dasherize(key)
35
- ActiveSupport::Inflector.dasherize(key)
36
- end
37
-
38
- # Underscore a key
39
- #
40
- # @param key [String] key to be underscore
41
- # @return [String] underscored key
42
- def underscore(key)
43
- ActiveSupport::Inflector.underscore(key)
44
- end
45
-
46
- # Classify a key
47
- #
48
- # @param key [String] key to be classified
49
- # @return [String] classified key
50
- def classify(key)
51
- ActiveSupport::Inflector.classify(key)
52
- end
53
24
  end
54
25
  end
@@ -0,0 +1,67 @@
1
+ require 'erb'
2
+ require 'forwardable'
3
+
4
+ module Alba
5
+ # Layout serialization
6
+ class Layout
7
+ extend Forwardable
8
+
9
+ def_delegators :@resource, :object, :params, :serializable_hash, :to_h
10
+
11
+ # @params file [String] name of the layout file
12
+ # @params inline [Proc] a proc returning JSON string or a Hash representing JSON
13
+ def initialize(file:, inline:)
14
+ if file
15
+ raise ArgumentError, 'File layout must be a String representing filename' unless file.is_a?(String)
16
+
17
+ @body = file
18
+ elsif inline
19
+ raise ArgumentError, 'Inline layout must be a Proc returning a Hash or a String' unless inline.is_a?(Proc)
20
+
21
+ @body = inline
22
+ else
23
+ raise ArgumentError, 'Layout must be either String or Proc'
24
+ end
25
+ end
26
+
27
+ # Serialize within layout
28
+ #
29
+ # @param resource [Alba::Resource] the original resource calling this layout
30
+ # @param serialized_json [String] JSON string for embedding
31
+ # @param binding [Binding] context for serialization
32
+ def serialize(resource:, serialized_json:, binding:)
33
+ @resource = resource
34
+ @serialized_json = serialized_json
35
+
36
+ if @body.is_a?(String)
37
+ serialize_within_string_layout(binding)
38
+ else
39
+ serialize_within_inline_layout
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :serialized_json
46
+
47
+ def serialize_within_string_layout(bnd)
48
+ ERB.new(File.read(@body)).result(bnd)
49
+ end
50
+
51
+ def serialize_within_inline_layout
52
+ inline = instance_eval(&@body)
53
+ case inline
54
+ when Hash then encode(inline)
55
+ when String then inline
56
+ else
57
+ raise Alba::Error, 'Inline layout must be a Proc returning a Hash or a String'
58
+ end
59
+ end
60
+
61
+ # This methods exists here instead of delegation because
62
+ # `Alba::Resource#encode` is private and it prints warning if we use `def_delegators`
63
+ def encode(hash)
64
+ @resource.instance_eval { encode(hash) }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,18 @@
1
+ module Alba
2
+ # Representing nested attribute
3
+ class NestedAttribute
4
+ # @param key_transformation [Symbol] determines how to transform keys
5
+ # @param block [Proc] class body
6
+ def initialize(key_transformation: :none, &block)
7
+ @key_transformation = key_transformation
8
+ @block = block
9
+ end
10
+
11
+ # @return [Hash]
12
+ def value(object)
13
+ resource_class = Alba.resource_class(&@block)
14
+ resource_class.transform_keys(@key_transformation)
15
+ resource_class.new(object).serializable_hash
16
+ end
17
+ end
18
+ end