alba 1.0.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/codecov.yml ADDED
@@ -0,0 +1,8 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ informational: true
6
+ patch:
7
+ default:
8
+ target: 90%
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', require: false # For backend
4
+ gem 'ffaker', require: false # For testing
5
+ gem 'minitest', '~> 5.14' # For test
6
+ gem 'rake', '~> 13.0' # For test and automation
7
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
8
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
9
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
10
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
11
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
12
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
13
+ gem 'simplecov-cobertura', require: false # For test coverage
14
+ gem 'yard', require: false # For documentation
15
+
16
+ platforms :ruby do
17
+ gem 'oj', '~> 3.11', require: false # For backend
18
+ gem 'ruby-prof', require: false # For performance profiling
19
+ end
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'minitest', '~> 5.14' # For test
4
+ gem 'rake', '~> 13.0' # For test and automation
5
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
6
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
7
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
8
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
9
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
10
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
11
+ gem 'simplecov-cobertura', require: false # For test coverage
12
+ gem 'yard', require: false # For documentation
13
+
14
+ platforms :ruby do
15
+ gem 'oj', '~> 3.11', require: false # For backend
16
+ gem 'ruby-prof', require: false # For performance profiling
17
+ end
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', require: false # For backend
4
+ gem 'minitest', '~> 5.14' # For test
5
+ gem 'rake', '~> 13.0' # For test and automation
6
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
7
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
8
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
9
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
10
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
11
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
12
+ gem 'simplecov-cobertura', require: false # For test coverage
13
+ gem 'yard', require: false # For documentation
14
+
15
+ platforms :ruby do
16
+ gem 'ruby-prof', require: false # For performance profiling
17
+ end
data/lib/alba.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'json'
1
2
  require_relative 'alba/version'
2
3
  require_relative 'alba/resource'
3
4
 
@@ -9,8 +10,14 @@ module Alba
9
10
  # Error class for backend which is not supported
10
11
  class UnsupportedBackend < Error; end
11
12
 
13
+ # Error class for type which is not supported
14
+ class UnsupportedType < Error; end
15
+
12
16
  class << self
13
- attr_reader :backend, :encoder, :inferring, :_on_error
17
+ attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
18
+
19
+ # Accessor for inflector, a module responsible for incflecting strings
20
+ attr_accessor :inflector
14
21
 
15
22
  # Set the backend, which actually serializes object into JSON
16
23
  #
@@ -20,22 +27,35 @@ module Alba
20
27
  # @raise [Alba::UnsupportedBackend] if backend is not supported
21
28
  def backend=(backend)
22
29
  @backend = backend&.to_sym
23
- set_encoder
30
+ set_encoder_from_backend
31
+ end
32
+
33
+ # Set encoder, a Proc object that accepts an object and generates JSON from it
34
+ # Set backend as `:custom` which indicates no preset encoder is used
35
+ #
36
+ # @param encoder [Proc]
37
+ # @raise [ArgumentError] if given encoder is not a Proc or its arity is not one
38
+ def encoder=(encoder)
39
+ raise ArgumentError, 'Encoder must be a Proc accepting one argument' unless encoder.is_a?(Proc) && encoder.arity == 1
40
+
41
+ @encoder = encoder
42
+ @backend = :custom
24
43
  end
25
44
 
26
45
  # Serialize the object with inline definitions
27
46
  #
28
47
  # @param object [Object] the object to be serialized
29
- # @param key [Symbol]
48
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
49
+ # @param root_key [Symbol, nil, true]
30
50
  # @param block [Block] resource block
31
51
  # @return [String] serialized JSON string
32
52
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
33
- def serialize(object, key: nil, &block)
34
- raise ArgumentError, 'Block required' unless block
53
+ def serialize(object, key: nil, root_key: nil, &block)
54
+ warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
55
+ klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
35
56
 
36
- resource_class.class_eval(&block)
37
- resource = resource_class.new(object)
38
- resource.serialize(key: key)
57
+ resource = klass.new(object)
58
+ resource.serialize(root_key: root_key || key)
39
59
  end
40
60
 
41
61
  # Enable inference for key and resource name
@@ -57,33 +77,62 @@ module Alba
57
77
  #
58
78
  # @param [Symbol] handler
59
79
  # @param [Block]
80
+ # @raise [ArgumentError] if both handler and block params exist
81
+ # @raise [ArgumentError] if both handler and block params don't exist
82
+ # @return [void]
60
83
  def on_error(handler = nil, &block)
61
84
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
62
85
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
63
86
 
64
- p block if block
65
87
  @_on_error = handler || block
66
88
  end
67
89
 
90
+ # Enable root key transformation
91
+ def enable_root_key_transformation!
92
+ @transforming_root_key = true
93
+ end
94
+
95
+ # Disable root key transformation
96
+ def disable_root_key_transformation!
97
+ @transforming_root_key = false
98
+ end
99
+
100
+ # @param block [Block] resource body
101
+ # @return [Class<Alba::Resource>] resource class
102
+ def resource_class(&block)
103
+ klass = Class.new
104
+ klass.include(Alba::Resource)
105
+ klass.class_eval(&block)
106
+ klass
107
+ end
108
+
109
+ # @param name [String] a String Alba infers resource name with
110
+ # @param nesting [String, nil] namespace Alba tries to find resource class in
111
+ # @return [Class<Alba::Resource>] resource class
112
+ def infer_resource_class(name, nesting: nil)
113
+ enable_inference!
114
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
115
+ const_parent.const_get("#{ActiveSupport::Inflector.classify(name)}Resource")
116
+ end
117
+
68
118
  private
69
119
 
70
- def set_encoder
120
+ def set_encoder_from_backend
71
121
  @encoder = case @backend
72
- when :oj
73
- try_oj
74
- when :active_support
75
- try_active_support
76
- when nil, :default, :json
77
- default_encoder
122
+ when :oj, :oj_strict then try_oj
123
+ when :oj_rails then try_oj(mode: :rails)
124
+ when :active_support then try_active_support
125
+ when nil, :default, :json then default_encoder
78
126
  else
79
127
  raise Alba::UnsupportedBackend, "Unsupported backend, #{backend}"
80
128
  end
81
129
  end
82
130
 
83
- def try_oj
131
+ def try_oj(mode: :strict)
84
132
  require 'oj'
85
- ->(hash) { Oj.dump(hash, mode: :strict) }
133
+ ->(hash) { Oj.dump(hash, mode: mode) }
86
134
  rescue LoadError
135
+ Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
87
136
  default_encoder
88
137
  end
89
138
 
@@ -91,24 +140,18 @@ module Alba
91
140
  require 'active_support/json'
92
141
  ->(hash) { ActiveSupport::JSON.encode(hash) }
93
142
  rescue LoadError
143
+ Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
94
144
  default_encoder
95
145
  end
96
146
 
97
147
  def default_encoder
98
148
  lambda do |hash|
99
- require 'json'
100
149
  JSON.dump(hash)
101
150
  end
102
151
  end
103
-
104
- def resource_class
105
- @resource_class ||= begin
106
- klass = Class.new
107
- klass.include(Alba::Resource)
108
- end
109
- end
110
152
  end
111
153
 
112
154
  @encoder = default_encoder
113
155
  @_on_error = :raise
156
+ @transforming_root_key = false # TODO: This will be true since 2.0
114
157
  end
@@ -2,11 +2,12 @@ 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
5
+ attr_reader :object, :name
6
6
 
7
- # @param name [Symbol] name of the method to fetch association
8
- # @param condition [Proc] a proc filtering data
9
- # @param resource [Class<Alba::Resource>] a resource class for the association
7
+ # @param name [Symbol, String] name of the method to fetch association
8
+ # @param condition [Proc, nil] a proc filtering data
9
+ # @param resource [Class<Alba::Resource>, nil] a resource class for the association
10
+ # @param nesting [String] a namespace where source class is inferred with
10
11
  # @param block [Block] used to define resource when resource arg is absent
11
12
  def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
12
13
  @name = name
@@ -15,19 +16,7 @@ module Alba
15
16
  @resource = resource
16
17
  return if @resource
17
18
 
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
26
- end
27
-
28
- # @abstract
29
- def to_hash
30
- :not_implemented
19
+ assign_resource(nesting)
31
20
  end
32
21
 
33
22
  private
@@ -41,11 +30,14 @@ module Alba
41
30
  end
42
31
  end
43
32
 
44
- def resource_class
45
- klass = Class.new
46
- klass.include(Alba::Resource)
47
- klass.class_eval(&@block)
48
- klass
33
+ def assign_resource(nesting)
34
+ @resource = if @block
35
+ Alba.resource_class(&@block)
36
+ elsif Alba.inferring
37
+ Alba.infer_resource_class(@name, nesting: nesting)
38
+ else
39
+ raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
40
+ end
49
41
  end
50
42
  end
51
43
  end
@@ -0,0 +1,36 @@
1
+ module Alba
2
+ # This module represents the inflector, which is used by default
3
+ module DefaultInflector
4
+ begin
5
+ require 'active_support/inflector'
6
+ rescue LoadError
7
+ raise ::Alba::Error, 'To use transform_keys, please install `ActiveSupport` gem.'
8
+ end
9
+
10
+ module_function
11
+
12
+ # Camelizes a key
13
+ #
14
+ # @param key [String] key to be camelized
15
+ # @return [String] camelized key
16
+ def camelize(key)
17
+ ActiveSupport::Inflector.camelize(key)
18
+ end
19
+
20
+ # Camelizes a key, 1st letter lowercase
21
+ #
22
+ # @param key [String] key to be camelized
23
+ # @return [String] camelized key
24
+ def camelize_lower(key)
25
+ ActiveSupport::Inflector.camelize(key, false)
26
+ end
27
+
28
+ # Dasherizes a key
29
+ #
30
+ # @param key [String] key to be dasherized
31
+ # @return [String] dasherized key
32
+ def dasherize(key)
33
+ ActiveSupport::Inflector.dasherize(key)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ module Alba
2
+ # This module creates key transform functions
3
+ module KeyTransformFactory
4
+ class << self
5
+ # Create key transform function for given transform_type
6
+ #
7
+ # @param transform_type [Symbol] transform type
8
+ # @return [Proc] transform function
9
+ # @raise [Alba::Error] when transform_type is not supported
10
+ def create(transform_type)
11
+ case transform_type
12
+ when :camel
13
+ ->(key) { _inflector.camelize(key) }
14
+ when :lower_camel
15
+ ->(key) { _inflector.camelize_lower(key) }
16
+ when :dash
17
+ ->(key) { _inflector.dasherize(key) }
18
+ else
19
+ raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def _inflector
26
+ Alba.inflector || begin
27
+ require_relative './default_inflector'
28
+ Alba::DefaultInflector
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/alba/many.rb CHANGED
@@ -6,15 +6,16 @@ module Alba
6
6
  # Recursively converts objects into an Array of Hashes
7
7
  #
8
8
  # @param target [Object] the object having an association method
9
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
9
10
  # @param params [Hash] user-given Hash for arbitrary data
10
11
  # @return [Array<Hash>]
11
- def to_hash(target, params: {})
12
+ def to_hash(target, within: nil, params: {})
12
13
  @object = target.public_send(@name)
13
14
  @object = @condition.call(@object, params) if @condition
14
15
  return if @object.nil?
15
16
 
16
17
  @resource = constantize(@resource)
17
- @object.map { |o| @resource.new(o, params: params).to_hash }
18
+ @resource.new(@object, params: params, within: within).to_hash
18
19
  end
19
20
  end
20
21
  end
data/lib/alba/one.rb CHANGED
@@ -6,15 +6,16 @@ module Alba
6
6
  # Recursively converts an object into a Hash
7
7
  #
8
8
  # @param target [Object] the object having an association method
9
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
9
10
  # @param params [Hash] user-given Hash for arbitrary data
10
11
  # @return [Hash]
11
- def to_hash(target, params: {})
12
+ def to_hash(target, within: nil, params: {})
12
13
  @object = target.public_send(@name)
13
14
  @object = @condition.call(object, params) if @condition
14
15
  return if @object.nil?
15
16
 
16
17
  @resource = constantize(@resource)
17
- @resource.new(object, params: params).to_hash
18
+ @resource.new(object, params: params, within: within).to_hash
18
19
  end
19
20
  end
20
21
  end
data/lib/alba/resource.rb CHANGED
@@ -1,14 +1,19 @@
1
1
  require_relative 'one'
2
2
  require_relative 'many'
3
+ require_relative 'key_transform_factory'
4
+ require_relative 'typed_attribute'
3
5
 
4
6
  module Alba
5
7
  # This module represents what should be serialized
6
8
  module Resource
7
9
  # @!parse include InstanceMethods
8
10
  # @!parse extend ClassMethods
9
- DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
11
+ DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze # rubocop:disable Layout/LineLength
10
12
  private_constant :DSLS
11
13
 
14
+ WITHIN_DEFAULT = Object.new.freeze
15
+ private_constant :WITHIN_DEFAULT
16
+
12
17
  # @private
13
18
  def self.included(base)
14
19
  super
@@ -28,19 +33,29 @@ module Alba
28
33
 
29
34
  # @param object [Object] the object to be serialized
30
35
  # @param params [Hash] user-given Hash for arbitrary data
31
- def initialize(object, params: {})
36
+ # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
37
+ def initialize(object, params: {}, within: WITHIN_DEFAULT)
32
38
  @object = object
33
39
  @params = params.freeze
40
+ @within = within
34
41
  DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
35
42
  end
36
43
 
37
44
  # Serialize object into JSON string
38
45
  #
39
- # @param key [Symbol]
46
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
47
+ # @param root_key [Symbol, nil, true]
48
+ # @param meta [Hash] metadata for this seialization
40
49
  # @return [String] serialized JSON string
41
- def serialize(key: nil)
42
- key = key.nil? ? _key : key
43
- hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
50
+ def serialize(key: nil, root_key: nil, meta: {})
51
+ warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
52
+ key = key.nil? && root_key.nil? ? fetch_key : root_key || key
53
+ hash = if key && key != ''
54
+ h = {key.to_s => serializable_hash}
55
+ hash_with_metadata(h, meta)
56
+ else
57
+ serializable_hash
58
+ end
44
59
  Alba.encoder.call(hash)
45
60
  end
46
61
 
@@ -54,27 +69,44 @@ module Alba
54
69
 
55
70
  private
56
71
 
72
+ def hash_with_metadata(hash, meta)
73
+ base = @_meta ? instance_eval(&@_meta) : {}
74
+ metadata = base.merge(meta)
75
+ hash[:meta] = metadata unless metadata.empty?
76
+ hash
77
+ end
78
+
79
+ def fetch_key
80
+ collection? ? _key_for_collection : _key
81
+ end
82
+
83
+ def _key_for_collection
84
+ return @_key_for_collection.to_s unless @_key_for_collection == true && Alba.inferring
85
+
86
+ key = resource_name.pluralize
87
+ transforming_root_key? ? transform_key(key) : key
88
+ end
89
+
57
90
  # @return [String]
58
91
  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
92
+ return @_key.to_s unless @_key == true && Alba.inferring
93
+
94
+ transforming_root_key? ? transform_key(resource_name) : resource_name
95
+ end
96
+
97
+ def resource_name
98
+ self.class.name.demodulize.delete_suffix('Resource').underscore
99
+ end
100
+
101
+ def transforming_root_key?
102
+ @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
66
103
  end
67
104
 
68
105
  def converter
69
106
  lambda do |object|
70
107
  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
108
+ key_and_attribute_body_from(object, key, attribute)
109
+ rescue ::Alba::Error, FrozenError, TypeError
78
110
  raise
79
111
  rescue StandardError => e
80
112
  handle_error(e, object, key, attribute)
@@ -83,18 +115,24 @@ module Alba
83
115
  end
84
116
  end
85
117
 
118
+ def key_and_attribute_body_from(object, key, attribute)
119
+ key = transform_key(key)
120
+ if attribute.is_a?(Array) # Conditional
121
+ conditional_attribute(object, key, attribute)
122
+ else
123
+ [key, fetch_attribute(object, attribute)]
124
+ end
125
+ end
126
+
86
127
  def conditional_attribute(object, key, attribute)
87
128
  condition = attribute.last
88
129
  arity = condition.arity
89
- return [] if arity <= 1 && !condition.call(object)
130
+ # We can return early to skip fetch_attribute
131
+ return [] if arity <= 1 && !instance_exec(object, &condition)
90
132
 
91
133
  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)
134
+ attr = attribute.first.is_a?(Alba::Association) ? attribute.first.object : fetched_attribute
135
+ return [] if arity >= 2 && !instance_exec(object, attr, &condition)
98
136
 
99
137
  [key, fetched_attribute]
100
138
  end
@@ -102,14 +140,10 @@ module Alba
102
140
  def handle_error(error, object, key, attribute)
103
141
  on_error = @_on_error || Alba._on_error
104
142
  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)
143
+ when :raise, nil then raise
144
+ when :nullify then [key, nil]
145
+ when :ignore then []
146
+ when Proc then on_error.call(error, object, key, attribute, self.class)
113
147
  else
114
148
  raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
115
149
  end
@@ -117,25 +151,39 @@ module Alba
117
151
 
118
152
  # Override this method to supply custom key transform method
119
153
  def transform_key(key)
120
- return key unless @_transform_keys
154
+ return key if @_transform_key_function.nil?
121
155
 
122
- require_relative 'key_transformer'
123
- KeyTransformer.transform(key, @_transform_keys)
156
+ @_transform_key_function.call(key.to_s)
124
157
  end
125
158
 
126
159
  def fetch_attribute(object, attribute)
127
160
  case attribute
128
- when Symbol
129
- object.public_send attribute
130
- when Proc
131
- instance_exec(object, &attribute)
132
- when Alba::One, Alba::Many
133
- attribute.to_hash(object, params: params)
161
+ when Symbol then object.public_send attribute
162
+ when Proc then instance_exec(object, &attribute)
163
+ when Alba::One, Alba::Many then yield_if_within(attribute.name.to_sym) { |within| attribute.to_hash(object, params: params, within: within) }
164
+ when TypedAttribute then attribute.value(object)
134
165
  else
135
166
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
136
167
  end
137
168
  end
138
169
 
170
+ def yield_if_within(association_name)
171
+ within = check_within(association_name)
172
+ yield(within) if within
173
+ end
174
+
175
+ def check_within(association_name)
176
+ case @within
177
+ when WITHIN_DEFAULT then WITHIN_DEFAULT # Default value, doesn't check within tree
178
+ when Hash then @within.fetch(association_name, nil) # Traverse within tree
179
+ when Array then @within.find { |item| item.to_sym == association_name }
180
+ when Symbol then @within == association_name
181
+ when nil, true, false then false # Stop here
182
+ else
183
+ raise Alba::Error, "Unknown type for within option: #{@within.class}"
184
+ end
185
+ end
186
+
139
187
  def collection?
140
188
  @object.is_a?(Enumerable)
141
189
  end
@@ -151,23 +199,49 @@ module Alba
151
199
  DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
152
200
  end
153
201
 
202
+ # Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine
203
+ # rubocop:disable Metrics/ParameterLists
204
+
154
205
  # Set multiple attributes at once
155
206
  #
156
207
  # @param attrs [Array<String, Symbol>]
157
- # @param options [Hash] option hash including `if` that is a condition to render these attributes
158
- def attributes(*attrs, **options)
208
+ # @param if [Proc] condition to decide if it should serialize these attributes
209
+ # @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
210
+ # attributes with name in its key and type and optional type converter in its value
211
+ # @return [void]
212
+ def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
213
+ if_value = binding.local_variable_get(:if)
214
+ assign_attributes(attrs, if_value)
215
+ assign_attributes_with_types(attrs_with_types, if_value)
216
+ end
217
+
218
+ def assign_attributes(attrs, if_value)
159
219
  attrs.each do |attr_name|
160
- attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
220
+ attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
161
221
  @_attributes[attr_name.to_sym] = attr
162
222
  end
163
223
  end
224
+ private :assign_attributes
225
+
226
+ def assign_attributes_with_types(attrs_with_types, if_value)
227
+ attrs_with_types.each do |attr_name, type_and_converter|
228
+ attr_name = attr_name.to_sym
229
+ type, type_converter = type_and_converter
230
+ typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter)
231
+ attr = if_value ? [typed_attr, if_value] : typed_attr
232
+ @_attributes[attr_name] = attr
233
+ end
234
+ end
235
+ private :assign_attributes_with_types
164
236
 
165
237
  # Set an attribute with the given block
166
238
  #
167
239
  # @param name [String, Symbol] key name
168
- # @param options [Hash] option hash including `if` that is a condition to render
240
+ # @param options [Hash<Symbol, Proc>]
241
+ # @option options [Proc] if a condition to decide if this attribute should be serialized
169
242
  # @param block [Block] the block called during serialization
170
243
  # @raise [ArgumentError] if block is absent
244
+ # @return [void]
171
245
  def attribute(name, **options, &block)
172
246
  raise ArgumentError, 'No block given in attribute method' unless block
173
247
 
