alba 0.13.1 → 1.3.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
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
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
14
+
15
+ platforms :ruby do
16
+ gem 'ruby-prof', require: false # For performance profiling
17
+ end
data/lib/alba.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require_relative 'alba/version'
2
- require_relative 'alba/serializer'
3
2
  require_relative 'alba/resource'
4
3
 
5
4
  # Core module
@@ -10,9 +9,12 @@ module Alba
10
9
  # Error class for backend which is not supported
11
10
  class UnsupportedBackend < Error; end
12
11
 
12
+ # Error class for type which is not supported
13
+ class UnsupportedType < Error; end
14
+
13
15
  class << self
14
- attr_reader :backend, :encoder
15
- attr_accessor :default_serializer
16
+ attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
17
+ attr_accessor :inflector
16
18
 
17
19
  # Set the backend, which actually serializes object into JSON
18
20
  #
@@ -28,25 +30,64 @@ module Alba
28
30
  # Serialize the object with inline definitions
29
31
  #
30
32
  # @param object [Object] the object to be serialized
31
- # @param with [nil, Proc, Alba::Serializer] selializer
33
+ # @param key [Symbol]
32
34
  # @param block [Block] resource block
33
35
  # @return [String] serialized JSON string
34
36
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
35
- def serialize(object, with: nil, &block)
37
+ def serialize(object, key: nil, &block)
36
38
  raise ArgumentError, 'Block required' unless block
37
39
 
38
- resource_class.class_eval(&block)
39
- resource = resource_class.new(object)
40
- with ||= @default_serializer
41
- resource.serialize(with: with)
40
+ klass = Class.new
41
+ klass.include(Alba::Resource)
42
+ klass.class_eval(&block)
43
+ resource = klass.new(object)
44
+ resource.serialize(key: key)
45
+ end
46
+
47
+ # Enable inference for key and resource name
48
+ def enable_inference!
49
+ begin
50
+ require 'active_support/inflector'
51
+ rescue LoadError
52
+ raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
53
+ end
54
+ @inferring = true
55
+ end
56
+
57
+ # Disable inference for key and resource name
58
+ def disable_inference!
59
+ @inferring = false
60
+ end
61
+
62
+ # Set error handler
63
+ #
64
+ # @param [Symbol] handler
65
+ # @param [Block]
66
+ def on_error(handler = nil, &block)
67
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
68
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
69
+
70
+ @_on_error = handler || block
71
+ end
72
+
73
+ # Enable root key transformation
74
+ def enable_root_key_transformation!
75
+ @transforming_root_key = true
76
+ end
77
+
78
+ # Disable root key transformation
79
+ def disable_root_key_transformation!
80
+ @transforming_root_key = false
42
81
  end
43
82
 
44
83
  private
45
84
 
46
85
  def set_encoder
47
86
  @encoder = case @backend
48
- when :oj
87
+ when :oj, :oj_strict
49
88
  try_oj
89
+ when :oj_rails
90
+ try_oj(mode: :rails)
50
91
  when :active_support
51
92
  try_active_support
52
93
  when nil, :default, :json
@@ -56,10 +97,11 @@ module Alba
56
97
  end
57
98
  end
58
99
 
59
- def try_oj
100
+ def try_oj(mode: :strict)
60
101
  require 'oj'
61
- ->(hash) { Oj.dump(hash, mode: :strict) }
102
+ ->(hash) { Oj.dump(hash, mode: mode) }
62
103
  rescue LoadError
104
+ Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
63
105
  default_encoder
64
106
  end
65
107
 
@@ -67,6 +109,7 @@ module Alba
67
109
  require 'active_support/json'
68
110
  ->(hash) { ActiveSupport::JSON.encode(hash) }
69
111
  rescue LoadError
112
+ Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
70
113
  default_encoder
71
114
  end
72
115
 
@@ -76,14 +119,9 @@ module Alba
76
119
  JSON.dump(hash)
77
120
  end
78
121
  end
