alba 1.3.0 → 1.4.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: b2c2e8c84ddccf4db9f9f18dd155361567f2a1733c55f817f5b4d97d573675d5
4
- data.tar.gz: 3f11dd120c8b57aef909d79f6570482b8af810cd19c57ddcaa0d7b2ee70c2d6b
3
+ metadata.gz: 01d43d93e589197104d9ee166e6a9dcf727fd204524b52145d7d93b45ba79cd2
4
+ data.tar.gz: 023f2e0f8ff17bc78a01e2dc1eb1f98d9c41b0cd353e311f350c2704a9ffa602
5
5
  SHA512:
6
- metadata.gz: 8948a681fb88b3d84e3751e6df0f5ca28314d6c9b5e183666780be95b14fd3c7728e6ad1eb13309d4b7114115b56edbb28fe929c2376bbcc5b762846a7d00404
7
- data.tar.gz: 9a2430efc4cf3b7a24b27bb40228dad67e00bd0cb10c9e0fc3764eb49e9caafda5b6d9fa0cc0335c77edc42e0a60416b20343dee5cee23dcfbfe350bed90e9a6
6
+ metadata.gz: 99992932a0f6a3d589e77e1b7dc4225be2e083a15320fa00a96f07a4cd9bbe9803454b94ecae409d439809a53225fb54ce283fcca074c0aed479e7d8b808d545
7
+ data.tar.gz: c77e99af9cf98781a2e961817b5c60e31fe178e6eef7bb8bd77b1af199043b8acd8395a27dd7cb10680ae82d28ec57e98106fef42f57055cb23752d63605a7b9
@@ -0,0 +1,21 @@
1
+ name: Performance Check
2
+
3
+ on: [pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ ruby: [2.5, 2.6, 2.7, 3.0]
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v2
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - name: Run benchmark
20
+ run: |
21
+ ruby script/perf_check.rb
data/.rubocop.yml CHANGED
@@ -13,12 +13,13 @@ AllCops:
13
13
  - 'Rakefile'
14
14
  - 'alba.gemspec'
15
15
  - 'benchmark/**/*.rb'
16
+ - 'script/**/*.rb'
16
17
  NewCops: enable
17
18
  EnabledByDefault: true
18
19
  TargetRubyVersion: 2.5
19
20
 
20
- # Oneline comment is not valid so until it gets valid, we disable it
21
- Bundler/GemComment:
21
+ # Items in Gemfile is dev dependencies and we don't have to specify versions.
22
+ Bundler/GemVersion:
22
23
  Enabled: false
23
24
 
24
25
  # We'd like to write something like:
@@ -44,9 +45,6 @@ Metrics:
44
45
  Exclude:
45
46
  - 'test/**/*.rb'
46
47
 
47
- Metrics/MethodLength:
48
- Max: 15
49
-
50
48
  # `Resource` module is a core module and its length tends to be long...
51
49
  Metrics/ModuleLength:
52
50
  Exclude:
@@ -55,7 +53,6 @@ Metrics/ModuleLength:
55
53
  # Resource class includes DSLs, which tend to accept long list of parameters
56
54
  Metrics/ParameterLists:
57
55
  Exclude:
58
- - 'lib/alba/resource.rb'
59
56
  - 'test/**/*.rb'
60
57
 
61
58
  # We need to eval resource code to test errors on resource classes
@@ -81,7 +78,10 @@ Style/InlineComment:
81
78
  Enabled: false
82
79
 
83
80
  Style/MethodCallWithArgsParentheses:
84
- Enabled: false
81
+ IgnoredMethods: ['require', 'require_relative', 'include', 'extend', 'puts', 'p', 'warn', 'raise', 'send', 'public_send']
82
+ Exclude:
83
+ # There are so many `attributes` call without parenthese and that's absolutely fine
84
+ - 'test/**/*.rb'
85
85
 
86
86
  # There are so many cases we just want `if` expression!
87
87
  Style/MissingElse:
data/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] 2021-06-30
10
+
11
+ - [Feat] Add a config method to set encoder directly
12
+ - [Feat] Implement `meta` method and option for metadata
13
+ - [Feat] Add `root_key` option to `Resource#serialize`
14
+ - [Feat] Enable setting key for collection with `root_key`
15
+ - [Feat] Add `Resource.root_key` and `Resource.root_key!`
16
+ - [Feat] `Alba.serialize` now infers resource class
17
+
9
18
  ## [1.3.0] 2021-05-31
10
19
 
11
20
  - [Perf] Improve performance for `many` [641d8f9]
data/Gemfile CHANGED
@@ -5,16 +5,17 @@ gemspec
5
5
 
6
6
  gem 'activesupport', require: false # For backend
7
7
  gem 'ffaker', require: false # For testing
8
+ gem 'inch', require: false # For inline documents
8
9
  gem 'minitest', '~> 5.14' # For test
9
10
  gem 'rake', '~> 13.0' # For test and automation
