alba 1.0.0 → 1.4.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.
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