@@ -176,12 +250,14 @@ module Alba
176
250
 
177
251
  # Set One association
178
252
  #
179
- # @param name [String, Symbol]
180
- # @param condition [Proc]
181
- # @param resource [Class<Alba::Resource>]
182
- # @param key [String, Symbol] used as key when given
183
- # @param options [Hash] option hash including `if` that is a condition to render
253
+ # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
254
+ # @param condition [Proc, nil] a Proc to modify the association
255
+ # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
256
+ # @param key [String, Symbol, nil] used as key when given
257
+ # @param options [Hash<Symbol, Proc>]
258
+ # @option options [Proc] if a condition to decide if this association should be serialized
184
259
  # @param block [Block]
260
+ # @return [void]
185
261
  # @see Alba::One#initialize
186
262
  def one(name, condition = nil, resource: nil, key: nil, **options, &block)
187
263
  nesting = self.name&.rpartition('::')&.first
@@ -192,12 +268,14 @@ module Alba
192
268
 
193
269
  # Set Many association
194
270
  #
195
- # @param name [String, Symbol]
196
- # @param condition [Proc]
197
- # @param resource [Class<Alba::Resource>]
198
- # @param key [String, Symbol] used as key when given
199
- # @param options [Hash] option hash including `if` that is a condition to render
271
+ # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
272
+ # @param condition [Proc, nil] a Proc to filter the collection
273
+ # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
274
+ # @param key [String, Symbol, nil] used as key when given
275
+ # @param options [Hash<Symbol, Proc>]
276
+ # @option options [Proc] if a condition to decide if this association should be serialized
200
277
  # @param block [Block]
278
+ # @return [void]
201
279
  # @see Alba::Many#initialize
202
280
  def many(name, condition = nil, resource: nil, key: nil, **options, &block)
203
281
  nesting = self.name&.rpartition('::')&.first
@@ -209,14 +287,40 @@ module Alba
209
287
  # Set key
210
288
  #
211
289
  # @param key [String, Symbol]
290
+ # @deprecated Use {#root_key} instead
212
291
  def key(key)
292
+ warn '[DEPRECATION] `key` is deprecated, use `root_key` instead.'
213
293
  @_key = key.respond_to?(:to_sym) ? key.to_sym : key
214
294
  end
215
295
 
296
+ # Set root key
297
+ #
298
+ # @param key [String, Symbol]
299
+ # @param key_for_collection [String, Symbol]
300
+ # @raise [NoMethodError] when key doesn't respond to `to_sym` method
301
+ def root_key(key, key_for_collection = nil)
302
+ @_key = key.to_sym
303
+ @_key_for_collection = key_for_collection&.to_sym
304
+ end
305
+
216
306
  # Set key to true
217
307
  #
308
+ # @deprecated Use {#root_key!} instead
218
309
  def key!
310
+ warn '[DEPRECATION] `key!` is deprecated, use `root_key!` instead.'
219
311
  @_key = true
312
+ @_key_for_collection = true
313
+ end
314
+
315
+ # Set root key to true
316
+ def root_key!
317
+ @_key = true
318
+ @_key_for_collection = true
319
+ end
320
+
321
+ # Set metadata
322
+ def meta(&block)
323
+ @_meta = block
220
324
  end
221
325
 
222
326
  # Delete attributes
@@ -232,20 +336,25 @@ module Alba
232
336
  # Transform keys as specified type
233
337
  #
234
338
  # @param type [String, Symbol]
235
- def transform_keys(type)
236
- @_transform_keys = type.to_sym
339
+ # @param root [Boolean] decides if root key also should be transformed
340
+ def transform_keys(type, root: nil)
341
+ @_transform_key_function = KeyTransformFactory.create(type.to_sym)
342
+ @_transforming_root_key = root
237
343
  end
238
344
 
239
345
  # Set error handler
346
+ # If this is set it's used as a error handler overriding global one
240
347
  #
241
- # @param [Symbol] handler
242
- # @param [Block]
348
+ # @param handler [Symbol] `:raise`, `:ignore` or `:nullify`
349
+ # @param block [Block]
243
350
  def on_error(handler = nil, &block)
244
351
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
245
352
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
246
353
 
247
354
  @_on_error = handler || block
248
355
  end
356
+
357
+ # rubocop:enable Metrics/ParameterLists
249
358
  end
250
359
  end
251
360
  end