alba 3.6.0 → 3.7.1

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: 9013289126b33b20a7460d7c5092a3df8a39c7d79c7114d6f33e7111a2703a20
4
- data.tar.gz: ae73ef30b48cdc1dfee93cd5c7cb55576ec0bd818d899c0fc2a041fbe09a90f7
3
+ metadata.gz: 9179eabfb7cd37ad4403470b3c83229f7dc558085d8946d84302473f8076694c
4
+ data.tar.gz: cb7092762e4881c13171447cc2dc4a6ec2ceb077bd6527a154d5dac6d3369967
5
5
  SHA512:
6
- metadata.gz: ed86e0c309632cb9fe70752f5c7d5ddacbd529ea5c8b06ec802736e2fd745604470cf3163b43d287a6409529babf0d6c4d713c0d7b6035d4248ea33eec8e3a49
7
- data.tar.gz: 93fc142fabd4a9358f279dccd0a85c8d0a22cabb8ff452d76ef221bb7ca929aa28bbd58a20bb29bc4a0bbe955e64a2616adf2f882728540e349eef3f82418906
6
+ metadata.gz: 8baf63e1b739731f221c6a76bb452d730c703fe848853f26f91219357001832599196902e98f2266fa04631873e3bd791dca46c7898c3f16fc2909acf1a74a10
7
+ data.tar.gz: '0872f1ca1d1a263f8d7dff94ed1e965d88e9ec423213d0b631c3bf155ddaabc5905da4d3b74344950ede8a238761777100abb42749e378beba55f3796cf85dda'
data/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.7.1] 2025-05-27
10
+
11
+ ### Fixed
12
+
13
+ - `select` now works with `trait` and `nested_attribute` [#436](https://github.com/okuramasafumi/alba/pull/436)
14
+
15
+ ## [3.7.0] 2025-05-08
16
+
17
+ ### Added
18
+
19
+ - Traits [#434](https://github.com/okuramasafumi/alba/pull/434)
20
+
9
21
  ## [3.6.0] 2025-03-11
10
22
 
11
23
  ### Added
data/README.md CHANGED
@@ -122,7 +122,7 @@ Alba supports CRuby 3.0 and higher and latest JRuby and TruffleRuby.
122
122
 
123
123
  ## Documentation
124
124
 
125
- You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramasafumi/alba).
125
+ You can find the documentation on [GitHub Pages](https://okuramasafumi.github.io/alba/).
126
126
 
127
127
  ## Features
128
128
 
@@ -1073,6 +1073,40 @@ class UserResource2
1073
1073
  end
1074
1074
  ```
1075
1075
 
1076
+ ### Traits
1077
+
1078
+ Traits is an easy way to a group of attributes and apply it to the resource.
1079
+
1080
+ ```ruby
1081
+ class User
1082
+ attr_accessor :id, :name, :email
1083
+
1084
+ def initialize(id, name, email)
1085
+ @id = id
1086
+ @name = name
1087
+ @email = email
1088
+ end
1089
+ end
1090
+
1091
+ class UserResource
1092
+ include Alba::Resource
1093
+
1094
+ attributes :id
1095
+
1096
+ trait :additional do
1097
+ attributes :name, :email
1098
+ end
1099
+ end
1100
+
1101
+ user = User.new(1, 'Foo', 'foo@example.org')
1102
+ UserResource.new(user).serialize # => '{"id":1}'
1103
+ UserResource.new(user, with_traits: :additional).serialize # => '{"id":1,"name":"Foo","email":"foo@example.com"}'
1104
+ ```
1105
+
1106
+ This way, we can keep the resource class simple and inject conditions from outside. We can get the same result with the combination of `if` and `params`, but using `traits` DSL can make the resource class readable.
1107
+
1108
+ We can specify multiple traits at once with `with_traits: []` keyword argument.
1109
+
1076
1110
  ### Default
1077
1111
 
1078
1112
  Alba doesn't support default value for attributes, but it's easy to set a default value.
@@ -1807,6 +1841,58 @@ Here, we override `serialize` method with `prepend`. In overridden method we pri
1807
1841
 
1808
1842
  Don't forget calling `super` in this way.
1809
1843
 
1844
+ ## Tips and Tricks
1845
+
1846
+ ### Treating specific classes as non-collection
1847
+
1848
+ Sometimes we need to serialize an object that's `Enumerable` but not a collection. By default, Alba treats `Hash`, `Range` and `Struct` as non-collection object, but if we want to add some classes to this list, we can override `Alba.collection?` method like following:
1849
+
1850
+ ```ruby
1851
+ Alba.singleton_class.prepend(
1852
+ Module.new do
1853
+ def collection?(object)
1854
+ super && !object.is_a?(SomeClass)
1855
+ end
1856
+ end
1857
+ )
1858
+ ```
1859
+
1860
+ ### Adding indexes to `many` association
1861
+
1862
+ Let's say an author has many books. We want returned JSON to include indexes of each book. In this case, we can reduce the number of executed SQL by fetching indexes ahead and push indexes into `param`.
1863
+
1864
+ ```ruby
1865
+ Author = Data.define(:id, :books)
1866
+ Book = Data.define(:id, :name)
1867
+
1868
+ book1 = Book.new(1, 'book1')
1869
+ book2 = Book.new(2, 'book2')
1870
+ book3 = Book.new(3, 'book3')
1871
+
1872
+ author = Author.new(2, [book2, book3, book1])
1873
+
1874
+ class AuthorResource
1875
+ include Alba::Resource
1876
+
1877
+ attributes :id
1878
+ many :books do
1879
+ attributes :id, :name
1880
+ attribute :index do |bar|
1881
+ params[:index][bar.id]
1882
+ end
1883
+ end
1884
+ end
1885
+
1886
+ AuthorResource.new(
1887
+ author,
1888
+ params: {
1889
+ index: author.books.map.with_index { |book, index| [book.id, index] }
1890
+ .to_h
1891
+ }
1892
+ ).serialize
1893
+ # => {"id":2,"books":[{"id":2,"name":"book2","index":0},{"id":3,"name":"book3","index":1},{"id":1,"name":"book1","index":2}]}
1894
+ ```
1895
+
1810
1896
  ## Rails
1811
1897
 
1812
1898
  When you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.
@@ -1840,6 +1926,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
1840
1926
 
1841
1927
  Thank you for begin interested in contributing to Alba! Please see [contributors guide](https://github.com/okuramasafumi/alba/blob/main/CONTRIBUTING.md) before start contributing. If you have any questions, please feel free to ask in [Discussions](https://github.com/okuramasafumi/alba/discussions).
1842
1928
 
1929
+ ## Versioning
1930
+
1931
+ Alba follows [Semver 2.0.0](https://semver.org/spec/v2.0.0.html).
1932
+
1843
1933
  ## License
1844
1934
 
1845
1935
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -47,8 +47,7 @@ module Alba
47
47
  # @return [Hash]
48
48
  def to_h(target, within: nil, params: {})
49
49
  params = params.merge(@params)
50
- object = target.is_a?(Hash) ? target.fetch(@name) : target.__send__(@name)
51
- object = @condition.call(object, params, target) if @condition
50
+ object = object_from(target, params)
52
51
  return if object.nil?
53
52
 
54
53
  if @resource.is_a?(Proc)
@@ -62,6 +61,12 @@ module Alba
62
61
 
63
62
  private
64
63
 
64
+ def object_from(target, params)
65
+ o = target.is_a?(Hash) ? target.fetch(@name) : target.__send__(@name)
66
+ o = @condition.call(o, params, target) if @condition
67
+ o
68
+ end
69
+
65
70
  def constantize(resource)
66
71
  case resource
67
72
  when Class
@@ -17,12 +17,13 @@ module Alba
17
17
  # @param object [Object] the object being serialized
18
18
  # @param params [Hash] params Hash inherited from Resource
19
19
  # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
20
+ # @param select [Method] select method object from its origin
20
21
  # @return [Hash] hash serialized from running the class body in the object
21
- def value(object:, params:, within:)
22
+ def value(object:, params:, within:, select: nil)
22
23
  resource_class = Alba.resource_class
23
24
  resource_class.transform_keys(@key_transformation)
24
25
  resource_class.class_eval(&@block)
25
- resource_class.new(object, params: params, within: within).serializable_hash
26
+ resource_class.new(object, params: params, within: within, select: select).serializable_hash
26
27
  end
27
28
  end
28
29
  end
data/lib/alba/resource.rb CHANGED
@@ -14,7 +14,7 @@ module Alba
14
14
  module Resource
15
15
  # @!parse include InstanceMethods
16
16
  # @!parse extend ClassMethods
17
- INTERNAL_VARIABLES = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _key_transformation_cascade: true, _on_error: nil, _on_nil: nil, _layout: nil, _collection_key: nil, _helper: nil, _resource_methods: [], _select_arity: nil}.freeze # rubocop:disable Layout/LineLength
17
+ INTERNAL_VARIABLES = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _key_transformation_cascade: true, _on_error: nil, _on_nil: nil, _layout: nil, _collection_key: nil, _helper: nil, _resource_methods: [], _select_arity: nil, _traits: {}}.freeze # rubocop:disable Layout/LineLength
18
18
  private_constant :INTERNAL_VARIABLES
19
19
 
20
20
  WITHIN_DEFAULT = Object.new.freeze
@@ -46,10 +46,16 @@ module Alba
46
46
  # @param object [Object] the object to be serialized
47
47
  # @param params [Hash] user-given Hash for arbitrary data
48
48
  # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
49
- def initialize(object, params: {}, within: WITHIN_DEFAULT)
49
+ # @param with_traits [Symbol, Array<Symbol>, nil] specified traits
50
+ # @param select [Method] select method object used with `nested_attribute` and `trait`
51
+ def initialize(object, params: {}, within: WITHIN_DEFAULT, with_traits: nil, select: nil)
50
52
  @object = object
51
53
  @params = params
52
54
  @within = within
55
+ @with_traits = with_traits
56
+ # select override to share the same method with `trait` and `nested_attribute`
57
+ # Trait and NestedAttribute generates anonymous class so it checks if it's anonymous class to prevent accidental overriding
58
+ self.class.define_method(:select, &select) if select && self.class.name.nil?
53
59
  _setup
54
60
  end
55
61
 
@@ -106,6 +112,20 @@ module Alba
106
112
 
107
113
  private
108
114
 
115
+ def hash_from_traits(obj)
116
+ h = {}
117
+ return h if @with_traits.nil?
118
+
119
+ Array(@with_traits).each do |trait|
120
+ body = @_traits.fetch(trait) { raise Alba::Error, "Trait not found: #{trait}" }
121
+
122
+ resource_class = Alba.resource_class
123
+ resource_class.class_eval(&body)
124
+ h.merge!(resource_class.new(obj, params: params, within: @within, select: method(:select)).serializable_hash)
125
+ end
126
+ h
127
+ end
128
+
109
129
  def deprecated_serializable_hash
110
130
  Alba.collection?(@object) ? serializable_hash_for_collection : converter.call(@object)
111
131
  end
@@ -212,7 +232,7 @@ module Alba
212
232
  rescue StandardError => e
213
233
  handle_error(e, obj, key, attribute, hash)
214
234
  end
215
- hash
235
+ @with_traits.nil? ? hash : hash.merge!(hash_from_traits(obj))
216
236
  end
217
237
 
218
238
  # This is default behavior for getting attributes for serialization
@@ -233,16 +253,18 @@ module Alba
233
253
  key = transform_key(key)
234
254
  value = fetch_attribute(obj, key, attribute)
235
255
  # When `select` is not overridden, skip calling it for better performance
236
- unless @_select_arity.nil?
237
- # `select` can be overridden with both 2 and 3 parameters
238
- # Here we check the arity and build arguments accordingly
239
- args = @_select_arity == 3 ? [key, value, attribute] : [key, value]
240
- return unless select(*args)
241
- end
256
+ return if !@_select_arity.nil? && !do_select(key, value, attribute)
242
257
 
243
258
  hash[key] = value unless Alba::REMOVE_KEY == value # rubocop:disable Style/YodaCondition
244
259
  end
245
260
 
261
+ def do_select(key, value, attribute)
262
+ # `select` can be overridden with both 2 and 3 parameters
263
+ # Here we check the arity and build arguments accordingly
264
+ args = @_select_arity == 3 ? [key, value, attribute] : [key, value]
265
+ select(*args)
266
+ end
267
+
246
268
  def handle_error(error, obj, key, attribute, hash)
247
269
  case (on_error = @_on_error || :raise)
248
270
  when :raise, nil then raise(error)
@@ -270,7 +292,7 @@ module Alba
270
292
  when Proc then instance_exec(obj, &attribute)
271
293
  when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(obj, params: params, within: within) }
272
294
  when TypedAttribute then attribute.value { |attr| fetch_attribute(obj, key, attr) }
273
- when NestedAttribute then attribute.value(object: obj, params: params, within: @within)
295
+ when NestedAttribute then attribute.value(object: obj, params: params, within: @within, select: method(:select))
274
296
  when ConditionalAttribute then attribute.with_passing_condition(resource: self, object: obj) { |attr| fetch_attribute(obj, key, attr) }
275
297
  # :nocov:
276
298
  else raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
@@ -455,6 +477,19 @@ module Alba
455
477
  end
456
478
  alias nested nested_attribute
457
479
 
480
+ # Set a trait
481
+ #
482
+ # @param name [String, Symbol] name of the trait
483
+ # @param block [Block] the "content" of the trait
484
+ # @raise [ArgumentError] if block is absent
485
+ # @return [void]
486
+ def trait(name, &block)
487
+ raise ArgumentError, 'No block given in trait method' unless block
488
+
489
+ name = name.to_sym
490
+ @_traits[name] = block
491
+ end
492
+
458
493
  # Set root key
459
494
  #
460
495
  # @param key [String, Symbol]
data/lib/alba/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alba
4
- VERSION = '3.6.0'
4
+ VERSION = '3.7.1'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alba
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.0
4
+ version: 3.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OKURA Masafumi
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Alba is the fastest JSON serializer for Ruby. It focuses on performance,
13
13
  flexibility and usability.
@@ -40,7 +40,7 @@ licenses:
40
40
  metadata:
41
41
  bug_tracker_uri: https://github.com/okuramasafumi/alba/issues
42
42
  changelog_uri: https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md
43
- documentation_uri: https://rubydoc.info/github/okuramasafumi/alba
43
+ documentation_uri: https://okuramasafumi.github.io/alba/
44
44
  source_code_uri: https://github.com/okuramasafumi/alba
45
45
  rubygems_mfa_required: 'true'
46
46
  rdoc_options: []