alba 3.5.0 → 3.7.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +96 -40
  4. data/lib/alba/association.rb +4 -1
  5. data/lib/alba/conditional_attribute.rb +11 -14
  6. data/lib/alba/layout.rb +8 -6
  7. data/lib/alba/railtie.rb +2 -2
  8. data/lib/alba/resource.rb +90 -17
  9. data/lib/alba/typed_attribute.rb +2 -0
  10. data/lib/alba/version.rb +1 -1
  11. data/lib/alba.rb +51 -32
  12. metadata +5 -53
  13. data/.codeclimate.yml +0 -12
  14. data/.editorconfig +0 -10
  15. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -26
  16. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  17. data/.github/dependabot.yml +0 -12
  18. data/.github/workflows/codeql-analysis.yml +0 -70
  19. data/.github/workflows/lint.yml +0 -17
  20. data/.github/workflows/main.yml +0 -41
  21. data/.gitignore +0 -11
  22. data/.rubocop.yml +0 -156
  23. data/.yardopts +0 -4
  24. data/CODE_OF_CONDUCT.md +0 -132
  25. data/CONTRIBUTING.md +0 -30
  26. data/Gemfile +0 -39
  27. data/HACKING.md +0 -42
  28. data/Rakefile +0 -17
  29. data/SECURITY.md +0 -12
  30. data/alba.gemspec +0 -33
  31. data/benchmark/Gemfile +0 -26
  32. data/benchmark/README.md +0 -137
  33. data/benchmark/collection.rb +0 -297
  34. data/benchmark/prep.rb +0 -56
  35. data/benchmark/single_resource.rb +0 -300
  36. data/bin/console +0 -15
  37. data/bin/setup +0 -8
  38. data/codecov.yml +0 -8
  39. data/docs/migrate_from_active_model_serializers.md +0 -359
  40. data/docs/migrate_from_jbuilder.md +0 -237
  41. data/docs/rails.md +0 -56
  42. data/gemfiles/without_active_support.gemfile +0 -19
  43. data/gemfiles/without_oj.gemfile +0 -19
  44. data/logo/alba-card.png +0 -0
  45. data/logo/alba-sign.png +0 -0
  46. data/logo/alba-typography.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad9473eff6bbd8fb5f282d2ad7c9d84ad3f104b0a5e190999bd7788fcec2d8be
4
- data.tar.gz: f778882ced7677d043694ba257df58bea4fb09fd108804fd4c0a28b7c722cc20
3
+ metadata.gz: 14523f0f31f5993205b02095c3e6a8115ef83728961cf720dde4d9599be3168a
4
+ data.tar.gz: 9a2de9344c9b7bf95daa07e0d61339a08285f0cdadf9b694b58f73e7b0773970
5
5
  SHA512:
6
- metadata.gz: 484f8725e3a71a7d7714f2d19e0c005c11fd995c33e59431bef48f1dac8336360fef3fdd7657f47984bded1753e366a85b4e8bd7101bd0e418c658a508788ccf
7
- data.tar.gz: 0e7c53e58d3e9146b775b06d929b37e9860dba366114db7aca21e53628e78f946dd0ac13c7dc2788b0cc7fb10eb7211b9afc0c7ecf7c4bedd892786a07c04ea2
6
+ metadata.gz: e7f8062cca1bbccbe4d4e871fd5ad2ab42a5b25acc0d2b9edd3e8247af021edaca981988eeb8428675c8bdc8bcbc14fa5ee45d90d524ca76987ab469fc2a6203
7
+ data.tar.gz: ea0cd1598c2ef8f76e740309ff925b8c26ef60bef6174755b23549d3fd9a98aefeab26bb6d47b73c92c9b92d84125717b157369f25ba2d3631532bec522a9765
data/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.7.0] 2025-05-08
10
+
11
+ ### Added
12
+
13
+ - Traits [#434](https://github.com/okuramasafumi/alba/pull/434)
14
+
15
+ ## [3.6.0] 2025-03-11
16
+
17
+ ### Added
18
+
19
+ - Add serializer keyword argument as an alias for resource [#408](https://github.com/okuramasafumi/alba/pull/408)
20
+ - `Alba.resource_for` as a replacement to `resource_with`
21
+ - Hash serialization [#427](https://github.com/okuramasafumi/alba/pull/427)
22
+
23
+ ### Changed
24
+
25
+ - Performance improvements [#421](https://github.com/okuramasafumi/alba/pull/421) and [#423](https://github.com/okuramasafumi/alba/pull/423)
26
+
27
+ ### Fixed
28
+
29
+ - Range is not a collection [#417](https://github.com/okuramasafumi/alba/issues/417)
30
+
31
+ ### Deprecated
32
+
33
+ - `Alba.resource_with` is deprecated in favor of `Alba.resource_for`
34
+ - Overriding `Alba::Resource#attributes` is now deprecated in favor of `Alba::Resource#select`
35
+
9
36
  ## [3.5.0] 2025-01-01
10
37
 
11
38
  ### 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
 
@@ -836,48 +836,11 @@ In this example we add `baz` attribute and change `root_key`. This way, you can
836
836
 
837
837
  ### Filtering attributes
838
838
 
839
- Filtering attributes can be done in two ways - with `attributes` and `select`. They have different semantics and usage.
840
-
841
- `select` is a new and more intuitive API, so generally it's recommended to use `select`.
842
-
843
- #### Filtering attributes with `attributes`
844
-
845
- You can filter out certain attributes by overriding `attributes` instance method. This is useful when you want to customize existing resource with inheritance.
846
-
847
- You can access raw attributes via `super` call. It returns a Hash whose keys are the name of the attribute and whose values are the body. Usually you need only keys to filter out, like below.
848
-
849
- ```ruby
850
- class Foo
851
- attr_accessor :id, :name, :body
852
-
853
- def initialize(id, name, body)
854
- @id = id
855
- @name = name
856
- @body = body
857
- end
858
- end
859
-
860
- class GenericFooResource
861
- include Alba::Resource
862
-
863
- attributes :id, :name, :body
864
- end
865
-
866
- class RestrictedFooResource < GenericFooResource
867
- def attributes
868
- super.select { |key, _| key.to_sym == :name }
869
- end
870
- end
871
-
872
- foo = Foo.new(1, 'my foo', 'body')
873
-
874
- RestrictedFooResource.new(foo).serialize
875
- # => '{"name":"my foo"}'
876
- ```
839
+ To filter attributes, you can use `select` instance method. Using `attributes` instance method is deprecated and will be removed in the future.
877
840
 
878
841
  #### Filtering attributes with `select`
879
842
 
880
- When you want to filter attributes based on more complex logic, you can use `select` instance method. `select` takes two parameters, the name of an attribute and the value of an attribute. If it returns false that attribute is rejected.
843
+ `select` takes two or three parameters, the name of an attribute, the value of an attribute and the attribute object (`Alba::Association`, for example). If it returns false that attribute is rejected.
881
844
 
882
845
  ```ruby
883
846
  class Foo
@@ -900,6 +863,9 @@ class RestrictedFooResource < GenericFooResource
900
863
  def select(_key, value)
901
864
  !value.nil?
902
865
  end
866
+
867
+ # This is also possible
868
+ # def select(_key, _value, _attribute)
903
869
  end
904
870
 
905
871
  foo = Foo.new(1, nil, 'body')
@@ -1107,6 +1073,40 @@ class UserResource2
1107
1073
  end
1108
1074
  ```
1109
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
+
1110
1110
  ### Default
1111
1111
 
1112
1112
  Alba doesn't support default value for attributes, but it's easy to set a default value.
@@ -1841,6 +1841,58 @@ Here, we override `serialize` method with `prepend`. In overridden method we pri
1841
1841
 
1842
1842
  Don't forget calling `super` in this way.
1843
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
+
1844
1896
  ## Rails
1845
1897
 
1846
1898
  When you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.
@@ -1874,6 +1926,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
1874
1926
 
1875
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).
1876
1928
 
1929
+ ## Versioning
1930
+
1931
+ Alba follows [Semver 2.0.0](https://semver.org/spec/v2.0.0.html).
1932
+
1877
1933
  ## License
1878
1934
 
1879
1935
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -32,6 +32,9 @@ module Alba
32
32
  end
33
33
 
34
34
  # This is the same API in `NestedAttribute`
35
+ #
36
+ # @param type [String, Symbol] one of `snake`, `:camel`, `:lower_camel`, `:dash` and `none`
37
+ # @return [void]
35
38
  def key_transformation=(type)
36
39
  @resource.transform_keys(type) unless @resource.is_a?(Proc)
37
40
  end
@@ -44,7 +47,7 @@ module Alba
44
47
  # @return [Hash]
45
48
  def to_h(target, within: nil, params: {})
46
49
  params = params.merge(@params)
47
- object = target.__send__(@name)
50
+ object = target.is_a?(Hash) ? target.fetch(@name) : target.__send__(@name)
48
51
  object = @condition.call(object, params, target) if @condition
49
52
  return if object.nil?
50
53
 
@@ -2,13 +2,12 @@
2
2
 
3
3
  require_relative 'association'
4
4
  require_relative 'constants'
5
- require 'ostruct'
6
5
 
7
6
  module Alba
8
7
  # Represents attribute with `if` option
9
8
  # @api private
10
9
  class ConditionalAttribute
11
- # @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute] real attribute wrapped with condition
10
+ # @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute, Alba::NestedAttribute] real attribute wrapped with condition
12
11
  # @param condition [Symbol, Proc] condition to check
13
12
  def initialize(body:, condition:)
14
13
  @body = body
@@ -26,7 +25,7 @@ module Alba
26
25
  fetched_attribute = yield(@body)
27
26
  return fetched_attribute unless with_two_arity_proc_condition
28
27
 
29
- return Alba::REMOVE_KEY unless resource.instance_exec(object, objectize(fetched_attribute), &@condition)
28
+ return Alba::REMOVE_KEY unless resource.instance_exec(object, second_object(object), &@condition)
30
29
 
31
30
  fetched_attribute
32
31
  end
@@ -51,17 +50,15 @@ module Alba
51
50
  @condition.is_a?(Proc) && @condition.arity >= 2
52
51
  end
53
52
 
54
- # OpenStruct is used as a simple solution for converting Hash or Array of Hash into an object
55
- # Using OpenStruct is not good in general, but in this case there's no other solution
56
- def objectize(fetched_attribute)
57
- return fetched_attribute unless @body.is_a?(Alba::Association)
58
-
59
- if fetched_attribute.is_a?(Array)
60
- fetched_attribute.map do |hash|
61
- OpenStruct.new(hash)
62
- end
63
- else
64
- OpenStruct.new(fetched_attribute)
53
+ def second_object(object)
54
+ case @body
55
+ when Symbol, Alba::Association, Alba::TypedAttribute
56
+ object.__send__(@body.name)
57
+ when Alba::NestedAttribute
58
+ nil
59
+ when Proc
60
+ @body.call(object)
61
+ else raise Alba::Error, "Unreachable code, @body is: #{@body.inspect}"
65
62
  end
66
63
  end
67
64
  end
data/lib/alba/layout.rb CHANGED
@@ -15,13 +15,9 @@ module Alba
15
15
  # @param inline [Proc] a proc returning JSON string or a Hash representing JSON
16
16
  def initialize(file:, inline:)
17
17
  @body = if file
18
- raise ArgumentError, 'File layout must be a String representing filename' unless file.is_a?(String)
19
-
20
- file
18
+ check_and_return(file, 'File layout must be a String representing filename', String)
21
19
  elsif inline
22
- raise ArgumentError, 'Inline layout must be a Proc returning a Hash or a String' unless inline.is_a?(Proc)
23
-
24
- inline
20
+ check_and_return(inline, 'Inline layout must be a Proc returning a Hash or a String', Proc)
25
21
  else
26
22
  raise ArgumentError, 'Layout must be either String or Proc'
27
23
  end
@@ -47,6 +43,12 @@ module Alba
47
43
 
48
44
  attr_reader :serialized_json
49
45
 
46
+ def check_and_return(obj, message, klass)
47
+ raise ArgumentError, message unless obj.is_a?(klass)
48
+
49
+ obj
50
+ end
51
+
50
52
  def serialize_within_string_layout(bnd)
51
53
  ERB.new(File.read(@body)).result(bnd)
52
54
  end
data/lib/alba/railtie.rb CHANGED
@@ -8,12 +8,12 @@ module Alba
8
8
 
9
9
  ActiveSupport.on_load(:action_controller) do
10
10
  define_method(:serialize) do |obj, with: nil, root_key: nil, meta: {}, &block|
11
- resource = with.nil? ? Alba.resource_with(obj, &block) : with.new(obj)
11
+ resource = with.nil? ? Alba.resource_for(obj, &block) : with.new(obj)
12
12
  resource.to_json(root_key: root_key, meta: meta)
13
13
  end
14
14
 
15
15
  define_method(:render_serialized_json) do |obj, with: nil, root_key: nil, meta: {}, &block|
16
- json = with.nil? ? Alba.resource_with(obj, &block) : with.new(obj)
16
+ json = with.nil? ? Alba.resource_for(obj, &block) : with.new(obj)
17
17
  render json: json.to_json(root_key: root_key, meta: meta)
18
18
  end
19
19
  end
data/lib/alba/resource.rb CHANGED
@@ -14,14 +14,14 @@ 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: []}.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
21
21
  private_constant :WITHIN_DEFAULT
22
22
 
23
23
  # `setup` method is meta-programmatically defined here for performance.
24
- # @private
24
+ # @api private
25
25
  def self.included(base) # rubocop:disable Metrics/MethodLength
26
26
  super
27
27
  base.class_eval do
@@ -46,10 +46,12 @@ 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
+ def initialize(object, params: {}, within: WITHIN_DEFAULT, with_traits: nil)
50
51
  @object = object
51
52
  @params = params
52
53
  @within = within
54
+ @with_traits = with_traits
53
55
  _setup
54
56
  end
55
57
 
@@ -68,7 +70,8 @@ module Alba
68
70
  # @see #serialize
69
71
  # @see https://github.com/rails/rails/blob/7-0-stable/actionpack/lib/action_controller/metal/renderers.rb#L156
70
72
  def to_json(options = {}, root_key: nil, meta: {})
71
- confusing_options = options.keys.select { |k| k.to_sym == :only || k.to_sym == :except }
73
+ confusing_keys = [:only, :except]
74
+ confusing_options = options.keys.select { |k| confusing_keys.include?(k.to_sym) }
72
75
  unless confusing_options.empty?
73
76
  confusing_options.sort!
74
77
  confusing_options.map! { |s| "\"#{s}\"" }
@@ -99,12 +102,30 @@ module Alba
99
102
  #
100
103
  # @return [Hash]
101
104
  def serializable_hash
102
- Alba.collection?(@object) ? serializable_hash_for_collection : converter.call(@object)
105
+ Alba.collection?(@object) ? serializable_hash_for_collection : attributes_to_hash(@object, {})
103
106
  end
104
107
  alias to_h serializable_hash
105
108
 
106
109
  private
107
110
 
111
+ def hash_from_traits(obj)
112
+ h = {}
113
+ return h if @with_traits.nil?
114
+
115
+ Array(@with_traits).each do |trait|
116
+ body = @_traits.fetch(trait) { raise Alba::Error, "Trait not found: #{trait}" }
117
+
118
+ resource_class = Alba.resource_class
119
+ resource_class.class_eval(&body)
120
+ h.merge!(resource_class.new(obj, params: params, within: @within).serializable_hash)
121
+ end
122
+ h
123
+ end
124
+
125
+ def deprecated_serializable_hash
126
+ Alba.collection?(@object) ? serializable_hash_for_collection : converter.call(@object)
127
+ end
128
+
108
129
  def serialize_with(hash)
109
130
  serialized_json = encode(hash)
110
131
  return serialized_json unless @_layout
@@ -130,6 +151,18 @@ module Alba
130
151
  end
131
152
 
132
153
  def serializable_hash_for_collection
154
+ if @_collection_key
155
+ @object.to_h do |item|
156
+ k = item.public_send(@_collection_key)
157
+ key = Alba.regularize_key(k)
158
+ [key, attributes_to_hash(item, {})]
159
+ end
160
+ else
161
+ @object.map { |obj| attributes_to_hash(obj, {}) }
162
+ end
163
+ end
164
+
165
+ def deprecated_serializable_hash_for_collection
133
166
  if @_collection_key
134
167
  @object.to_h do |item|
135
168
  k = item.public_send(@_collection_key)
@@ -195,27 +228,35 @@ module Alba
195
228
  rescue StandardError => e
196
229
  handle_error(e, obj, key, attribute, hash)
197
230
  end
198
- hash
231
+ @with_traits.nil? ? hash : hash.merge!(hash_from_traits(obj))
199
232
  end
200
233
 
201
234
  # This is default behavior for getting attributes for serialization
202
235
  # Override this method to filter certain attributes
236
+ #
237
+ # @deprecated in favor of `select`
203
238
  def attributes
204
239
  @_attributes
205
240
  end
206
241
 
207
242
  # Default implementation for selecting attributes
208
243
  # Override this method to filter attributes based on key and value
209
- def select(_key, _value)
244
+ def select(_key, _value, _attribute)
210
245
  true
211
246
  end
212
247
 
213
248
  def set_key_and_attribute_body_from(obj, key, attribute, hash)
214
249
  key = transform_key(key)
215
250
  value = fetch_attribute(obj, key, attribute)
216
- return unless select(key, value)
251
+ # When `select` is not overridden, skip calling it for better performance
252
+ unless @_select_arity.nil?
253
+ # `select` can be overridden with both 2 and 3 parameters
254
+ # Here we check the arity and build arguments accordingly
255
+ args = @_select_arity == 3 ? [key, value, attribute] : [key, value]
256
+ return unless select(*args)
257
+ end
217
258
 
218
- hash[key] = value unless value == Alba::REMOVE_KEY
259
+ hash[key] = value unless Alba::REMOVE_KEY == value # rubocop:disable Style/YodaCondition
219
260
  end
220
261
 
221
262
  def handle_error(error, obj, key, attribute, hash)
@@ -259,12 +300,16 @@ module Alba
259
300
  end
260
301
 
261
302
  def _fetch_attribute_from_object_first(obj, attribute)
303
+ return obj.fetch(attribute) if obj.is_a?(Hash)
304
+
262
305
  obj.__send__(attribute)
263
306
  rescue NoMethodError
264
307
  __send__(attribute, obj)
265
308
  end
266
309
 
267
310
  def _fetch_attribute_from_resource_first(obj, attribute)
311
+ return obj.fetch(attribute) if obj.is_a?(Hash)
312
+
268
313
  if @_resource_methods.include?(attribute)
269
314
  __send__(attribute, obj)
270
315
  else
@@ -299,12 +344,27 @@ module Alba
299
344
  attr_reader(*INTERNAL_VARIABLES.keys)
300
345
 
301
346
  # This `method_added` is used for defining "resource methods"
302
- def method_added(method_name)
303
- _resource_methods << method_name.to_sym unless method_name.to_sym == :_setup
347
+ def method_added(method_name) # rubocop:disable Metrics/MethodLength
348
+ case method_name
349
+ when :collection_converter, :converter
350
+ warn "Defining ##{method_name} methods is deprecated", category: :deprecated, uplevel: 1
351
+ alias_method :serializable_hash_for_collection, :deprecated_serializable_hash_for_collection
352
+ private(:serializable_hash_for_collection)
353
+ alias_method :serializable_hash, :deprecated_serializable_hash
354
+ alias_method :to_h, :deprecated_serializable_hash
355
+ when :attributes
356
+ warn 'Overriding `attributes` is deprecated, use `select` instead.', category: :deprecated, uplevel: 1
357
+ when :select
358
+ @_select_arity = instance_method(:select).arity
359
+ when :_setup # noop
360
+ else
361
+ _resource_methods << method_name.to_sym
362
+ end
363
+
304
364
  super
305
365
  end
306
366
 
307
- # @private
367
+ # @api private
308
368
  def inherited(subclass)
309
369
  super
310
370
  INTERNAL_VARIABLES.each_key { |name| subclass.instance_variable_set(:"@#{name}", instance_variable_get(:"@#{name}").clone) }
@@ -316,7 +376,7 @@ module Alba
316
376
  #
317
377
  # @param attrs [Array<String, Symbol>]
318
378
  # @param if [Proc] condition to decide if it should serialize these attributes
319
- # @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
379
+ # @param attrs_with_types [Hash{Symbol, String => Array<Symbol, Proc>, Symbol}]
320
380
  # attributes with name in its key and type and optional type converter in its value
321
381
  # @return [void]
322
382
  def attributes(*attrs, if: nil, **attrs_with_types)
@@ -364,6 +424,7 @@ module Alba
364
424
  # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
365
425
  # @param condition [Proc, nil] a Proc to modify the association
366
426
  # @param resource [Class<Alba::Resource>, String, Proc, nil] representing resource for this association
427
+ # @param serializer [Class<Alba::Resource>, String, Proc, nil] alias for `resource`
367
428
  # @param key [String, Symbol, nil] used as key when given
368
429
  # @param params [Hash] params override for the association
369
430
  # @param options [Hash<Symbol, Proc>]
@@ -371,7 +432,8 @@ module Alba
371
432
  # @param block [Block]
372
433
  # @return [void]
373
434
  # @see Alba::Association#initialize
374
- def association(name, condition = nil, resource: nil, key: nil, params: {}, **options, &block)
435
+ def association(name, condition = nil, resource: nil, serializer: nil, key: nil, params: {}, **options, &block)
436
+ resource ||= serializer
375
437
  transformation = @_key_transformation_cascade ? @_transform_type : :none
376
438
  assoc = Association.new(
377
439
  name: name, condition: condition, resource: resource, params: params, nesting: nesting, key_transformation: transformation, helper: @_helper, &block
@@ -409,6 +471,19 @@ module Alba
409
471
  end
410
472
  alias nested nested_attribute
411
473
 
474
+ # Set a trait
475
+ #
476
+ # @param name [String, Symbol] name of the trait
477
+ # @param block [Block] the "content" of the trait
478
+ # @raise [ArgumentError] if block is absent
479
+ # @return [void]
480
+ def trait(name, &block)
481
+ raise ArgumentError, 'No block given in trait method' unless block
482
+
483
+ name = name.to_sym
484
+ @_traits[name] = block
485
+ end
486
+
412
487
  # Set root key
413
488
  #
414
489
  # @param key [String, Symbol]
@@ -477,9 +552,7 @@ module Alba
477
552
  if @_key_transformation_cascade
478
553
  # We need to update key transformation of associations and nested attributes
479
554
  @_attributes.each_value do |attr|
480
- next unless attr.is_a?(Association) || attr.is_a?(NestedAttribute)
481
-
482
- attr.key_transformation = type
555
+ attr.key_transformation = type if attr.is_a?(Association) || attr.is_a?(NestedAttribute)
483
556
  end
484
557
  end
485
558
  self # Return the new class
@@ -4,6 +4,8 @@ module Alba
4
4
  # Representing typed attributes to encapsulate logic about types
5
5
  # @api private
6
6
  class TypedAttribute
7
+ attr_reader :name
8
+
7
9
  # @param name [Symbol, String]
8
10
  # @param type [Symbol, Class]
9
11
  # @param converter [Proc]
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.5.0'
4
+ VERSION = '3.7.0'
5
5
  end