alba 0.13.1 → 1.0.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: 993f4b887c19f4e1149ddb850cd07a8cb8c2042cde0fb3974b1380537707a55d
4
- data.tar.gz: acf77a0aa35f7fe23fc3f98843064d2800e0b4e0739e587dd29aab65f06269ad
3
+ metadata.gz: c3999a63b9564e6bf0a7b00dfeb64a19d6d076c94047f7ee869a4ac4a38c6646
4
+ data.tar.gz: dd7244bf63111e4795639a62dac91a744e51adb38f6a88d851c9cc6de3043dc0
5
5
  SHA512:
6
- metadata.gz: 9b8e52dcaf4ca33cba0685c2a0a06c16df69cd04b6d399c092e920548f5baa6a21aeab8097f90e92ab35ac9c260d83858c19462a9ec2a689d97c6999daef5e3a
7
- data.tar.gz: e525a66c30477cab6741436128398a6f0309a0f786040fe946a9f2783d08f619df00d85157d04c5876cc579e19cceabf22dcb3556addf6ad061d44d1f0cfa888
6
+ metadata.gz: f93aaadf9b8fae4b9ae334f87647c3b0f1645147fd237b62f12451f4306f5cb6699ed25c935b563b68ab45c965bb1cfb8f2c73e08b23899a477384c37124d325
7
+ data.tar.gz: adb456a4796782e725bffa5720d1b5e6f831c1e0ec215721a814b331890f4a1d0ebe592e7e906239138729dd81586ad6e1c850ece61c5495ce829551feada9a1
data/.gitignore CHANGED
@@ -6,3 +6,5 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+
10
+ Gemfile.lock
data/.rubocop.yml CHANGED
@@ -15,6 +15,7 @@ AllCops:
15
15
  - 'benchmark/**/*.rb'
16
16
  NewCops: enable
17
17
  EnabledByDefault: true
18
+ TargetRubyVersion: 2.5
18
19
 
19
20
  # Oneline comment is not valid so until it gets valid, we disable it
20
21
  Bundler/GemComment:
@@ -36,6 +37,11 @@ Metrics/ClassLength:
36
37
  Metrics/MethodLength:
37
38
  Max: 15
38
39
 
40
+ # Resource class includes DSLs, which tend to accept long list of parameters
41
+ Metrics/ParameterLists:
42
+ Exclude:
43
+ - 'lib/alba/resource.rb'
44
+
39
45
  Style/ConstantVisibility:
40
46
  Exclude:
41
47
  - 'lib/alba/version.rb'
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0] - 2021-04-07
10
+
11
+ This is the first major release of Alba and it includes so many features. To see all the features you can have a look at [README](https://github.com/okuramasafumi/alba/blob/master/README.md#features).
data/Gemfile CHANGED
@@ -9,8 +9,9 @@ gem 'minitest', '~> 5.14' # For test
9
9
  gem 'oj', '~> 3.11', platform: :ruby, require: false # For backend
10
10
  gem 'rake', '~> 13.0' # For test and automation
11
11
  gem 'rubocop', '>= 0.79.0', require: false # For lint
12
- gem 'rubocop-minitest', '~> 0.10.3', require: false # For lint
12
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
13
13
  gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
14
14
  gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
15
15
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
16
+ gem 'ruby-prof', require: false # For performance profiling
16
17
  gem 'yard', require: false
data/README.md CHANGED
@@ -19,8 +19,6 @@ Alba has some advantages over other JSON serializers which we've wanted to have.
19
19
 
20
20
  DSL is great. It makes the coding experience natural and intuitive. However, remembering lots of DSL requires us a lot of effort. Unfortunately, most of the existing libraries have implemented their features via DSL and it's not easy to understand how they behave entirely. Alba's core DSL are only four (`attributes`, `attribute`, `one` and `many`) so it's easy to understand how to use.
21
21
 
22
- Alba is also understandable internally. The codebase is much smaller than the alternatives. In fact, it's about 330 lines of code. Look at the code on [GitHub](https://github.com/okuramasafumi/alba/tree/master/lib) and you'll be surprised how simple it is!
23
-
24
22
  ### Performance
25
23
 
26
24
  Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/master/benchmark).
@@ -56,10 +54,12 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
56
54
  * One and many association with the ability to define them inline
57
55
  * Adding condition and filter to association
58
56
  * Parameters can be injected and used in attributes and associations
