alba 1.6.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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