alba 3.4.0 → 3.6.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 +31 -0
  3. data/README.md +7 -41
  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 +6 -5
  8. data/lib/alba/resource.rb +59 -15
  9. data/lib/alba/typed_attribute.rb +2 -0
  10. data/lib/alba/version.rb +1 -1
  11. data/lib/alba.rb +72 -38
  12. metadata +4 -52
  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 -39
  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 -29
  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 -24
  32. data/benchmark/README.md +0 -119
  33. data/benchmark/collection.rb +0 -275
  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: d1a7b2fac43a1ae40bc0598d6f989fb7b81108d00ca929d31327113590880b2f
4
- data.tar.gz: d6bc3c22c9178eeaf8b3b7f3d3d92bc526edd18904fa736c0c25488250f89d02
3
+ metadata.gz: 9013289126b33b20a7460d7c5092a3df8a39c7d79c7114d6f33e7111a2703a20
4
+ data.tar.gz: ae73ef30b48cdc1dfee93cd5c7cb55576ec0bd818d899c0fc2a041fbe09a90f7
5
5
  SHA512:
6
- metadata.gz: da62c8a7d30ab7c4c39032950c9e02cef238b8662f403843351116fb86e3a3010e2780a3b5efe958697fd698e7e703a130a292129f3efad4da253cc9e84570bd
7
- data.tar.gz: 94a580ab65397d4f61a8534bfd22999d86bdd9e6cca0ec83b15d03bd2fdbdedc220bbbaf091b20c34e58c7dfe5c7b21c9f046a78b25639dcecf59b71b95199c0
6
+ metadata.gz: ed86e0c309632cb9fe70752f5c7d5ddacbd529ea5c8b06ec802736e2fd745604470cf3163b43d287a6409529babf0d6c4d713c0d7b6035d4248ea33eec8e3a49
7
+ data.tar.gz: 93fc142fabd4a9358f279dccd0a85c8d0a22cabb8ff452d76ef221bb7ca929aa28bbd58a20bb29bc4a0bbe955e64a2616adf2f882728540e349eef3f82418906
data/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.6.0] 2025-03-11
10
+
11
+ ### Added
12
+
13
+ - Add serializer keyword argument as an alias for resource [#408](https://github.com/okuramasafumi/alba/pull/408)
14
+ - `Alba.resource_for` as a replacement to `resource_with`
15
+ - Hash serialization [#427](https://github.com/okuramasafumi/alba/pull/427)
16
+
17
+ ### Changed
18
+
19
+ - Performance improvements [#421](https://github.com/okuramasafumi/alba/pull/421) and [#423](https://github.com/okuramasafumi/alba/pull/423)
20
+
21
+ ### Fixed
22
+
23
+ - Range is not a collection [#417](https://github.com/okuramasafumi/alba/issues/417)
24
+
25
+ ### Deprecated
26
+
27
+ - `Alba.resource_with` is deprecated in favor of `Alba.resource_for`
28
+ - Overriding `Alba::Resource#attributes` is now deprecated in favor of `Alba::Resource#select`
29
+
30
+ ## [3.5.0] 2025-01-01
31
+
32
+ ### Added
33
+
34
+ - `render_serialized_json` now works with `root_key` and `meta` [#398](https://github.com/okuramasafumi/alba/pull/398)
35
+
36
+ ### Improved
37
+
38
+ - Add transform_keys caching [#402](https://github.com/okuramasafumi/alba/pull/402)
39
+
9
40
  ## [3.4.0] 2024-12-01
10
41
 
11
42
  ### Added
data/README.md CHANGED
@@ -686,7 +686,7 @@ Alba.serialize(something)
686
686
  # => Same as `FooResource.new(something).serialize` when `something` is an instance of `Foo`.
687
687
  ```
688
688
 
689
- Although this might be useful sometimes, it's generally recommended to define a class for Resource.
689
+ Although this might be useful sometimes, it's generally recommended to define a class for Resource. Defining a class is often more readable and more maintainable, and inline definitions cannot levarage the benefit of YJIT (it's the slowest with the benchmark YJIT enabled).
690
690
 
691
691
  #### Inline definition for multiple root keys
692
692
 
@@ -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')
@@ -1156,7 +1122,7 @@ end
1156
1122
  class UserResource
1157
1123
  include Alba::Resource
1158
1124
 
1159
- root_key!
1125
+ root_key! # This is required to add inferred root key, otherwise it has no root key
1160
1126
 
1161
1127
  attributes :id
1162
1128
 
@@ -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
@@ -7,13 +7,14 @@ module Alba
7
7
  Alba.inflector = :active_support
8
8
 
9
9
  ActiveSupport.on_load(:action_controller) do
10
- define_method(:serialize) do |obj, with: nil, &block|
11
- with.nil? ? Alba.resource_with(obj, &block) : with.new(obj)
10
+ define_method(:serialize) do |obj, with: nil, root_key: nil, meta: {}, &block|
11
+ resource = with.nil? ? Alba.resource_for(obj, &block) : with.new(obj)
12
+ resource.to_json(root_key: root_key, meta: meta)
12
13
  end
13
14
 
14
- define_method(:render_serialized_json) do |obj, with: nil, &block|
15
- json = with.nil? ? Alba.resource_with(obj, &block) : with.new(obj)
16
- render json: json
15
+ define_method(:render_serialized_json) do |obj, with: nil, root_key: nil, meta: {}, &block|
16
+ json = with.nil? ? Alba.resource_for(obj, &block) : with.new(obj)
17
+ render json: json.to_json(root_key: root_key, meta: meta)
17
18
  end
18
19
  end
19
20
  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}.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
@@ -68,7 +68,8 @@ module Alba
68
68
  # @see #serialize
69
69
  # @see https://github.com/rails/rails/blob/7-0-stable/actionpack/lib/action_controller/metal/renderers.rb#L156
70
70
  def to_json(options = {}, root_key: nil, meta: {})
71
- confusing_options = options.keys.select { |k| k.to_sym == :only || k.to_sym == :except }
71
+ confusing_keys = [:only, :except]
72
+ confusing_options = options.keys.select { |k| confusing_keys.include?(k.to_sym) }
72
73
  unless confusing_options.empty?
73
74
  confusing_options.sort!
74
75
  confusing_options.map! { |s| "\"#{s}\"" }
@@ -99,12 +100,16 @@ module Alba
99
100
  #
100
101
  # @return [Hash]
101
102
  def serializable_hash
102
- Alba.collection?(@object) ? serializable_hash_for_collection : converter.call(@object)
103
+ Alba.collection?(@object) ? serializable_hash_for_collection : attributes_to_hash(@object, {})
103
104
  end
104
105
  alias to_h serializable_hash
105
106
 
106
107
  private
107
108
 
109
+ def deprecated_serializable_hash
110
+ Alba.collection?(@object) ? serializable_hash_for_collection : converter.call(@object)
111
+ end
112
+
108
113
  def serialize_with(hash)
109
114
  serialized_json = encode(hash)
110
115
  return serialized_json unless @_layout
@@ -130,6 +135,18 @@ module Alba
130
135
  end
131
136
 
132
137
  def serializable_hash_for_collection
138
+ if @_collection_key
139
+ @object.to_h do |item|
140
+ k = item.public_send(@_collection_key)
141
+ key = Alba.regularize_key(k)
142
+ [key, attributes_to_hash(item, {})]
143
+ end
144
+ else
145
+ @object.map { |obj| attributes_to_hash(obj, {}) }
146
+ end
147
+ end
148
+
149
+ def deprecated_serializable_hash_for_collection
133
150
  if @_collection_key
134
151
  @object.to_h do |item|
135
152
  k = item.public_send(@_collection_key)
@@ -200,22 +217,30 @@ module Alba
200
217
 
201
218
  # This is default behavior for getting attributes for serialization
202
219
  # Override this method to filter certain attributes
220
+ #
221
+ # @deprecated in favor of `select`
203
222
  def attributes
204
223
  @_attributes
205
224
  end
206
225
 
207
226
  # Default implementation for selecting attributes
208
227
  # Override this method to filter attributes based on key and value
209
- def select(_key, _value)
228
+ def select(_key, _value, _attribute)
210
229
  true
211
230
  end
212
231
 
213
232
  def set_key_and_attribute_body_from(obj, key, attribute, hash)
214
233
  key = transform_key(key)
215
234
  value = fetch_attribute(obj, key, attribute)
216
- return unless select(key, value)
235
+ # 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
217
242
 
218
- hash[key] = value unless value == Alba::REMOVE_KEY
243
+ hash[key] = value unless Alba::REMOVE_KEY == value # rubocop:disable Style/YodaCondition
219
244
  end
220
245
 
221
246
  def handle_error(error, obj, key, attribute, hash)
@@ -259,12 +284,16 @@ module Alba
259
284
  end
260
285
 
261
286
  def _fetch_attribute_from_object_first(obj, attribute)
287
+ return obj.fetch(attribute) if obj.is_a?(Hash)
288
+
262
289
  obj.__send__(attribute)
263
290
  rescue NoMethodError
264
291
  __send__(attribute, obj)
265
292
  end
266
293
 
267
294
  def _fetch_attribute_from_resource_first(obj, attribute)
295
+ return obj.fetch(attribute) if obj.is_a?(Hash)
296
+
268
297
  if @_resource_methods.include?(attribute)
269
298
  __send__(attribute, obj)
270
299
  else
@@ -299,12 +328,27 @@ module Alba
299
328
  attr_reader(*INTERNAL_VARIABLES.keys)
300
329
 
301
330
  # 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
331
+ def method_added(method_name) # rubocop:disable Metrics/MethodLength
332
+ case method_name
333
+ when :collection_converter, :converter
334
+ warn "Defining ##{method_name} methods is deprecated", category: :deprecated, uplevel: 1
335
+ alias_method :serializable_hash_for_collection, :deprecated_serializable_hash_for_collection
336
+ private(:serializable_hash_for_collection)
337
+ alias_method :serializable_hash, :deprecated_serializable_hash
338
+ alias_method :to_h, :deprecated_serializable_hash
339
+ when :attributes
340
+ warn 'Overriding `attributes` is deprecated, use `select` instead.', category: :deprecated, uplevel: 1
341
+ when :select
342
+ @_select_arity = instance_method(:select).arity
343
+ when :_setup # noop
344
+ else
345
+ _resource_methods << method_name.to_sym
346
+ end
347
+
304
348
  super
305
349
  end
306
350
 
307
- # @private
351
+ # @api private
308
352
  def inherited(subclass)
309
353
  super
310
354
  INTERNAL_VARIABLES.each_key { |name| subclass.instance_variable_set(:"@#{name}", instance_variable_get(:"@#{name}").clone) }
@@ -316,7 +360,7 @@ module Alba
316
360
  #
317
361
  # @param attrs [Array<String, Symbol>]
318
362
  # @param if [Proc] condition to decide if it should serialize these attributes
319
- # @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
363
+ # @param attrs_with_types [Hash{Symbol, String => Array<Symbol, Proc>, Symbol}]
320
364
  # attributes with name in its key and type and optional type converter in its value
321
365
  # @return [void]
322
366
  def attributes(*attrs, if: nil, **attrs_with_types)
@@ -364,6 +408,7 @@ module Alba
364
408
  # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
365
409
  # @param condition [Proc, nil] a Proc to modify the association
366
410
  # @param resource [Class<Alba::Resource>, String, Proc, nil] representing resource for this association
411
+ # @param serializer [Class<Alba::Resource>, String, Proc, nil] alias for `resource`
367
412
  # @param key [String, Symbol, nil] used as key when given
368
413
  # @param params [Hash] params override for the association
369
414
  # @param options [Hash<Symbol, Proc>]
@@ -371,7 +416,8 @@ module Alba
371
416
  # @param block [Block]
372
417
  # @return [void]
373
418
  # @see Alba::Association#initialize
374
- def association(name, condition = nil, resource: nil, key: nil, params: {}, **options, &block)
419
+ def association(name, condition = nil, resource: nil, serializer: nil, key: nil, params: {}, **options, &block)
420
+ resource ||= serializer
375
421
  transformation = @_key_transformation_cascade ? @_transform_type : :none
376
422
  assoc = Association.new(
377
423
  name: name, condition: condition, resource: resource, params: params, nesting: nesting, key_transformation: transformation, helper: @_helper, &block
@@ -477,9 +523,7 @@ module Alba
477
523
  if @_key_transformation_cascade
478
524
  # We need to update key transformation of associations and nested attributes
479
525
  @_attributes.each_value do |attr|
480
- next unless attr.is_a?(Association) || attr.is_a?(NestedAttribute)
481
-
482
- attr.key_transformation = type
526
+ attr.key_transformation = type if attr.is_a?(Association) || attr.is_a?(NestedAttribute)
483
527
  end
484
528
  end
485
529
  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.4.0'
4
+ VERSION = '3.6.0'
5
5
  end
data/lib/alba.rb CHANGED
@@ -46,13 +46,15 @@ module Alba
46
46
  # @param root_key [Symbol, nil, true]
47
47
  # @param block [Block] resource block
48
48
  # @return [String] serialized JSON string
49
- # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
49
+ # @raise [ArgumentError] if both object and block are not given
50
50
  def serialize(object = nil, with: :inference, root_key: nil, &block)
51
+ raise ArgumentError, 'Either object or block must be given' if object.nil? && block.nil?
52
+
51
53
  if collection?(object)
52
- h = hashify_collection(object, with, &block)
54
+ h = hashify_collection(object, with, root_key, &block)
53
55
  Alba.encoder.call(h)
54
56
  else
55
- resource = resource_with(object, &block)
57
+ resource = resource_for(object, &block)
56
58
  resource.serialize(root_key: root_key)
57
59
  end
58
60
  end
@@ -64,22 +66,24 @@ module Alba
64
66
  # @param root_key [Symbol, nil, true]
65
67
  # @param block [Block] resource block
66
68
  # @return [String] serialized JSON string
67
- # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
69
+ # @raise [ArgumentError] if both object and block are not given
68
70
  def hashify(object = nil, with: :inference, root_key: nil, &block)
71
+ raise ArgumentError, 'Either object or block must be given' if object.nil? && block.nil?
72
+
69
73
  if collection?(object)
70
- hashify_collection(object, with, &block)
74
+ hashify_collection(object, with, root_key, &block)
71
75
  else
72
- resource = resource_with(object, &block)
76
+ resource = resource_for(object, &block)
73
77
  resource.as_json(root_key: root_key)
74
78
  end
75
79
  end
76
80
 
77
81
  # Detect if object is a collection or not.
78
- # When object is a Struct, it's Enumerable but not a collection
82
+ # When object is a Struct or a Range, it's Enumerable but not a collection
79
83
  #
80
84
  # @api private
81
85
  def collection?(object)
82
- object.is_a?(Enumerable) && !object.is_a?(Struct)
86
+ object.is_a?(Enumerable) && !object.is_a?(Struct) && !object.is_a?(Range) && !object.is_a?(Hash)
83
87
  end
84
88
 
85
89
  # Enable inference for key and resource name
@@ -117,6 +121,7 @@ module Alba
117
121
  # When it's a Class or a Module, it should have some methods, see {Alba::DefaultInflector}
118
122
  def inflector=(inflector)
119
123
  @inflector = inflector_from(inflector)
124
+ reset_transform_keys
120
125
  end
121
126
 
122
127
  # @param block [Block] resource body
@@ -144,11 +149,13 @@ module Alba
144
149
 
145
150
  # Configure Alba to symbolize keys
146
151
  def symbolize_keys!
152
+ reset_transform_keys unless @symbolize_keys
147
153
  @symbolize_keys = true
148
154
  end
149
155
 
150
156
  # Configure Alba to stringify (not symbolize) keys
151
157
  def stringify_keys!
158
+ reset_transform_keys if @symbolize_keys
152
159
  @symbolize_keys = false
153
160
  end
154
161
 
@@ -159,8 +166,9 @@ module Alba
159
166
  # @return [Symbol, String, nil]
160
167
  def regularize_key(key)
161
168
  return if key.nil?
169
+ return key.to_sym if @symbolize_keys
162
170
 
163
- @symbolize_keys ? key.to_sym : key.to_s
171
+ key.is_a?(Symbol) ? key.name : key.to_s
164
172
  end
165
173
 
166
174
  # Transform a key with given transform_type
@@ -168,19 +176,22 @@ module Alba
168
176
  # @param key [String] a target key
169
177
  # @param transform_type [Symbol] a transform type, either one of `camel`, `lower_camel`, `dash` or `snake`
170
178
  # @return [String]
171
- def transform_key(key, transform_type:)
179
+ def transform_key(key, transform_type:) # rubocop:disable Metrics/MethodLength
172
180
  raise Alba::Error, 'Inflector is nil. You must set inflector before transforming keys.' unless inflector
173
181
 
174
- key = key.to_s
182
+ @_transformed_keys[transform_type][key] ||= begin
183
+ key = key.to_s
175
184
 
176
- k = case transform_type
177
- when :camel then inflector.camelize(key)
178
- when :lower_camel then inflector.camelize_lower(key)
179
- when :dash then inflector.dasherize(key)
180
- when :snake then inflector.underscore(key)
181
- else raise Alba::Error, "Unknown transform type: #{transform_type}"
182
- end
183
- regularize_key(k)
185
+ k = case transform_type
186
+ when :camel then inflector.camelize(key)
187
+ when :lower_camel then inflector.camelize_lower(key)
188
+ when :dash then inflector.dasherize(key)
189
+ when :snake then inflector.underscore(key)
190
+ else raise Alba::Error, "Unknown transform type: #{transform_type}"
191
+ end
192
+
193
+ regularize_key(k)
194
+ end
184
195
  end
185
196
 
186
197
  # Register types, used for both builtin and custom types
@@ -208,22 +219,49 @@ module Alba
208
219
  @_on_error = :raise
209
220
  @_on_nil = nil
210
221
  @types = {}
222
+ reset_transform_keys
211
223
  register_default_types
212
224
  end
213
225
 
226
+ # @deprecated Use resource_for instead
227
+ def resource_with(object, with: :inference, &block)
228
+ Kernel.warn('Alba.resource_with is deprecated. Use `Alba.resource_for` instead.')
229
+ _resource_for(object, with: with, &block)
230
+ end
231
+
214
232
  # Get a resource object from arguments
215
233
  # If block is given, it creates a resource class with the block
216
- # Otherwise, it infers resource class from the object's class name
234
+ # Otherwise, it behaves depending on `with` argument
217
235
  #
218
- # @ param object [Object] the object whose class name is used for inferring resource class
219
- def resource_with(object, &block)
220
- klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
221
-
222
- klass.new(object)
236
+ # @param object [Object] the object whose class name is used for inferring resource class
237
+ # @param with [:inference, Proc, Class<Alba::Resource>] determines how to get resource class for `object`
238
+ # When it's `:inference`, it infers resource class from `object`'s class name
239
+ # When it's a Proc, it calls the Proc with `object` as an argument
240
+ # When it's a Class, it uses the Class as a resource class
241
+ # Otherwise, it raises an ArgumentError
242
+ # @return [Alba::Resource] resource class with `object` as its target object
243
+ # @raise [ArgumentError] if `with` argument is not one of `:inference`, Proc or Class
244
+ def resource_for(object, with: :inference, &block)
245
+ _resource_for(object, with: with, &block)
223
246
  end
224
247
 
225
248
  private
226
249
 
250
+ def _resource_for(object, with: :inference, &block) # rubocop:disable Metrics/MethodLength
251
+ klass = if block
252
+ resource_class(&block)
253
+ else
254
+ case with
255
+ when :inference then infer_resource_class(object.class.name)
256
+ when Class then with
257
+ when Proc then with.call(object)
258
+ else raise ArgumentError, '`with` argument must be either :inference, Proc or Class'
259
+ end
260
+ end
261
+
262
+ klass.new(object)
263
+ end
264
+
227
265
  def inflector_from(name_or_module)
228
266
  case name_or_module
229
267
  when nil then nil
@@ -276,22 +314,14 @@ module Alba
276
314
  end
277
315
  end
278
316
 
279
- def hashify_collection(collection, with, &block) # rubocop:disable Metrics/MethodLength
280
- collection.map do |obj|
281
- resource = if block
282
- resource_class(&block)
283
- else
284
- case with
285
- when Class then with
286
- when :inference then infer_resource_class(obj.class.name)
287
- when Proc then with.call(obj)
288
- else raise ArgumentError, '`with` argument must be either :inference, Proc or Class'
289
- end
290
- end
317
+ def hashify_collection(collection, with, root_key, &block)
318
+ array = collection.map do |obj|
319
+ resource = resource_for(obj, with: with, &block)
291
320
  raise Alba::Error if resource.nil?
292
321
 
293
- resource.new(obj).to_h
322
+ resource.to_h
294
323
  end
324
+ root_key ? {root_key => array} : array
295
325
  end
296
326
 
297
327
  def validate_inflector(inflector)
@@ -302,6 +332,10 @@ module Alba
302
332
  inflector
303
333
  end
304
334
 
335
+ def reset_transform_keys
336
+ @_transformed_keys = Hash.new { |h, k| h[k] = {} }
337
+ end
338
+
305
339
  def register_default_types # rubocop:disable Metrics/AbcSize
306
340
  [String, :String].each do |t|
307
341
  register_type(t, check: ->(obj) { obj.is_a?(String) }, converter: lambda(&:to_s))