79
-
80
- def resource_class
81
- @resource_class ||= begin
82
- klass = Class.new
83
- klass.include(Alba::Resource)
84
- end
85
- end
86
122
  end
87
123
 
88
124
  @encoder = default_encoder
125
+ @_on_error = :raise
126
+ @transforming_root_key = false # TODO: This will be true since 2.0
89
127
  end
@@ -2,30 +2,53 @@ 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, :name
6
+
5
7
  # @param name [Symbol] name of the method to fetch association
6
8
  # @param condition [Proc] a proc filtering data
7
9
  # @param resource [Class<Alba::Resource>] a resource class for the association
8
10
  # @param block [Block] used to define resource when resource arg is absent
9
- def initialize(name:, condition: nil, resource: nil, &block)
11
+ def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
10
12
  @name = name
11
13
  @condition = condition
12
14
  @block = block
13
- @resource = resource || resource_class
14
- raise ArgumentError, 'resource or block is required' if @resource.nil? && @block.nil?
15
- end
15
+ @resource = resource
16
+ return if @resource
16
17
 
17
- # @abstract
18
- def to_hash
19
- :not_implemented
18
+ assign_resource(nesting)
20
19
  end
21
20
 
22
21
  private
23
22
 
23
+ def constantize(resource)
24
+ case resource # rubocop:disable Style/MissingElse
25
+ when Class
26
+ resource
27
+ when Symbol, String
28
+ Object.const_get(resource)
29
+ end
30
+ end
31
+
32
+ def assign_resource(nesting)
33
+ @resource = if @block
34
+ resource_class
35
+ elsif Alba.inferring
36
+ resource_class_with_nesting(nesting)
37
+ else
38
+ raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
39
+ end
40
+ end
41
+
24
42
  def resource_class
25
43
  klass = Class.new
26
44
  klass.include(Alba::Resource)
27
45
  klass.class_eval(&@block)
28
46
  klass
29
47
  end
48
+
49
+ def resource_class_with_nesting(nesting)
50
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
51
+ const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
52
+ end
30
53
  end
31
54
  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
+ # @params 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
+ # @params 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
+ # @params 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
+ # @params 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,14 +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
- objects = target.public_send(@name)
13
- objects = @condition.call(objects, params) if @condition
14
- return if objects.nil?
12
+ def to_hash(target, within: nil, params: {})
13
+ @object = target.public_send(@name)
14
+ @object = @condition.call(@object, params) if @condition
15
+ return if @object.nil?
15
16
 
16
- objects.map { |o| @resource.new(o, params: params).to_hash }
17
+ @resource = constantize(@resource)
18
+ @resource.new(@object, params: params, within: within).to_hash
17
19
  end
18
20
  end
19
21
  end
data/lib/alba/one.rb CHANGED
@@ -6,14 +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
- object = target.public_send(@name)
13
- object = @condition.call(object, params) if @condition
14
- return if object.nil?
12
+ def to_hash(target, within: nil, params: {})
13
+ @object = target.public_send(@name)
14
+ @object = @condition.call(object, params) if @condition
15
+ return if @object.nil?
15
16
 
16
- @resource.new(object, params: params).to_hash
17
+ @resource = constantize(@resource)
18
+ @resource.new(object, params: params, within: within).to_hash
17
19
  end
18
20
  end
19
21
  end
data/lib/alba/resource.rb CHANGED
@@ -1,15 +1,19 @@
1
- require_relative 'serializer'
2
1
  require_relative 'one'
3
2
  require_relative 'many'
3
+ require_relative 'key_transform_factory'
4
+ require_relative 'typed_attribute'
4
5
 
5
6
  module Alba
6
7
  # This module represents what should be serialized
7
8
  module Resource
8
9
  # @!parse include InstanceMethods
9
10
  # @!parse extend ClassMethods