59
- * Setting root key separately in Serializer
60
- * Adding metadata
57
+ * Conditional attributes and associations
61
58
  * Selectable backend
62
59
  * Key transformation
60
+ * Root key inference
61
+ * Error handling
62
+ * Resource name inflection based on association name
63
63
  * No runtime dependencies
64
64
 
65
65
  ## Anti features
@@ -69,7 +69,6 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
69
69
  * Supporting all existing JSON encoder/decoder
70
70
  * Cache
71
71
  * [JSON:API](https://jsonapi.org) support
72
- * Association name inflection
73
72
  * And many others
74
73
 
75
74
  ## Usage
@@ -78,7 +77,7 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
78
77
 
79
78
  Alba's configuration is fairly simple.
80
79
 
81
- #### Backend
80
+ #### Backend configuration
82
81
 
83
82
  Backend is the actual part serializing an object into JSON. Alba supports these backends.
84
83
 
@@ -92,6 +91,26 @@ You can set a backend like this:
92
91
  Alba.backend = :oj
93
92
  ```
94
93
 
94
+ #### Inference configuration
95
+
96
+ You can enable inference feature using `enable_inference!` method.
97
+
98
+ ```ruby
99
+ Alba.enable_inference!
100
+ ```
101
+
102
+ You must install `ActiveSupport` to enable inference.
103
+
104
+ #### Error handling configuration
105
+
106
+ You can configure error handling with `on_error` method.
107
+
108
+ ```ruby
109
+ Alba.on_error :ignore
110
+ ```
111
+
112
+ For the details, see [Error handling section](#error-handling)
113
+
95
114
  ### Simple serialization with key
96
115
 
97
116
  ```ruby
@@ -109,6 +128,8 @@ end
109
128
  class UserResource
110
129
  include Alba::Resource
111
130
 
131
+ key :user
132
+
112
133
  attributes :id, :name
113
134
 
114
135
  attribute :name_with_email do |resource|
@@ -116,12 +137,6 @@ class UserResource
116
137
  end
117
138
  end
118
139
 
119
- class SerializerWithKey
120
- include Alba::Serializer
121
-
122
- set key: :user
123
- end
124
-
125
140
  user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
126
141
  UserResource.new(user).serialize
127
142
  # => "{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}"
@@ -181,7 +196,7 @@ UserResource.new(user).serialize
181
196
  `Alba.serialize` method is a shortcut to define everything inline.
182
197
 
183
198
  ```ruby
184
- Alba.serialize(user, with: proc { set key: :foo }) do
199
+ Alba.serialize(user, key: :foo) do
185
200
  attributes :id
186
201
  many :articles do
187
202
  attributes :title, :body
@@ -190,7 +205,7 @@ end
190
205
  # => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'
191
206
  ```
192
207
 
193
- Although this might be useful sometimes, it's generally recommended to define a class for both Resource and Serializer.
208
+ Although this might be useful sometimes, it's generally recommended to define a class for Resource.
194
209
 
195
210
  ### Inheritance and Ignorance
196
211
 
@@ -284,14 +299,147 @@ end
284
299
 
285
300
  user = User.new(1, nil, nil)
286
301
  UserResource.new(user).serialize # => '{"id":1}'
287
-
288
-
289
302
  ```
290
303
 
291
304
  The key part is the use of `Proc#>>` since `Alba::Resource#converter` returns a `Proc` which contains the basic logic and it's impossible to change its behavior by just overriding the method.
292
305
 
293
306
  It's not recommended to swap the whole conversion logic. It's recommended to always call `super` when you override `converter`.
294
307
 
308
+ ### Conditional attributes
309
+
310
+ Filtering attributes with overriding `convert` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.
311
+
312
+ In these cases, conditional attributes works well. We can pass `if` option to `attributes`, `attribute`, `one` and `many`. Below is an example for the same effect as [filtering attributes section](#filtering-attributes).
313
+
314
+ ```ruby
315
+ class User
316
+ attr_accessor :id, :name, :email, :created_at, :updated_at
317
+
318
+ def initialize(id, name, email)
319
+ @id = id
320
+ @name = name
321
+ @email = email
322
+ end
323
+ end
324
+
325
+ class UserResource
326
+ include Alba::Resource
327
+
328
+ attributes :id, :name, :email, if: proc { |user, attribute| !attribute.nil? }
329
+ end
330
+
331
+ user = User.new(1, nil, nil)
332
+ UserResource.new(user).serialize # => '{"id":1}'
333
+ ```
334
+
335
+ ### Inference
336
+
337
+ After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
338
+
339
+ ```ruby
340
+ Alba.enable_inference!
341
+
342
+ class User
343
+ attr_reader :id
344
+ attr_accessor :articles
345
+
346
+ def initialize(id)
347
+ @id = id
348
+ @articles = []
349
+ end
350
+ end
351
+
352
+ class Article
353
+ attr_accessor :id, :title
354
+
355
+ def initialize(id, title)
356
+ @id = id
357
+ @title = title
358
+ end
359
+ end
360
+
361
+ class ArticleResource
362
+ include Alba::Resource
363
+
364
+ attributes :title
365
+ end
366
+
367
+ class UserResource
368
+ include Alba::Resource
369
+
370
+ key!
371
+
372
+ attributes :id
373
+
374
+ many :articles
375
+ end
376
+
377
+ user = User.new(1)
378
+ user.articles << Article.new(1, 'The title')
379
+
380
+ UserResource.new(user).serialize # => '{"user":{"id":1,"articles":[{"title":"The title"}]}}'
381
+ UserResource.new([user]).serialize # => '{"users":[{"id":1,"articles":[{"title":"The title"}]}]}'
382
+ ```
383
+
384
+ This resource automatically sets its root key to either "users" or "user", depending on the given object is collection or not.
385
+
386
+ Also, you don't have to specify which resource class to use with `many`. Alba infers it from association name.
387
+
388
+ Note that to enable this feature you must install `ActiveSupport` gem.
389
+
390
+ ### Error handling
391
+
392
+ You can set error handler globally or per resource using `on_error`.
393
+
394
+ ```ruby
395
+ class User
396
+ attr_accessor :id, :name
397
+
398
+ def initialize(id, name, email)
399
+ @id = id
400
+ @name = name
401
+ @email = email
402
+ end
403
+
404
+ def email
405
+ raise RuntimeError, 'Error!'
406
+ end
407
+ end
408
+
409
+ class UserResource
410
+ include Alba::Resource
411
+
412
+ attributes :id, :name, :email
413
+
414
+ on_error :ignore
415
+ end
416
+
417
+ user = User.new(1, 'Test', 'email@example.com')
418
+ UserResource.new(user).serialize # => '{"id":1,"name":"Test"}'
419
+ ```
420
+
421
+ This way you can exclude an entry when fetching an attribute gives an exception.
422
+
423
+ There are four possible arguments `on_error` method accepts.
424
+
425
+ * `:raise` re-raises an error. This is the default behavior.
426
+ * `:ignore` ignores the entry with the error.
427
+ * `:nullify` sets the attribute with the error to `nil`.
428
+ * Block gives you more control over what to be returned.
429
+
430
+ The block receives five arguments, `error`, `object`, `key`, `attribute` and `resource class` and must return a two-element array. Below is an example.
431
+
432
+ ```ruby
433
+ # Global error handling
434
+ Alba.on_error do |error, object, key, attribute, resource_class|
435
+ if resource_class == MyResource
436
+ ['error_fallback', object.error_fallback]
437
+ else
438
+ [key, error.message]
439
+ end
440
+ end
441
+ ```
442
+
295
443
  ## Comparison
296
444
 
297
445
  Alba is faster than alternatives.
@@ -309,18 +457,6 @@ Alba.backend = :active_support
309
457
 
310
458
  The name "Alba" comes from "albatross", a kind of birds. In Japanese, this bird is called "Aho-dori", which means "stupid bird". I find it funny because in fact albatrosses fly really fast. I hope Alba looks stupid but in fact it does its job quick.
311
459
 
312
- ## Alba internals
313
-
314
- Alba has three component, `Serializer`, `Resource` and `Value` (`Value` is conceptual and not implemented directly).
315
-
316
- `Serializer` is a component responsible for rendering JSON output with `Resource`. `Serializer` can add more data to `Resource` such as `metadata`. Users can define one single `Serializer` and reuse it for all `Resource`s. The main interface is `#serialize`.
317
-
318
- `Resource` is a component responsible for defining how an object (or a collection of objects) is converted into JSON. The difference between `Serializer` and `Resource` is that while `Serializer` can add arbitrary data into JSON, `Resource` can get data only from the object under it. The main interface is `#serializable_hash`.
319
-
320
- `One` and `Many` are the special object fetching other resources and converting them into Hash.
321
-
322
- The main `Alba` module holds config values and one convenience method, `.serialize`.
323
-
324
460
  ## Development
325
461
 
326
462
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/alba.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require_relative 'alba/version'
2
- require_relative 'alba/serializer'
3
2
  require_relative 'alba/resource'
4
3
 
5
4
  # Core module
@@ -11,8 +10,7 @@ module Alba
11
10
  class UnsupportedBackend < Error; end
12
11
 
13
12
  class << self
14
- attr_reader :backend, :encoder
15
- attr_accessor :default_serializer
13
+ attr_reader :backend, :encoder, :inferring, :_on_error
16
14
 
17
15
  # Set the backend, which actually serializes object into JSON
18
16
  #
@@ -28,17 +26,43 @@ module Alba
28
26
  # Serialize the object with inline definitions
29
27
  #
30
28
  # @param object [Object] the object to be serialized
31
- # @param with [nil, Proc, Alba::Serializer] selializer
29
+ # @param key [Symbol]
32
30
  # @param block [Block] resource block
33
31
  # @return [String] serialized JSON string
34
32
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
35
- def serialize(object, with: nil, &block)
33
+ def serialize(object, key: nil, &block)
36
34
  raise ArgumentError, 'Block required' unless block
37
35
 
38
36
  resource_class.class_eval(&block)
39
37
  resource = resource_class.new(object)
40
- with ||= @default_serializer
41
- resource.serialize(with: with)
38
+ resource.serialize(key: key)
39
+ end
40
+
41
+ # Enable inference for key and resource name
42
+ def enable_inference!
43
+ begin
44
+ require 'active_support/inflector'
45
+ rescue LoadError
46
+ raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
47
+ end
48
+ @inferring = true
49
+ end
50
+
51
+ # Disable inference for key and resource name
52
+ def disable_inference!
53
+ @inferring = false
54
+ end
55
+
56
+ # Set error handler
57
+ #
58
+ # @param [Symbol] handler
59
+ # @param [Block]
60
+ def on_error(handler = nil, &block)
61
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
62
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
63
+
64
+ p block if block
65
+ @_on_error = handler || block
42
66
  end
43
67
 
44
68
  private
@@ -86,4 +110,5 @@ module Alba
86
110
  end
87
111
 
88
112
  @encoder = default_encoder
113
+ @_on_error = :raise
89
114
  end
@@ -2,16 +2,27 @@ module Alba
2
2
  # Base class for `One` and `Many`
3
3
  # Child class should implement `to_hash` method
4
4
  class Association
5
+ attr_reader :object
6
+
5
7
  # @param name [Symbol] name of the method to fetch association
6
8
  # @param condition [Proc] a proc filtering data
7
9
  # @param resource [Class<Alba::Resource>] a resource class for the association
8
10
  # @param block [Block] used to define resource when resource arg is absent
9
- def initialize(name:, condition: nil, resource: nil, &block)
11
+ def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
10
12
  @name = name
11
13
  @condition = condition
12
14
  @block = block
13
- @resource = resource || resource_class
14
- raise ArgumentError, 'resource or block is required' if @resource.nil? && @block.nil?
15
+ @resource = resource
16
+ return if @resource
17
+
18
+ if @block
19
+ @resource = resource_class
20
+ elsif Alba.inferring
21
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
22
+ @resource = const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
23
+ else
24
+ raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
25
+ end
15
26
  end
16
27
 
17
28
  # @abstract
@@ -21,6 +32,15 @@ module Alba
21
32
 
22
33
  private
23
34
 
35
+ def constantize(resource)
36
+ case resource # rubocop:disable Style/MissingElse
37
+ when Class
38
+ resource
39
+ when Symbol, String
40
+ Object.const_get(resource)
41
+ end
42
+ end
43
+
24
44
  def resource_class
25
45
  klass = Class.new
26
46
  klass.include(Alba::Resource)
data/lib/alba/many.rb CHANGED
@@ -9,11 +9,12 @@ module Alba
9
9
  # @param params [Hash] user-given Hash for arbitrary data
10
10
  # @return [Array<Hash>]
11
11
  def to_hash(target, params: {})
12
- objects = target.public_send(@name)
13
- objects = @condition.call(objects, params) if @condition
14
- return if objects.nil?
12
+ @object = target.public_send(@name)
13
+ @object = @condition.call(@object, params) if @condition
14
+ return if @object.nil?
15
15
 
16
- objects.map { |o| @resource.new(o, params: params).to_hash }
16
+ @resource = constantize(@resource)
17
+ @object.map { |o| @resource.new(o, params: params).to_hash }
17
18
  end
18
19
  end
19
20
  end
data/lib/alba/one.rb CHANGED
@@ -9,10 +9,11 @@ module Alba
9
9
  # @param params [Hash] user-given Hash for arbitrary data
10
10
  # @return [Hash]
11
11
  def to_hash(target, params: {})
12
- object = target.public_send(@name)
13
- object = @condition.call(object, params) if @condition
14
- return if object.nil?
12
+ @object = target.public_send(@name)
13
+ @object = @condition.call(object, params) if @condition
14
+ return if @object.nil?
15
15
 
16
+ @resource = constantize(@resource)
16
17
  @resource.new(object, params: params).to_hash
17
18
  end
18
19
  end
data/lib/alba/resource.rb CHANGED
@@ -1,4 +1,3 @@
1
- require_relative 'serializer'
2
1
  require_relative 'one'
3
2
  require_relative 'many'
4
3
 
@@ -7,7 +6,7 @@ module Alba
7
6
  module Resource
8
7
  # @!parse include InstanceMethods
9
8
  # @!parse extend ClassMethods
10
- DSLS = {_attributes: {}, _serializer: nil, _key: nil, _transform_keys: nil}.freeze
9
+ DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
11
10
  private_constant :DSLS
12
11
 
13
12
  # @private
@@ -25,7 +24,7 @@ module Alba
25
24
 
26
25
  # Instance methods
27
26
  module InstanceMethods
28
- attr_reader :object, :_key, :params
27
+ attr_reader :object, :params
29
28
 
30
29
  # @param object [Object] the object to be serialized
31
30
  # @param params [Hash] user-given Hash for arbitrary data
@@ -35,22 +34,14 @@ module Alba
35
34
  DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
36
35
  end
37
36
 
38
- # Get serializer with `with` argument and serialize self with it
37
+ # Serialize object into JSON string
39
38
  #
40
- # @param with [nil, Proc, Alba::Serializer] selializer
39
+ # @param key [Symbol]
41
40
  # @return [String] serialized JSON string
42
- def serialize(with: nil)
43
- serializer = case with
44
- when nil
45
- @_serializer || empty_serializer
46
- when ->(obj) { obj.is_a?(Class) && obj <= Alba::Serializer }
47
- with
48
- when Proc
49
- inline_extended_serializer(with)
50
- else
51
- raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc'
52
- end
53
- serializer.new(self).serialize
41
+ def serialize(key: nil)
42
+ key = key.nil? ? _key : key
43
+ hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
44
+ Alba.encoder.call(hash)
54
45
  end
55
46
 
56
47
  # A Hash for serialization
@@ -61,22 +52,68 @@ module Alba
61
52
  end
62
53
  alias to_hash serializable_hash
63
54
 
64
- # @return [Symbol]
65
- def key
66
- @_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym
67
- end
68
-
69
55
  private
70
56
 
71
- # rubocop:disable Style/MethodCalledOnDoEndBlock
57
+ # @return [String]
58
+ def _key
59
+ if @_key == true && Alba.inferring
60
+ demodulized = ActiveSupport::Inflector.demodulize(self.class.name)
61
+ meth = collection? ? :tableize : :singularize
62
+ ActiveSupport::Inflector.public_send(meth, demodulized.delete_suffix('Resource').downcase)
63
+ else
64
+ @_key.to_s
65
+ end
66
+ end
67
+
72
68
  def converter
73
- lambda do |resource|
74
- @_attributes.map do |key, attribute|
75
- [transform_key(key), fetch_attribute(resource, attribute)]
76
- end.to_h
69
+ lambda do |object|
70
+ arrays = @_attributes.map do |key, attribute|
71
+ key = transform_key(key)
72
+ if attribute.is_a?(Array) # Conditional
73
+ conditional_attribute(object, key, attribute)
74
+ else
75
+ [key, fetch_attribute(object, attribute)]
76
+ end
77
+ rescue ::Alba::Error, FrozenError
78
+ raise
79
+ rescue StandardError => e
80
+ handle_error(e, object, key, attribute)
81
+ end
82
+ arrays.reject(&:empty?).to_h
83
+ end
84
+ end
85
+
86
+ def conditional_attribute(object, key, attribute)
87
+ condition = attribute.last
88
+ arity = condition.arity
89
+ return [] if arity <= 1 && !condition.call(object)
90
+
91
+ fetched_attribute = fetch_attribute(object, attribute.first)
92
+ attr = if attribute.first.is_a?(Alba::Association)
93
+ attribute.first.object
94
+ else
95
+ fetched_attribute
96
+ end
97
+ return [] if arity >= 2 && !condition.call(object, attr)
98
+
99
+ [key, fetched_attribute]
100
+ end
101
+
102
+ def handle_error(error, object, key, attribute)
103
+ on_error = @_on_error || Alba._on_error
104
+ case on_error
105
+ when :raise, nil
106
+ raise
107
+ when :nullify
108
+ [key, nil]
109
+ when :ignore
110
+ []
111
+ when Proc
112
+ on_error.call(error, object, key, attribute, self.class)
113
+ else
114
+ raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
77
115
  end
78
116
  end
79
- # rubocop:enable Style/MethodCalledOnDoEndBlock
80
117
 
81
118
  # Override this method to supply custom key transform method
82
119
  def transform_key(key)
@@ -86,31 +123,19 @@ module Alba
86
123
  KeyTransformer.transform(key, @_transform_keys)
87
124
  end
88
125
 
89
- def fetch_attribute(resource, attribute)
126
+ def fetch_attribute(object, attribute)
90
127
  case attribute
91
128
  when Symbol
92
- resource.public_send attribute
129
+ object.public_send attribute
93
130
  when Proc
94
- instance_exec(resource, &attribute)
131
+ instance_exec(object, &attribute)
95
132
  when Alba::One, Alba::Many
96
- attribute.to_hash(resource, params: params)
133
+ attribute.to_hash(object, params: params)
97
134
  else
98
135
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
99
136
  end
100
137
  end
101
138
 
102
- def empty_serializer
103
- klass = Class.new
104
- klass.include Alba::Serializer
105
- klass
106
- end
107
-
108
- def inline_extended_serializer(with)
109
- klass = empty_serializer
110
- klass.class_eval(&with)
111
- klass
112
- end
113
-
114
139
  def collection?
115
140
  @object.is_a?(Enumerable)
116
141
  end
@@ -129,19 +154,24 @@ module Alba
129
154
  # Set multiple attributes at once
130
155
  #
131
156
  # @param attrs [Array<String, Symbol>]
132
- def attributes(*attrs)
133
- attrs.each { |attr_name| @_attributes[attr_name.to_sym] = attr_name.to_sym }
157
+ # @param options [Hash] option hash including `if` that is a condition to render these attributes
158
+ def attributes(*attrs, **options)
159
+ attrs.each do |attr_name|
160
+ attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
161
+ @_attributes[attr_name.to_sym] = attr
162
+ end
134
163
  end
135
164
 
136
165
  # Set an attribute with the given block
137
166
  #
138
167
  # @param name [String, Symbol] key name
168
+ # @param options [Hash] option hash including `if` that is a condition to render
139
169
  # @param block [Block] the block called during serialization
140
170
  # @raise [ArgumentError] if block is absent
141
- def attribute(name, &block)
171
+ def attribute(name, **options, &block)
142
172
  raise ArgumentError, 'No block given in attribute method' unless block
143
173
 
144
- @_attributes[name.to_sym] = block
174
+ @_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
145
175
  end
146
176
 
147
177
  # Set One association
@@ -150,10 +180,13 @@ module Alba
150
180
  # @param condition [Proc]
151
181
  # @param resource [Class<Alba::Resource>]
152
182
  # @param key [String, Symbol] used as key when given
183
+ # @param options [Hash] option hash including `if` that is a condition to render
153
184
  # @param block [Block]
154
185
  # @see Alba::One#initialize
155
- def one(name, condition = nil, resource: nil, key: nil, &block)
156
- @_attributes[key&.to_sym || name.to_sym] = One.new(name: name, condition: condition, resource: resource, &block)
186
+ def one(name, condition = nil, resource: nil, key: nil, **options, &block)
187
+ nesting = self.name&.rpartition('::')&.first
188
+ one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
189
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one
157
190
  end
158
191
  alias has_one one
159
192
 
@@ -163,25 +196,27 @@ module Alba
163
196
  # @param condition [Proc]
164
197
  # @param resource [Class<Alba::Resource>]
165
198
  # @param key [String, Symbol] used as key when given
199
+ # @param options [Hash] option hash including `if` that is a condition to render
166
200
  # @param block [Block]
167
201
  # @see Alba::Many#initialize
168
- def many(name, condition = nil, resource: nil, key: nil, &block)
169
- @_attributes[key&.to_sym || name.to_sym] = Many.new(name: name, condition: condition, resource: resource, &block)
202
+ def many(name, condition = nil, resource: nil, key: nil, **options, &block)
203
+ nesting = self.name&.rpartition('::')&.first
204
+ many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
205
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
170
206
  end
171
207
  alias has_many many
172
208
 
173
- # Set serializer for the resource
174
- #
175
- # @param name [Alba::Serializer]
176
- def serializer(name)
177
- @_serializer = name <= Alba::Serializer ? name : nil
178
- end
179
-
180
209
  # Set key
181
210
  #
182
211
  # @param key [String, Symbol]
183
212
  def key(key)
184
- @_key = key.to_sym
213
+ @_key = key.respond_to?(:to_sym) ? key.to_sym : key
214
+ end
215
+
216
+ # Set key to true
217
+ #
218
+ def key!
219
+ @_key = true
185
220
  end
186
221
 
187
222
  # Delete attributes
@@ -200,6 +235,17 @@ module Alba
200
235
  def transform_keys(type)
201
236
  @_transform_keys = type.to_sym
202
237
  end
238
+
239
+ # Set error handler
240
+ #
241
+ # @param [Symbol] handler
242
+ # @param [Block]
243
+ def on_error(handler = nil, &block)
244
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
245
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
246
+
247
+ @_on_error = handler || block
248
+ end
203
249
  end
204
250
  end
205
251
  end
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '0.13.1'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
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: 0.13.1
4
+ version: 1.0.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-03-24 00:00:00.000000000 Z
11
+ date: 2021-04-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Alba is designed to be a simple, easy to use and fast alternative to
14
14
  existing JSON serializers. Its performance is better than almost all gems which
@@ -23,9 +23,9 @@ files:
23
23
  - ".gitignore"
24
24
  - ".rubocop.yml"
25
25
  - ".yardopts"
26
+ - CHANGELOG.md
26
27
  - CODE_OF_CONDUCT.md
27
28
  - Gemfile
28
- - Gemfile.lock
29
29
  - LICENSE.txt
30
30
  - README.md
31
31
  - Rakefile
@@ -39,7 +39,6 @@ files:
39
39
  - lib/alba/many.rb
40
40
  - lib/alba/one.rb
41
41
  - lib/alba/resource.rb
42
- - lib/alba/serializer.rb
43
42
  - lib/alba/version.rb
44
43
  - sider.yml
45
44
  homepage: https://github.com/okuramasafumi/alba
@@ -64,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
63
  - !ruby/object:Gem::Version
65
64
  version: '0'
66
65
  requirements: []
67
- rubygems_version: 3.2.11
66
+ rubygems_version: 3.2.14
68
67
  signing_key:
69
68
  specification_version: 4
70
69
  summary: Alba is the fastest JSON serializer for Ruby.
data/Gemfile.lock DELETED
@@ -1,92 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- alba (0.13.1)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- activesupport (6.1.3)
10
- concurrent-ruby (~> 1.0, >= 1.0.2)
11
- i18n (>= 1.6, < 2)
12
- minitest (>= 5.1)
13
- tzinfo (~> 2.0)
14
- zeitwerk (~> 2.3)
15
- ast (2.4.2)
16
- concurrent-ruby (1.1.8)
17
- coveralls (0.8.23)
18
- json (>= 1.8, < 3)
19
- simplecov (~> 0.16.1)
20
- term-ansicolor (~> 1.3)
21
- thor (>= 0.19.4, < 2.0)
22
- tins (~> 1.6)
23
- docile (1.3.2)
24
- i18n (1.8.9)
25
- concurrent-ruby (~> 1.0)
26
- json (2.3.1)
27
- minitest (5.14.3)
28
- oj (3.11.2)
29
- parallel (1.20.1)
30
- parser (3.0.0.0)
31
- ast (~> 2.4.1)
32
- rainbow (3.0.0)
33
- rake (13.0.3)
34
- regexp_parser (2.1.1)
35
- rexml (3.2.4)
36
- rubocop (1.11.0)
37
- parallel (~> 1.10)
38
- parser (>= 3.0.0.0)
39
- rainbow (>= 2.2.2, < 4.0)
40
- regexp_parser (>= 1.8, < 3.0)
41
- rexml
42
- rubocop-ast (>= 1.2.0, < 2.0)
43
- ruby-progressbar (~> 1.7)
44
- unicode-display_width (>= 1.4.0, < 3.0)
45
- rubocop-ast (1.4.1)
46
- parser (>= 2.7.1.5)
47
- rubocop-minitest (0.10.3)
48
- rubocop (>= 0.87, < 2.0)
49
- rubocop-performance (1.10.1)
50
- rubocop (>= 0.90.0, < 2.0)
51
- rubocop-ast (>= 0.4.0)
52
- rubocop-rake (0.5.1)
53
- rubocop
54
- rubocop-sensible (0.3.0)
55
- rubocop (>= 0.60.0)
56
- ruby-progressbar (1.11.0)
57
- simplecov (0.16.1)
58
- docile (~> 1.1)
59
- json (>= 1.8, < 3)
60
- simplecov-html (~> 0.10.0)
61
- simplecov-html (0.10.2)
62
- sync (0.5.0)
63
- term-ansicolor (1.7.1)
64
- tins (~> 1.0)
65
- thor (1.0.1)
66
- tins (1.25.0)
67
- sync
68
- tzinfo (2.0.4)
69
- concurrent-ruby (~> 1.0)
70
- unicode-display_width (2.0.0)
71
- yard (0.9.26)
72
- zeitwerk (2.4.2)
73
-
74
- PLATFORMS
75
- ruby
76
-
77
- DEPENDENCIES
78
- activesupport
79
- alba!
80
- coveralls
81
- minitest (~> 5.14)
82
- oj (~> 3.11)
83
- rake (~> 13.0)
84
- rubocop (>= 0.79.0)
85
- rubocop-minitest (~> 0.10.3)
86
- rubocop-performance (~> 1.10.1)
87
- rubocop-rake (>= 0.5.1)
88
- rubocop-sensible (~> 0.3.0)
89
- yard
90
-
91
- BUNDLED WITH
92
- 2.2.6
@@ -1,75 +0,0 @@
1
- module Alba
2
- # This module represents how a resource should be serialized.
3
- module Serializer
4
- # @!parse include InstanceMethods
5
- # @!parse extend ClassMethods
6
-
7
- # @private
8
- def self.included(base)
9
- super
10
- base.instance_variable_set('@_opts', {}) unless base.instance_variable_defined?('@_opts')
11
- base.instance_variable_set('@_metadata', {}) unless base.instance_variable_defined?('@_metadata')
12
- base.include InstanceMethods
13
- base.extend ClassMethods
14
- end
15
-
16
- # Instance methods
17
- module InstanceMethods
18
- # @param resource [Alba::Resource]
19
- def initialize(resource)
20
- @resource = resource
21
- @hash = resource.serializable_hash
22
- @hash = {key.to_sym => @hash} if key
23
- return if metadata.empty?
24
-
25
- # @hash is either Hash or Array
26
- @hash.is_a?(Hash) ? @hash.merge!(metadata.to_h) : @hash << metadata
27
- end
28
-
29
- # Use real encoder to actually serialize to JSON
30
- #
31
- # @return [String] JSON string
32
- def serialize
33
- Alba.encoder.call(@hash)
34
- end
35
-
36
- private
37
-
38
- def key
39
- opts = self.class._opts
40
- opts[:key] == true ? @resource.key : opts[:key]
41
- end
42
-
43
- def metadata
44
- metadata = self.class._metadata
45
- metadata.transform_values { |block| block.call(@resource.object) }
46
- end
47
- end
48
-
49
- # Class methods
50
- module ClassMethods
51
- attr_reader :_opts, :_metadata
52
-
53
- # @private
54
- def inherited(subclass)
55
- super
56
- %w[_opts _metadata].each { |name| subclass.instance_variable_set("@#{name}", public_send(name).clone) }
57
- end
58
-
59
- # Set options, currently key only
60
- #
61
- # @param key [Boolean, Symbol]
62
- def set(key: false)
63
- @_opts[:key] = key
64
- end
65
-
66
- # Set metadata
67
- #
68
- # @param name [String, Symbol] key for the metadata
69
- # @param block [Block] the content of the metadata
70
- def metadata(name, &block)
71
- @_metadata[name.to_sym] = block
72
- end
73
- end
74
- end
75
- end