10
11
  gem 'rubocop', '>= 0.79.0', require: false # For lint
11
- gem 'rubocop-minitest', '~> 0.12.0', require: false # For lint
12
+ gem 'rubocop-minitest', '~> 0.13.0', require: false # For lint
12
13
  gem 'rubocop-performance', '~> 1.11.0', require: false # For lint
13
14
  gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
14
15
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
15
16
  gem 'simplecov', '~> 0.21.0', require: false # For test coverage
16
17
  gem 'simplecov-cobertura', require: false # For test coverage
17
- gem 'yard', require: false
18
+ gem 'yard', require: false # For documentation
18
19
 
19
20
  platforms :ruby do
20
21
  gem 'oj', '~> 3.11', require: false # For backend
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
  [![CI](https://github.com/okuramasafumi/alba/actions/workflows/main.yml/badge.svg)](https://github.com/okuramasafumi/alba/actions/workflows/main.yml)
3
3
  [![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/master/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability)
5
+ [![Inline docs](http://inch-ci.org/github/okuramasafumi/alba.svg?branch=main)](http://inch-ci.org/github/okuramasafumi/alba)
5
6
  ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/okuramasafumi/alba)
6
7
  ![GitHub](https://img.shields.io/github/license/okuramasafumi/alba)
7
8
 
@@ -98,6 +99,16 @@ You can set a backend like this:
98
99
  Alba.backend = :oj
99
100
  ```
100
101
 
102
+ #### Encoder configuration
103
+
104
+ You can also set JSON encoder directly with a Proc.
105
+
106
+ ```ruby
107
+ Alba.encoder = ->(object) { JSON.generate(object) }
108
+ ```
109
+
110
+ You can consider setting a backend with Symbol as a shortcut to set encoder.
111
+
101
112
  #### Inference configuration
102
113
 
103
114
  You can enable inference feature using `enable_inference!` method.
@@ -198,6 +209,35 @@ UserResource.new(user).serialize
198
209
  # => '{"id":1,"articles":[{"title":"Hello World!"},{"title":"Super nice"}]}'
199
210
  ```
200
211
 
212
+ You can define associations inline if you don't need a class for association.
213
+
214
+ ```ruby
215
+ class ArticleResource
216
+ include Alba::Resource
217
+
218
+ attributes :title
219
+ end
220
+
221
+ class UserResource
222
+ include Alba::Resource
223
+
224
+ attributes :id
225
+
226
+ many :articles, resource: ArticleResource
227
+ end
228
+
229
+ # This class works the same as `UserResource`
230
+ class AnotherUserResource
231
+ include Alba::Resource
232
+
233
+ attributes :id
234
+
235
+ many :articles do
236
+ attributes :title
237
+ end
238
+ end
239
+ ```
240
+
201
241
  ### Inline definition with `Alba.serialize`
202
242
 
203
243
  `Alba.serialize` method is a shortcut to define everything inline.
@@ -212,6 +252,13 @@ end
212
252
  # => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'
213
253
  ```
214
254
 
255
+ `Alba.serialize` can be used when you don't know what kind of object you serialize. For example:
256
+
257
+ ```ruby
258
+ Alba.serialize(something)
259
+ # => Same as `FooResource.new(something).serialize` when `something` is an instance of `Foo`.
260
+ ```
261
+
215
262
  Although this might be useful sometimes, it's generally recommended to define a class for Resource.
216
263
 
217
264
  ### Inheritance and Ignorance
@@ -235,13 +282,12 @@ class GenericFooResource
235
282
  attributes :id, :name, :body
236
283
  end
237
284
 
238
- class RestrictedFooResouce < GenericFooResource
285
+ class RestrictedFooResource < GenericFooResource
239
286
  ignoring :id, :body
240
287
  end
241
288
 
242
- RestrictedFooResouce.new(foo).serialize
289
+ RestrictedFooResource.new(foo).serialize
243
290
  # => '{"name":"my foo"}'
244
- end
245
291
  ```
246
292
 
247
293
  ### Key transformation
@@ -402,6 +448,20 @@ user = User.new(1, nil, nil)
402
448
  UserResource.new(user).serialize # => '{"id":1}'
403
449
  ```
404
450
 
451
+ ### Default
452
+
453
+ Alba doesn't support default value for attributes, but it's easy to set a default value.
454
+
455
+ ```ruby
456
+ class FooResource
457
+ attribute :bar do |foo|
458
+ foo.bar || 'default bar'
459
+ end
460
+ end
461
+ ```
462
+
463
+ We believe this is clearer than using some (not implemented yet) DSL such as `default` because there are some conditions where default values should be applied (`nil`, `blank?`, `empty?` etc.)
464
+
405
465
  ### Inference
406
466
 
407
467
  After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
@@ -510,6 +570,54 @@ Alba.on_error do |error, object, key, attribute, resource_class|
510
570
  end
511
571
  ```
512
572
 
573
+ ### Metadata
574
+
575
+ You can set a metadata with `meta` DSL or `meta` option.
576
+
577
+ ```ruby
578
+ class UserResource
579
+ include Alba::Resource
580
+
581
+ root_key :user, :users
582
+
583
+ attributes :id, :name
584
+
585
+ meta do
586
+ if object.is_a?(Enumerable)
587
+ {size: object.size}
588
+ else
589
+ {foo: :bar}
590
+ end
591
+ end
592
+ end
593
+
594
+ user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
595
+ UserResource.new([user]).serialize
596
+ # => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1}}'
597
+
598
+ # You can merge metadata with `meta` option
599
+
600
+ UserResource.new([user]).serialize(meta: {foo: :bar})
601
+ # => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1,"foo":"bar"}}'
602
+
603
+ # You can set metadata with `meta` option alone
604
+
605
+ class UserResourceWithoutMeta
606
+ include Alba::Resource
607
+
608
+ root_key :user, :users
609
+
610
+ attributes :id, :name
611
+ end
612
+
613
+ UserResource.new([user]).serialize(meta: {foo: :bar})
614
+ # => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"foo":"bar"}}'
615
+ ```
616
+
617
+ You can use `object` method to access the underlying object and `params` to access the params in `meta` block.
618
+
619
+ Note that setting root key is required when setting a metadata.
620
+
513
621
  ### Circular associations control
514
622
 
515
623
  **Note that this feature works correctly since version 1.3. In previous versions it doesn't work as expected.**
data/alba.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = 'https://github.com/okuramasafumi/alba'
17
- spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md'
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
data/gemfiles/all.gemfile CHANGED
@@ -11,7 +11,7 @@ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
11
11
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
12
12
  gem 'simplecov', '~> 0.21.0', require: false # For test coverage
13
13
  gem 'simplecov-cobertura', require: false # For test coverage
14
- gem 'yard', require: false
14
+ gem 'yard', require: false # For documentation
15
15
 
16
16
  platforms :ruby do
17
17
  gem 'oj', '~> 3.11', require: false # For backend
@@ -9,7 +9,7 @@ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
9
9
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
10
10
  gem 'simplecov', '~> 0.21.0', require: false # For test coverage
11
11
  gem 'simplecov-cobertura', require: false # For test coverage
12
- gem 'yard', require: false
12
+ gem 'yard', require: false # For documentation
13
13
 
14
14
  platforms :ruby do
15
15
  gem 'oj', '~> 3.11', require: false # For backend
@@ -10,7 +10,7 @@ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
10
10
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
11
11
  gem 'simplecov', '~> 0.21.0', require: false # For test coverage
12
12
  gem 'simplecov-cobertura', require: false # For test coverage
13
- gem 'yard', require: false
13
+ gem 'yard', require: false # For documentation
14
14
 
15
15
  platforms :ruby do
16
16
  gem 'ruby-prof', require: false # For performance profiling
data/lib/alba.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'json'
1
2
  require_relative 'alba/version'
2
3
  require_relative 'alba/resource'
3
4
 
@@ -14,6 +15,8 @@ module Alba
14
15
 
15
16
  class << self
16
17
  attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
18
+
19
+ # Accessor for inflector, a module responsible for incflecting strings
17
20
  attr_accessor :inflector
18
21
 
19
22
  # Set the backend, which actually serializes object into JSON
@@ -24,24 +27,35 @@ module Alba
24
27
  # @raise [Alba::UnsupportedBackend] if backend is not supported
25
28
  def backend=(backend)
26
29
  @backend = backend&.to_sym
27
- set_encoder
30
+ set_encoder_from_backend
31
+ end
32
+
33
+ # Set encoder, a Proc object that accepts an object and generates JSON from it
34
+ # Set backend as `:custom` which indicates no preset encoder is used
35
+ #
36
+ # @param encoder [Proc]
37
+ # @raise [ArgumentError] if given encoder is not a Proc or its arity is not one
38
+ def encoder=(encoder)
39
+ raise ArgumentError, 'Encoder must be a Proc accepting one argument' unless encoder.is_a?(Proc) && encoder.arity == 1
40
+
41
+ @encoder = encoder
42
+ @backend = :custom
28
43
  end
29
44
 
30
45
  # Serialize the object with inline definitions
31
46
  #
32
47
  # @param object [Object] the object to be serialized
33
- # @param key [Symbol]
48
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
49
+ # @param root_key [Symbol, nil, true]
34
50
  # @param block [Block] resource block
35
51
  # @return [String] serialized JSON string
36
52
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
37
- def serialize(object, key: nil, &block)
38
- raise ArgumentError, 'Block required' unless block
53
+ def serialize(object, key: nil, root_key: nil, &block)
54
+ warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
55
+ klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
39
56
 
40
- klass = Class.new
41
- klass.include(Alba::Resource)
42
- klass.class_eval(&block)
43
57
  resource = klass.new(object)
44
- resource.serialize(key: key)
58
+ resource.serialize(root_key: root_key || key)
45
59
  end
46
60
 
47
61
  # Enable inference for key and resource name
@@ -63,6 +77,9 @@ module Alba
63
77
  #
64
78
  # @param [Symbol] handler
65
79
  # @param [Block]
80
+ # @raise [ArgumentError] if both handler and block params exist
81
+ # @raise [ArgumentError] if both handler and block params don't exist
82
+ # @return [void]
66
83
  def on_error(handler = nil, &block)
67
84
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
68
85
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
@@ -80,18 +97,32 @@ module Alba
80
97
  @transforming_root_key = false
81
98
  end
82
99
 
100
+ # @param block [Block] resource body
101
+ # @return [Class<Alba::Resource>] resource class
102
+ def resource_class(&block)
103
+ klass = Class.new
104
+ klass.include(Alba::Resource)
105
+ klass.class_eval(&block)
106
+ klass
107
+ end
108
+
109
+ # @param name [String] a String Alba infers resource name with
110
+ # @param nesting [String, nil] namespace Alba tries to find resource class in
111
+ # @return [Class<Alba::Resource>] resource class
112
+ def infer_resource_class(name, nesting: nil)
113
+ enable_inference!
114
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
115
+ const_parent.const_get("#{ActiveSupport::Inflector.classify(name)}Resource")
116
+ end
117
+
83
118
  private
84
119
 
85
- def set_encoder
120
+ def set_encoder_from_backend
86
121
  @encoder = case @backend
87
- when :oj, :oj_strict
88
- try_oj
89
- when :oj_rails
90
- try_oj(mode: :rails)
91
- when :active_support
92
- try_active_support
93
- when nil, :default, :json
94
- default_encoder
122
+ when :oj, :oj_strict then try_oj
123
+ when :oj_rails then try_oj(mode: :rails)
124
+ when :active_support then try_active_support
125
+ when nil, :default, :json then default_encoder
95
126
  else
96
127
  raise Alba::UnsupportedBackend, "Unsupported backend, #{backend}"
97
128
  end
@@ -115,7 +146,6 @@ module Alba
115
146
 
116
147
  def default_encoder
117
148
  lambda do |hash|
118
- require 'json'
119
149
  JSON.dump(hash)
120
150
  end
121
151
  end
@@ -4,9 +4,10 @@ module Alba
4
4
  class Association
5
5
  attr_reader :object, :name
6
6
 
7
- # @param name [Symbol] name of the method to fetch association
8
- # @param condition [Proc] a proc filtering data
9
- # @param resource [Class<Alba::Resource>] a resource class for the association
7
+ # @param name [Symbol, String] name of the method to fetch association
8
+ # @param condition [Proc, nil] a proc filtering data
9
+ # @param resource [Class<Alba::Resource>, nil] a resource class for the association
10
+ # @param nesting [String] a namespace where source class is inferred with
10
11
  # @param block [Block] used to define resource when resource arg is absent
11
12
  def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
12
13
  @name = name
@@ -31,24 +32,12 @@ module Alba
31
32
 
32
33
  def assign_resource(nesting)
33
34
  @resource = if @block
34
- resource_class
35
+ Alba.resource_class(&@block)
35
36
  elsif Alba.inferring
36
- resource_class_with_nesting(nesting)
37
+ Alba.infer_resource_class(@name, nesting: nesting)
37
38
  else
38
39
  raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
39
40
  end
40
41
  end
41
-
42
- def resource_class
43
- klass = Class.new
44
- klass.include(Alba::Resource)
45
- klass.class_eval(&@block)
46
- klass
47
- end
48
-
49
- def resource_class_with_nesting(nesting)
50
- const_parent = nesting.nil? ? Object : Object.const_get(nesting)
51
- const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
52
- end
53
42
  end
54
43
  end
@@ -11,7 +11,7 @@ module Alba
11
11
 
12
12
  # Camelizes a key
13
13
  #
14
- # @params key [String] key to be camelized
14
+ # @param key [String] key to be camelized
15
15
  # @return [String] camelized key
16
16
  def camelize(key)
17
17
  ActiveSupport::Inflector.camelize(key)
@@ -19,7 +19,7 @@ module Alba
19
19
 
20
20
  # Camelizes a key, 1st letter lowercase
21
21
  #
22
- # @params key [String] key to be camelized
22
+ # @param key [String] key to be camelized
23
23
  # @return [String] camelized key
24
24
  def camelize_lower(key)
25
25
  ActiveSupport::Inflector.camelize(key, false)
@@ -27,7 +27,7 @@ module Alba
27
27
 
28
28
  # Dasherizes a key
29
29
  #
30
- # @params key [String] key to be dasherized
30
+ # @param key [String] key to be dasherized
31
31
  # @return [String] dasherized key
32
32
  def dasherize(key)
33
33
  ActiveSupport::Inflector.dasherize(key)
@@ -4,7 +4,7 @@ module Alba
4
4
  class << self
5
5
  # Create key transform function for given transform_type
6
6
  #
7
- # @params transform_type [Symbol] transform type
7
+ # @param transform_type [Symbol] transform type
8
8
  # @return [Proc] transform function
9
9
  # @raise [Alba::Error] when transform_type is not supported
10
10
  def create(transform_type)
data/lib/alba/resource.rb CHANGED
@@ -8,7 +8,7 @@ module Alba
8
8
  module Resource
9
9
  # @!parse include InstanceMethods
10
10
  # @!parse extend ClassMethods
11
- DSLS = {_attributes: {}, _key: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze
11
+ DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze # rubocop:disable Layout/LineLength
12
12
  private_constant :DSLS
13
13
 
14
14
  WITHIN_DEFAULT = Object.new.freeze
@@ -33,7 +33,7 @@ module Alba
33
33
 
34
34
  # @param object [Object] the object to be serialized
35
35
  # @param params [Hash] user-given Hash for arbitrary data
36
- # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
36
+ # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
37
37
  def initialize(object, params: {}, within: WITHIN_DEFAULT)
38
38
  @object = object
39
39
  @params = params.freeze
@@ -43,11 +43,19 @@ module Alba
43
43
 
44
44
  # Serialize object into JSON string
45
45
  #
46
- # @param key [Symbol]
46
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
47
+ # @param root_key [Symbol, nil, true]
48
+ # @param meta [Hash] metadata for this seialization
47
49
  # @return [String] serialized JSON string
48
- def serialize(key: nil)
49
- key = key.nil? ? _key : key
50
- hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
50
+ def serialize(key: nil, root_key: nil, meta: {})
51
+ warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
52
+ key = key.nil? && root_key.nil? ? fetch_key : root_key || key
53
+ hash = if key && key != ''
54
+ h = {key.to_s => serializable_hash}
55
+ hash_with_metadata(h, meta)
56
+ else
57
+ serializable_hash
58
+ end
51
59
  Alba.encoder.call(hash)
52
60
  end
53
61
 
@@ -61,15 +69,29 @@ module Alba
61
69
 
62
70
  private
63
71
 
72
+ def hash_with_metadata(hash, meta)
73
+ base = @_meta ? instance_eval(&@_meta) : {}
74
+ metadata = base.merge(meta)
75
+ hash[:meta] = metadata unless metadata.empty?
76
+ hash
77
+ end
78
+
79
+ def fetch_key
80
+ collection? ? _key_for_collection : _key
81
+ end
82
+
83
+ def _key_for_collection
84
+ return @_key_for_collection.to_s unless @_key_for_collection == true && Alba.inferring
85
+
86
+ key = resource_name.pluralize
87
+ transforming_root_key? ? transform_key(key) : key
88
+ end
89
+
64
90
  # @return [String]
65
91
  def _key
66
92
  return @_key.to_s unless @_key == true && Alba.inferring
67
93
 
68
- transforming_root_key? ? transform_key(key_from_resource_name) : key_from_resource_name
69
- end
70
-
71
- def key_from_resource_name
72
- collection? ? resource_name.pluralize : resource_name
94
+ transforming_root_key? ? transform_key(resource_name) : resource_name
73
95
  end
74
96
 
75
97
  def resource_name
@@ -105,14 +127,11 @@ module Alba
105
127
  def conditional_attribute(object, key, attribute)
106
128
  condition = attribute.last
107
129
  arity = condition.arity
130
+ # We can return early to skip fetch_attribute
108
131
  return [] if arity <= 1 && !instance_exec(object, &condition)
109
132
 
110
133
  fetched_attribute = fetch_attribute(object, attribute.first)
111
- attr = if attribute.first.is_a?(Alba::Association)
112
- attribute.first.object
113
- else
114
- fetched_attribute
115
- end
134
+ attr = attribute.first.is_a?(Alba::Association) ? attribute.first.object : fetched_attribute
116
135
  return [] if arity >= 2 && !instance_exec(object, attr, &condition)
117
136
 
118
137
  [key, fetched_attribute]
@@ -121,14 +140,10 @@ module Alba
121
140
  def handle_error(error, object, key, attribute)
122
141
  on_error = @_on_error || Alba._on_error
123
142
  case on_error
124
- when :raise, nil
125
- raise
126
- when :nullify
127
- [key, nil]
128
- when :ignore
129
- []
130
- when Proc
131
- on_error.call(error, object, key, attribute, self.class)
143
+ when :raise, nil then raise
144
+ when :nullify then [key, nil]
145
+ when :ignore then []
146
+ when Proc then on_error.call(error, object, key, attribute, self.class)
132
147
  else
133
148
  raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
134
149
  end
@@ -143,34 +158,27 @@ module Alba
143
158
 
144
159
  def fetch_attribute(object, attribute)
145
160
  case attribute
146
- when Symbol
147
- object.public_send attribute
148
- when Proc
149
- instance_exec(object, &attribute)
150
- when Alba::One, Alba::Many
151
- within = check_within(attribute.name.to_sym)
152
- return unless within
153
-
154
- attribute.to_hash(object, params: params, within: within)
155
- when TypedAttribute
156
- attribute.value(object)
161
+ when Symbol then object.public_send attribute
162
+ when Proc then instance_exec(object, &attribute)
163
+ when Alba::One, Alba::Many then yield_if_within(attribute.name.to_sym) { |within| attribute.to_hash(object, params: params, within: within) }
164
+ when TypedAttribute then attribute.value(object)
157
165
  else
158
166
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
159
167
  end
160
168
  end
161
169
 
170
+ def yield_if_within(association_name)
171
+ within = check_within(association_name)
172
+ yield(within) if within
173
+ end
174
+
162
175
  def check_within(association_name)
163
176
  case @within
164
- when WITHIN_DEFAULT # Default value, doesn't check within tree
165
- WITHIN_DEFAULT
166
- when Hash # Traverse within tree
167
- @within.fetch(association_name, nil)
168
- when Array # within tree ends with Array
169
- @within.find { |item| item.to_sym == association_name }
170
- when Symbol # within tree could end with Symbol
171
- @within == association_name
172
- when nil, true, false # In these cases, Alba stops serialization here.
173
- false
177
+ when WITHIN_DEFAULT then WITHIN_DEFAULT # Default value, doesn't check within tree
178
+ when Hash then @within.fetch(association_name, nil) # Traverse within tree
179
+ when Array then @within.find { |item| item.to_sym == association_name }
180
+ when Symbol then @within == association_name
181
+ when nil, true, false then false # Stop here
174
182
  else
175
183
  raise Alba::Error, "Unknown type for within option: #{@within.class}"
176
184
  end
@@ -191,11 +199,16 @@ module Alba
191
199
  DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
192
200
  end
193
201
 
202
+ # Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine
203
+ # rubocop:disable Metrics/ParameterLists
204
+
194
205
  # Set multiple attributes at once
195
206
  #
196
207
  # @param attrs [Array<String, Symbol>]
197
- # @param if [Boolean] condition to decide if it should render these attributes
198
- # @param attrs_with_types [Hash] attributes with name in its key and type and optional type converter in its value
208
+ # @param if [Proc] condition to decide if it should serialize these attributes
209
+ # @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
210
+ # attributes with name in its key and type and optional type converter in its value
211
+ # @return [void]
199
212
  def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
200
213
  if_value = binding.local_variable_get(:if)
201
214
  assign_attributes(attrs, if_value)
@@ -224,9 +237,11 @@ module Alba
224
237
  # Set an attribute with the given block
225
238
  #
226
239
  # @param name [String, Symbol] key name
227
- # @param options [Hash] option hash including `if` that is a condition to render
240
+ # @param options [Hash<Symbol, Proc>]
241
+ # @option options [Proc] if a condition to decide if this attribute should be serialized
228
242
  # @param block [Block] the block called during serialization
229
243
  # @raise [ArgumentError] if block is absent
244
+ # @return [void]
230
245
  def attribute(name, **options, &block)
231
246
  raise ArgumentError, 'No block given in attribute method' unless block
232
247
 
@@ -235,12 +250,14 @@ module Alba
235
250
 
236
251
  # Set One association
237
252
  #
238
- # @param name [String, Symbol]
239
- # @param condition [Proc]
240
- # @param resource [Class<Alba::Resource>]
241
- # @param key [String, Symbol] used as key when given
242
- # @param options [Hash] option hash including `if` that is a condition to render
253
+ # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
254
+ # @param condition [Proc, nil] a Proc to modify the association
255
+ # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
256
+ # @param key [String, Symbol, nil] used as key when given
257
+ # @param options [Hash<Symbol, Proc>]
258
+ # @option options [Proc] if a condition to decide if this association should be serialized
243
259
  # @param block [Block]
260
+ # @return [void]
244
261
  # @see Alba::One#initialize
245
262
  def one(name, condition = nil, resource: nil, key: nil, **options, &block)
246
263
  nesting = self.name&.rpartition('::')&.first
@@ -251,12 +268,14 @@ module Alba
251
268
 
252
269
  # Set Many association
253
270
  #
254
- # @param name [String, Symbol]
255
- # @param condition [Proc]
256
- # @param resource [Class<Alba::Resource>]
257
- # @param key [String, Symbol] used as key when given
258
- # @param options [Hash] option hash including `if` that is a condition to render
271
+ # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
272
+ # @param condition [Proc, nil] a Proc to filter the collection
273
+ # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
274
+ # @param key [String, Symbol, nil] used as key when given
275
+ # @param options [Hash<Symbol, Proc>]
276
+ # @option options [Proc] if a condition to decide if this association should be serialized
259
277
  # @param block [Block]
278
+ # @return [void]
260
279
  # @see Alba::Many#initialize
261
280
  def many(name, condition = nil, resource: nil, key: nil, **options, &block)
262
281
  nesting = self.name&.rpartition('::')&.first
@@ -268,14 +287,40 @@ module Alba
268
287
  # Set key
269
288
  #
270
289
  # @param key [String, Symbol]
290
+ # @deprecated Use {#root_key} instead
271
291
  def key(key)
292
+ warn '[DEPRECATION] `key` is deprecated, use `root_key` instead.'
272
293
  @_key = key.respond_to?(:to_sym) ? key.to_sym : key
273
294
  end
274
295
 
296
+ # Set root key
297
+ #
298
+ # @param key [String, Symbol]
299
+ # @param key_for_collection [String, Symbol]
300
+ # @raise [NoMethodError] when key doesn't respond to `to_sym` method
301
+ def root_key(key, key_for_collection = nil)
302
+ @_key = key.to_sym
303
+ @_key_for_collection = key_for_collection&.to_sym
304
+ end
305
+
275
306
  # Set key to true
276
307
  #
308
+ # @deprecated Use {#root_key!} instead
277
309
  def key!
310
+ warn '[DEPRECATION] `key!` is deprecated, use `root_key!` instead.'
278
311
  @_key = true
312
+ @_key_for_collection = true
313
+ end
314
+
315
+ # Set root key to true
316
+ def root_key!
317
+ @_key = true
318
+ @_key_for_collection = true
319
+ end
320
+
321
+ # Set metadata
322
+ def meta(&block)
323
+ @_meta = block
279
324
  end
280
325
 
281
326
  # Delete attributes
@@ -298,15 +343,18 @@ module Alba
298
343
  end
299
344
 
300
345
  # Set error handler
346
+ # If this is set it's used as a error handler overriding global one
301
347
  #
302
- # @param [Symbol] handler
303
- # @param [Block]
348
+ # @param handler [Symbol] `:raise`, `:ignore` or `:nullify`
349
+ # @param block [Block]
304
350
  def on_error(handler = nil, &block)
305
351
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
306
352
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
307
353
 
308
354
  @_on_error = handler || block
309
355
  end
356
+
357
+ # rubocop:enable Metrics/ParameterLists
310
358
  end
311
359
  end
312
360
  end
@@ -28,12 +28,9 @@ module Alba
28
28
  def check(object)
29
29
  value = object.public_send(@name)
30
30
  type_correct = case @type
31
- when :String, ->(klass) { klass == String }
32
- value.is_a?(String)
33
- when :Integer, ->(klass) { klass == Integer }
34
- value.is_a?(Integer)
35
- when :Boolean
36
- [true, false].include?(value)
31
+ when :String, ->(klass) { klass == String } then value.is_a?(String)
32
+ when :Integer, ->(klass) { klass == Integer } then value.is_a?(Integer)
33
+ when :Boolean then [true, false].include?(value)
37
34
  else
38
35
  raise Alba::UnsupportedType, "Unknown type: #{@type}"
39
36
  end
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '1.3.0'.freeze
2
+ VERSION = '1.4.0'.freeze
3
3
  end
@@ -0,0 +1,174 @@
1
+ # Benchmark script to run varieties of JSON serializers
2
+ # Fetch Alba from local, otherwise fetch latest from RubyGems
3
+ # exit(status)
4
+
5
+ # --- Bundle dependencies ---
6
+
7
+ require "bundler/inline"
8
+
9
+ gemfile(true) do
10
+ source "https://rubygems.org"
11
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
12
+
13
+ gem "activerecord", "~> 6.1.3"
14
+ gem "alba", path: '../'
15
+ gem "benchmark-ips"
16
+ gem "blueprinter"
17
+ gem "jbuilder"
18
+ gem "multi_json"
19
+ gem "oj"
20
+ gem "sqlite3"
21
+ end
22
+
23
+ # --- Test data model setup ---
24
+
25
+ require "active_record"
26
+ require "oj"
27
+ require "sqlite3"
28
+ Oj.optimize_rails
29
+
30
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
31
+
32
+ ActiveRecord::Schema.define do
33
+ create_table :posts, force: true do |t|
34
+ t.string :body
35
+ end
36
+
37
+ create_table :comments, force: true do |t|
38
+ t.integer :post_id
39
+ t.string :body
40
+ t.integer :commenter_id
41
+ end
42
+
43
+ create_table :users, force: true do |t|
44
+ t.string :name
45
+ end
46
+ end
47
+
48
+ class Post < ActiveRecord::Base
49
+ has_many :comments
50
+ has_many :commenters, through: :comments, class_name: 'User', source: :commenter
51
+
52
+ def attributes
53
+ {id: nil, body: nil, commenter_names: commenter_names}
54
+ end
55
+
56
+ def commenter_names
57
+ commenters.pluck(:name)
58
+ end
59
+ end
60
+
61
+ class Comment < ActiveRecord::Base
62
+ belongs_to :post
63
+ belongs_to :commenter, class_name: 'User'
64
+
65
+ def attributes
66
+ {id: nil, body: nil}
67
+ end
68
+ end
69
+
70
+ class User < ActiveRecord::Base
71
+ has_many :comments
72
+ end
73
+
74
+ # --- Alba serializers ---
75
+
76
+ require "alba"
77
+
78
+ class AlbaCommentResource
79
+ include ::Alba::Resource
80
+ attributes :id, :body
81
+ end
82
+
83
+ class AlbaPostResource
84
+ include ::Alba::Resource
85
+ attributes :id, :body
86
+ attribute :commenter_names do |post|
87
+ post.commenters.pluck(:name)
88
+ end
89
+ many :comments, resource: AlbaCommentResource
90
+ end
91
+
92
+ # --- Blueprint serializers ---
93
+
94
+ require "blueprinter"
95
+
96
+ class CommentBlueprint < Blueprinter::Base
97
+ fields :id, :body
98
+ end
99
+
100
+ class PostBlueprint < Blueprinter::Base
101
+ fields :id, :body, :commenter_names
102
+ association :comments, blueprint: CommentBlueprint
103
+
104
+ def commenter_names
105
+ commenters.pluck(:name)
106
+ end
107
+ end
108
+
109
+ # --- JBuilder serializers ---
110
+
111
+ require "jbuilder"
112
+
113
+ class Post
114
+ def to_builder
115
+ Jbuilder.new do |post|
116
+ post.call(self, :id, :body, :commenter_names, :comments)
117
+ end
118
+ end
119
+
120
+ def commenter_names
121
+ commenters.pluck(:name)
122
+ end
123
+ end
124
+
125
+ class Comment
126
+ def to_builder
127
+ Jbuilder.new do |comment|
128
+ comment.call(self, :id, :body)
129
+ end
130
+ end
131
+ end
132
+
133
+ # --- Test data creation ---
134
+
135
+ 100.times do |i|
136
+ post = Post.create!(body: "post#{i}")
137
+ user1 = User.create!(name: "John#{i}")
138
+ user2 = User.create!(name: "Jane#{i}")
139
+ 10.times do |n|
140
+ post.comments.create!(commenter: user1, body: "Comment1_#{i}_#{n}")
141
+ post.comments.create!(commenter: user2, body: "Comment2_#{i}_#{n}")
142
+ end
143
+ end
144
+
145
+ posts = Post.all.to_a
146
+
147
+ # --- Store the serializers in procs ---
148
+
149
+ alba = Proc.new { AlbaPostResource.new(posts).serialize }
150
+ blueprinter = Proc.new { PostBlueprint.render(posts) }
151
+ jbuilder = Proc.new do
152
+ Jbuilder.new do |json|
153
+ json.array!(posts) do |post|
154
+ json.post post.to_builder
155
+ end
156
+ end.target!
157
+ end
158
+
159
+ # --- Run the benchmarks ---
160
+
161
+ require 'benchmark/ips'
162
+ result = Benchmark.ips do |x|
163
+ x.report(:alba, &alba)
164
+ x.report(:blueprinter, &blueprinter)
165
+ x.report(:jbuilder, &jbuilder)
166
+ end
167
+
168
+ entries = result.entries.map {|entry| [entry.label, entry.iterations]}
169
+ alba_ips = entries.find {|e| e.first == :alba }.last
170
+ blueprinter_ips = entries.find {|e| e.first == :blueprinter }.last
171
+ jbuidler_ips = entries.find {|e| e.first == :jbuilder }.last
172
+ # Alba should be as fast as jbuilder and faster than blueprinter
173
+ alba_is_fast_enough = (alba_ips - jbuidler_ips) > -10.0 && (alba_ips - blueprinter_ips) > 10.0
174
+ exit(alba_is_fast_enough)
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.3.0
4
+ version: 1.4.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-05-31 00:00:00.000000000 Z
11
+ date: 2021-06-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Alba is the fastest JSON serializer for Ruby. It focuses on performance,
14
14
  flexibility and usability.
@@ -22,6 +22,7 @@ files:
22
22
  - ".github/ISSUE_TEMPLATE/feature_request.md"
23
23
  - ".github/dependabot.yml"
24
24
  - ".github/workflows/main.yml"
25
+ - ".github/workflows/perf.yml"
25
26
  - ".gitignore"
26
27
  - ".rubocop.yml"
27
28
  - ".yardopts"
@@ -50,6 +51,7 @@ files:
50
51
  - lib/alba/resource.rb
51
52
  - lib/alba/typed_attribute.rb
52
53
  - lib/alba/version.rb
54
+ - script/perf_check.rb
53
55
  - sider.yml
54
56
  homepage: https://github.com/okuramasafumi/alba
55
57
  licenses:
@@ -57,7 +59,7 @@ licenses:
57
59
  metadata:
58
60
  homepage_uri: https://github.com/okuramasafumi/alba
59
61
  source_code_uri: https://github.com/okuramasafumi/alba
60
- changelog_uri: https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md
62
+ changelog_uri: https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md
61
63
  post_install_message:
62
64
  rdoc_options: []
63
65
  require_paths: