alba 1.3.0 → 1.4.0

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