10
- DSLS = {_attributes: {}, _serializer: nil, _key: nil, _transform_keys: nil}.freeze
11
+ DSLS = {_attributes: {}, _key: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze
11
12
  private_constant :DSLS
12
13
 
14
+ WITHIN_DEFAULT = Object.new.freeze
15
+ private_constant :WITHIN_DEFAULT
16
+
13
17
  # @private
14
18
  def self.included(base)
15
19
  super
@@ -25,32 +29,26 @@ module Alba
25
29
 
26
30
  # Instance methods
27
31
  module InstanceMethods
28
- attr_reader :object, :_key, :params
32
+ attr_reader :object, :params
29
33
 
30
34
  # @param object [Object] the object to be serialized
31
35
  # @param params [Hash] user-given Hash for arbitrary data
32
- def initialize(object, params: {})
36
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
37
+ def initialize(object, params: {}, within: WITHIN_DEFAULT)
33
38
  @object = object
34
39
  @params = params.freeze
40
+ @within = within
35
41
  DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
36
42
  end
37
43
 
38
- # Get serializer with `with` argument and serialize self with it
44
+ # Serialize object into JSON string
39
45
  #
40
- # @param with [nil, Proc, Alba::Serializer] selializer
46
+ # @param key [Symbol]
41
47
  # @return [String] serialized JSON string
42
- def serialize(with: nil)
43
- serializer = case with
44
- when nil
45
- @_serializer || empty_serializer
46
- when ->(obj) { obj.is_a?(Class) && obj <= Alba::Serializer }
47
- with
48
- when Proc
49
- inline_extended_serializer(with)
50
- else
51
- raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc'
52
- end
53
- serializer.new(self).serialize
48
+ def serialize(key: nil)
49
+ key = key.nil? ? _key : key
50
+ hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
51
+ Alba.encoder.call(hash)
54
52
  end
55
53
 
56
54
  # A Hash for serialization
@@ -61,54 +59,121 @@ module Alba
61
59
  end
62
60
  alias to_hash serializable_hash
63
61
 
64
- # @return [Symbol]
65
- def key
66
- @_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym
62
+ private
63
+
64
+ # @return [String]
65
+ def _key
66
+ return @_key.to_s unless @_key == true && Alba.inferring
67
+
68
+ transforming_root_key? ? transform_key(key_from_resource_name) : key_from_resource_name
67
69
  end
68
70
 
69
- private
71
+ def key_from_resource_name
72
+ collection? ? resource_name.pluralize : resource_name
73
+ end
74
+
75
+ def resource_name
76
+ self.class.name.demodulize.delete_suffix('Resource').underscore
77
+ end
78
+
79
+ def transforming_root_key?
80
+ @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
81
+ end
70
82
 
71
- # rubocop:disable Style/MethodCalledOnDoEndBlock
72
83
  def converter
73
- lambda do |resource|
74
- @_attributes.map do |key, attribute|
75
- [transform_key(key), fetch_attribute(resource, attribute)]
76
- end.to_h
84
+ lambda do |object|
85
+ arrays = @_attributes.map do |key, attribute|
86
+ key_and_attribute_body_from(object, key, attribute)
87
+ rescue ::Alba::Error, FrozenError, TypeError
88
+ raise
89
+ rescue StandardError => e
90
+ handle_error(e, object, key, attribute)
91
+ end
92
+ arrays.reject(&:empty?).to_h
93
+ end
94
+ end
95
+
96
+ def key_and_attribute_body_from(object, key, attribute)
97
+ key = transform_key(key)
98
+ if attribute.is_a?(Array) # Conditional
99
+ conditional_attribute(object, key, attribute)
100
+ else
101
+ [key, fetch_attribute(object, attribute)]
102
+ end
103
+ end
104
+
105
+ def conditional_attribute(object, key, attribute)
106
+ condition = attribute.last
107
+ arity = condition.arity
108
+ return [] if arity <= 1 && !instance_exec(object, &condition)
109
+
110
+ fetched_attribute = fetch_attribute(object, attribute.first)
111
+ attr = if attribute.first.is_a?(Alba::Association)
112
+ attribute.first.object
113
+ else
114
+ fetched_attribute
115
+ end
116
+ return [] if arity >= 2 && !instance_exec(object, attr, &condition)
117
+
118
+ [key, fetched_attribute]
119
+ end
120
+
121
+ def handle_error(error, object, key, attribute)
122
+ on_error = @_on_error || Alba._on_error
123
+ case on_error
124
+ when :raise, nil
125
+ raise
126
+ when :nullify
127
+ [key, nil]
128
+ when :ignore
129
+ []
130
+ when Proc
131
+ on_error.call(error, object, key, attribute, self.class)
132
+ else
133
+ raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
77
134
  end
78
135
  end
79
- # rubocop:enable Style/MethodCalledOnDoEndBlock
80
136
 
81
137
  # Override this method to supply custom key transform method
82
138
  def transform_key(key)
83
- return key unless @_transform_keys
139
+ return key if @_transform_key_function.nil?
84
140
 
85
- require_relative 'key_transformer'
86
- KeyTransformer.transform(key, @_transform_keys)
141
+ @_transform_key_function.call(key.to_s)
87
142
  end
88
143
 
89
- def fetch_attribute(resource, attribute)
144
+ def fetch_attribute(object, attribute)
90
145
  case attribute
91
146
  when Symbol
92
- resource.public_send attribute
147
+ object.public_send attribute
93
148
  when Proc
94
- instance_exec(resource, &attribute)
149
+ instance_exec(object, &attribute)
95
150
  when Alba::One, Alba::Many
96
- attribute.to_hash(resource, params: params)
151
+ within = check_within(attribute.name.to_sym)
152
+ return unless within
153
+
154
+ attribute.to_hash(object, params: params, within: within)
155
+ when TypedAttribute
156
+ attribute.value(object)
97
157
  else
98
158
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
99
159
  end
100
160
  end
101
161
 
102
- def empty_serializer
103
- klass = Class.new
104
- klass.include Alba::Serializer
105
- klass
106
- end
107
-
108
- def inline_extended_serializer(with)
109
- klass = empty_serializer
110
- klass.class_eval(&with)
111
- klass
162
+ def check_within(association_name)
163
+ case @within
164
+ when WITHIN_DEFAULT # Default value, doesn't check within tree
165
+ WITHIN_DEFAULT
166
+ when Hash # Traverse within tree
167
+ @within.fetch(association_name, nil)
168
+ when Array # within tree ends with Array
169
+ @within.find { |item| item.to_sym == association_name }
170
+ when Symbol # within tree could end with Symbol
171
+ @within == association_name
172
+ when nil, true, false # In these cases, Alba stops serialization here.
173
+ false
174
+ else
175
+ raise Alba::Error, "Unknown type for within option: #{@within.class}"
176
+ end
112
177
  end
113
178
 
114
179
  def collection?
@@ -129,19 +194,43 @@ module Alba
129
194
  # Set multiple attributes at once
130
195
  #
131
196
  # @param attrs [Array<String, Symbol>]
132
- def attributes(*attrs)
133
- attrs.each { |attr_name| @_attributes[attr_name.to_sym] = attr_name.to_sym }
197
+ # @param if [Boolean] condition to decide if it should render these attributes
198
+ # @param attrs_with_types [Hash] attributes with name in its key and type and optional type converter in its value
199
+ def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
200
+ if_value = binding.local_variable_get(:if)
201
+ assign_attributes(attrs, if_value)
202
+ assign_attributes_with_types(attrs_with_types, if_value)
203
+ end
204
+
205
+ def assign_attributes(attrs, if_value)
206
+ attrs.each do |attr_name|
207
+ attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
208
+ @_attributes[attr_name.to_sym] = attr
209
+ end
210
+ end
211
+ private :assign_attributes
212
+
213
+ def assign_attributes_with_types(attrs_with_types, if_value)
214
+ attrs_with_types.each do |attr_name, type_and_converter|
215
+ attr_name = attr_name.to_sym
216
+ type, type_converter = type_and_converter
217
+ typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter)
218
+ attr = if_value ? [typed_attr, if_value] : typed_attr
219
+ @_attributes[attr_name] = attr
220
+ end
134
221
  end
222
+ private :assign_attributes_with_types
135
223
 
136
224
  # Set an attribute with the given block
137
225
  #
138
226
  # @param name [String, Symbol] key name
227
+ # @param options [Hash] option hash including `if` that is a condition to render
139
228
  # @param block [Block] the block called during serialization
140
229
  # @raise [ArgumentError] if block is absent
141
- def attribute(name, &block)
230
+ def attribute(name, **options, &block)
142
231
  raise ArgumentError, 'No block given in attribute method' unless block
143
232
 
144
- @_attributes[name.to_sym] = block
233
+ @_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
145
234
  end
146
235
 
147
236
  # Set One association
@@ -150,10 +239,13 @@ module Alba
150
239
  # @param condition [Proc]
151
240
  # @param resource [Class<Alba::Resource>]
152
241
  # @param key [String, Symbol] used as key when given
242
+ # @param options [Hash] option hash including `if` that is a condition to render
153
243
  # @param block [Block]
154
244
  # @see Alba::One#initialize
155
- def one(name, condition = nil, resource: nil, key: nil, &block)
156
- @_attributes[key&.to_sym || name.to_sym] = One.new(name: name, condition: condition, resource: resource, &block)
245
+ def one(name, condition = nil, resource: nil, key: nil, **options, &block)
246
+ nesting = self.name&.rpartition('::')&.first
247
+ one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
248
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one
157
249
  end
158
250
  alias has_one one
159
251
 
@@ -163,25 +255,27 @@ module Alba
163
255
  # @param condition [Proc]
164
256
  # @param resource [Class<Alba::Resource>]
165
257
  # @param key [String, Symbol] used as key when given
258
+ # @param options [Hash] option hash including `if` that is a condition to render
166
259
  # @param block [Block]
167
260
  # @see Alba::Many#initialize
168
- def many(name, condition = nil, resource: nil, key: nil, &block)
169
- @_attributes[key&.to_sym || name.to_sym] = Many.new(name: name, condition: condition, resource: resource, &block)
261
+ def many(name, condition = nil, resource: nil, key: nil, **options, &block)
262
+ nesting = self.name&.rpartition('::')&.first
263
+ many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
264
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
170
265
  end
171
266
  alias has_many many
172
267
 
173
- # Set serializer for the resource
174
- #
175
- # @param name [Alba::Serializer]
176
- def serializer(name)
177
- @_serializer = name <= Alba::Serializer ? name : nil
178
- end
179
-
180
268
  # Set key
181
269
  #
182
270
  # @param key [String, Symbol]
183
271
  def key(key)
184
- @_key = key.to_sym
272
+ @_key = key.respond_to?(:to_sym) ? key.to_sym : key
273
+ end
274
+
275
+ # Set key to true
276
+ #
277
+ def key!
278
+ @_key = true
185
279
  end
186
280
 
187
281
  # Delete attributes
@@ -197,8 +291,21 @@ module Alba
197
291
  # Transform keys as specified type
198
292
  #
199
293
  # @param type [String, Symbol]
200
- def transform_keys(type)
201
- @_transform_keys = type.to_sym
294
+ # @param root [Boolean] decides if root key also should be transformed
295
+ def transform_keys(type, root: nil)
296
+ @_transform_key_function = KeyTransformFactory.create(type.to_sym)
297
+ @_transforming_root_key = root
298
+ end
299
+
300
+ # Set error handler
301
+ #
302
+ # @param [Symbol] handler
303
+ # @param [Block]
304
+ def on_error(handler = nil, &block)
305
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
306
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
307
+
308
+ @_on_error = handler || block
202
309
  end
203
310
  end
204
311
  end