alba 0.11.0 → 1.0.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.
@@ -0,0 +1,198 @@
1
+ # Benchmark script to run varieties of JSON serializers
2
+ # Fetch Alba from local, otherwise fetch latest from RubyGems
3
+
4
+ require "bundler/inline"
5
+
6
+ gemfile(true) do
7
+ source "https://rubygems.org"
8
+
9
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
10
+
11
+ gem "activerecord", "6.1.3"
12
+ gem "sqlite3"
13
+ gem "jbuilder"
14
+ gem "active_model_serializers"
15
+ gem "blueprinter"
16
+ gem "representable"
17
+ gem "alba", path: '../'
18
+ gem "oj"
19
+ gem "multi_json"
20
+ end
21
+
22
+ require "active_record"
23
+ require "sqlite3"
24
+ require "logger"
25
+ require "oj"
26
+ Oj.optimize_rails
27
+
28
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
29
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
30
+
31
+ ActiveRecord::Schema.define do
32
+ create_table :posts, force: true do |t|
33
+ t.string :body
34
+ end
35
+
36
+ create_table :comments, force: true do |t|
37
+ t.integer :post_id
38
+ t.string :body
39
+ t.integer :commenter_id
40
+ end
41
+
42
+ create_table :users, force: true do |t|
43
+ t.string :name
44
+ end
45
+ end
46
+
47
+ class Post < ActiveRecord::Base
48
+ has_many :comments
49
+ has_many :commenters, through: :comments, class_name: 'User', source: :commenter
50
+
51
+ def attributes
52
+ {id: nil, body: nil, commenter_names: commenter_names}
53
+ end
54
+
55
+ def commenter_names
56
+ commenters.pluck(:name)
57
+ end
58
+ end
59
+
60
+ class Comment < ActiveRecord::Base
61
+ belongs_to :post
62
+ belongs_to :commenter, class_name: 'User'
63
+
64
+ def attributes
65
+ {id: nil, body: nil}
66
+ end
67
+ end
68
+
69
+ class User < ActiveRecord::Base
70
+ has_many :comments
71
+ end
72
+
73
+ require "alba"
74
+ Alba.backend = :oj
75
+
76
+ class AlbaCommentResource
77
+ include ::Alba::Resource
78
+ attributes :id, :body
79
+ end
80
+
81
+ class AlbaPostResource
82
+ include ::Alba::Resource
83
+ attributes :id, :body
84
+ many :comments, resource: AlbaCommentResource
85
+ attribute :commenter_names do |post|
86
+ post.commenters.pluck(:name)
87
+ end
88
+ end
89
+
90
+ require "jbuilder"
91
+ class Post
92
+ def to_builder
93
+ Jbuilder.new do |post|
94
+ post.call(self, :id, :body, :comments, :commenter_names)
95
+ end
96
+ end
97
+
98
+ def commenter_names
99
+ commenters.pluck(:name)
100
+ end
101
+ end
102
+
103
+ class Comment
104
+ def to_builder
105
+ Jbuilder.new do |comment|
106
+ comment.call(self, :id, :body)
107
+ end
108
+ end
109
+ end
110
+
111
+ require "active_model_serializers"
112
+
113
+ class AMSCommentSerializer < ActiveModel::Serializer
114
+ attributes :id, :body
115
+ end
116
+
117
+ class AMSPostSerializer < ActiveModel::Serializer
118
+ attributes :id, :body
119
+ has_many :comments, serializer: AMSCommentSerializer
120
+ attribute :commenter_names
121
+ def commenter_names
122
+ object.commenters.pluck(:name)
123
+ end
124
+ end
125
+
126
+ require "blueprinter"
127
+
128
+ class CommentBlueprint < Blueprinter::Base
129
+ fields :id, :body
130
+ end
131
+
132
+ class PostBlueprint < Blueprinter::Base
133
+ fields :id, :body, :commenter_names
134
+ association :comments, blueprint: CommentBlueprint
135
+ def commenter_names
136
+ commenters.pluck(:name)
137
+ end
138
+ end
139
+
140
+ require "representable"
141
+
142
+ class CommentRepresenter < Representable::Decorator
143
+ include Representable::JSON
144
+
145
+ property :id
146
+ property :body
147
+ end
148
+
149
+ class PostRepresenter < Representable::Decorator
150
+ include Representable::JSON
151
+
152
+ property :id
153
+ property :body
154
+ property :commenter_names
155
+ collection :comments
156
+
157
+ def commenter_names
158
+ commenters.pluck(:name)
159
+ end
160
+ end
161
+
162
+ post = Post.create!(body: 'post')
163
+ user1 = User.create!(name: 'John')
164
+ user2 = User.create!(name: 'Jane')
165
+ post.comments.create!(commenter: user1, body: 'Comment1')
166
+ post.comments.create!(commenter: user2, body: 'Comment2')
167
+ post.reload
168
+
169
+ alba = Proc.new { AlbaPostResource.new(post).serialize }
170
+ jbuilder = Proc.new { post.to_builder.target! }
171
+ ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
172
+ rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
173
+ blueprinter = Proc.new { PostBlueprint.render(post) }
174
+ representable = Proc.new { PostRepresenter.new(post).to_json }
175
+ alba_inline = Proc.new do
176
+ Alba.serialize(post) do
177
+ attributes :id, :body
178
+ attribute :commenter_names do |post|
179
+ post.commenters.pluck(:name)
180
+ end
181
+ many :comments do
182
+ attributes :id, :body
183
+ end
184
+ end
185
+ end
186
+ [alba, jbuilder, ams, rails, blueprinter, representable, alba_inline].each {|x| puts x.call }
187
+
188
+ require 'benchmark'
189
+ time = 1000
190
+ Benchmark.bmbm do |x|
191
+ x.report(:alba) { time.times(&alba) }
192
+ x.report(:jbuilder) { time.times(&jbuilder) }
193
+ x.report(:ams) { time.times(&ams) }
194
+ x.report(:rails) { time.times(&rails) }
195
+ x.report(:blueprinter) { time.times(&blueprinter) }
196
+ x.report(:representable) { time.times(&representable) }
197
+ x.report(:alba_inline) { time.times(&alba_inline) }
198
+ end
data/lib/alba.rb CHANGED
@@ -1,28 +1,68 @@
1
- require 'alba/version'
2
- require 'alba/serializer'
3
- require 'alba/resource'
1
+ require_relative 'alba/version'
2
+ require_relative 'alba/resource'
4
3
 
5
4
  # Core module
6
5
  module Alba
6
+ # Base class for Errors
7
7
  class Error < StandardError; end
8
+
9
+ # Error class for backend which is not supported
8
10
  class UnsupportedBackend < Error; end
9
11
 
10
12
  class << self
11
- attr_reader :backend, :encoder
12
- attr_accessor :default_serializer
13
+ attr_reader :backend, :encoder, :inferring, :_on_error
13
14
 
15
+ # Set the backend, which actually serializes object into JSON
16
+ #
17
+ # @param backend [#to_sym, nil] the name of the backend
18
+ # Possible values are `oj`, `active_support`, `default`, `json` and nil
19
+ # @return [Proc] the proc to encode object into JSON
20
+ # @raise [Alba::UnsupportedBackend] if backend is not supported
14
21
  def backend=(backend)
15
22
  @backend = backend&.to_sym
16
23
  set_encoder
17
24
  end
18
25
 
19
- def serialize(object, with: nil, &block)
26
+ # Serialize the object with inline definitions
27
+ #
28
+ # @param object [Object] the object to be serialized
29
+ # @param key [Symbol]
30
+ # @param block [Block] resource block
31
+ # @return [String] serialized JSON string
32
+ # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
33
+ def serialize(object, key: nil, &block)
20
34
  raise ArgumentError, 'Block required' unless block
21
35
 
22
36
  resource_class.class_eval(&block)
23
37
  resource = resource_class.new(object)
24
- with ||= @default_serializer
25
- resource.serialize(with: with)
38
+ resource.serialize(key: key)
39
+ end
40
+
41
+ # Enable inference for key and resource name
42
+ def enable_inference!
43
+ begin
44
+ require 'active_support/inflector'
45
+ rescue LoadError
46
+ raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
47
+ end
48
+ @inferring = true
49
+ end
50
+
51
+ # Disable inference for key and resource name
52
+ def disable_inference!
53
+ @inferring = false
54
+ end
55
+
56
+ # Set error handler
57
+ #
58
+ # @param [Symbol] handler
59
+ # @param [Block]
60
+ def on_error(handler = nil, &block)
61
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
62
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
63
+
64
+ p block if block
65
+ @_on_error = handler || block
26
66
  end
27
67
 
28
68
  private
@@ -63,11 +103,12 @@ module Alba
63
103
 
64
104
  def resource_class
65
105
  @resource_class ||= begin
66
- klass = Class.new
67
- klass.include(Alba::Resource)
68
- end
106
+ klass = Class.new
107
+ klass.include(Alba::Resource)
108
+ end
69
109
  end
70
110
  end
71
111
 
72
112
  @encoder = default_encoder
113
+ @_on_error = :raise
73
114
  end
@@ -2,20 +2,45 @@ module Alba
2
2
  # Base class for `One` and `Many`
3
3
  # Child class should implement `to_hash` method
4
4
  class Association
5
- def initialize(name:, condition: nil, resource: nil, &block)
5
+ attr_reader :object
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
10
+ # @param block [Block] used to define resource when resource arg is absent
11
+ def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
6
12
  @name = name
7
13
  @condition = condition
8
14
  @block = block
9
- @resource = resource || resource_class
10
- raise ArgumentError, 'resource or block is required' if @resource.nil? && @block.nil?
15
+ @resource = resource
16
+ return if @resource
17
+
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
11
26
  end
12
27
 
28
+ # @abstract
13
29
  def to_hash
14
30
  :not_implemented
15
31
  end
16
32
 
17
33
  private
18
34
 
35
+ def constantize(resource)
36
+ case resource # rubocop:disable Style/MissingElse
37
+ when Class
38
+ resource
39
+ when Symbol, String
40
+ Object.const_get(resource)
41
+ end
42
+ end
43
+
19
44
  def resource_class
20
45
  klass = Class.new
21
46
  klass.include(Alba::Resource)
@@ -0,0 +1,31 @@
1
+ module Alba
2
+ # Transform keys using `ActiveSupport::Inflector`
3
+ module KeyTransformer
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
+ # Transform key as given transform_type
13
+ #
14
+ # @params key [String] key to be transformed
15
+ # @params transform_type [Symbol] transform type
16
+ # @return [String] transformed key
17
+ # @raise [Alba::Error] when transform_type is not supported
18
+ def transform(key, transform_type)
19
+ case transform_type
20
+ when :camel
21
+ ActiveSupport::Inflector.camelize(key)
22
+ when :lower_camel
23
+ ActiveSupport::Inflector.camelize(key, false)
24
+ when :dash
25
+ ActiveSupport::Inflector.dasherize(key)
26
+ else
27
+ raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/alba/many.rb CHANGED
@@ -1,12 +1,20 @@
1
- require 'alba/association'
1
+ require_relative 'association'
2
2
 
3
3
  module Alba
4
4
  # Representing many association
5
5
  class Many < Association
6
+ # Recursively converts objects into an Array of Hashes
7
+ #
8
+ # @param target [Object] the object having an association method
9
+ # @param params [Hash] user-given Hash for arbitrary data
10
+ # @return [Array<Hash>]
6
11
  def to_hash(target, params: {})
7
- objects = target.public_send(@name)
8
- objects = @condition.call(objects, params) if @condition
9
- objects.map { |o| @resource.new(o, params: params).to_hash }
12
+ @object = target.public_send(@name)
13
+ @object = @condition.call(@object, params) if @condition
14
+ return if @object.nil?
15
+
16
+ @resource = constantize(@resource)
17
+ @object.map { |o| @resource.new(o, params: params).to_hash }
10
18
  end
11
19
  end
12
20
  end
data/lib/alba/one.rb CHANGED
@@ -1,11 +1,19 @@
1
- require 'alba/association'
1
+ require_relative 'association'
2
2
 
3
3
  module Alba
4
4
  # Representing one association
5
5
  class One < Association
6
+ # Recursively converts an object into a Hash
7
+ #
8
+ # @param target [Object] the object having an association method
9
+ # @param params [Hash] user-given Hash for arbitrary data
10
+ # @return [Hash]
6
11
  def to_hash(target, params: {})
7
- object = target.public_send(@name)
8
- object = @condition.call(object, params) if @condition
12
+ @object = target.public_send(@name)
13
+ @object = @condition.call(object, params) if @condition
14
+ return if @object.nil?
15
+
16
+ @resource = constantize(@resource)
9
17
  @resource.new(object, params: params).to_hash
10
18
  end
11
19
  end
data/lib/alba/resource.rb CHANGED
@@ -1,11 +1,15 @@
1
- require 'alba/serializer'
2
- require 'alba/one'
3
- require 'alba/many'
1
+ require_relative 'one'
2
+ require_relative 'many'
4
3
 
5
4
  module Alba
6
5
  # This module represents what should be serialized
7
6
  module Resource
8
- DSLS = {_attributes: {}, _serializer: nil, _key: nil}.freeze
7
+ # @!parse include InstanceMethods
8
+ # @!parse extend ClassMethods
9
+ DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
10
+ private_constant :DSLS
11
+
12
+ # @private
9
13
  def self.included(base)
10
14
  super
11
15
  base.class_eval do
@@ -20,66 +24,116 @@ module Alba
20
24
 
21
25
  # Instance methods
22
26
  module InstanceMethods
23
- attr_reader :object, :_key, :params
27
+ attr_reader :object, :params
24
28
 
29
+ # @param object [Object] the object to be serialized
30
+ # @param params [Hash] user-given Hash for arbitrary data
25
31
  def initialize(object, params: {})
26
32
  @object = object
27
- @params = params
33
+ @params = params.freeze
28
34
  DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
29
35
  end
30
36
 
31
- def serialize(with: nil)
32
- serializer = case with
33
- when nil
34
- @_serializer || empty_serializer
35
- when ->(obj) { obj.is_a?(Class) && obj <= Alba::Serializer }
36
- with
37
- when Proc
38
- inline_extended_serializer(with)
39
- else
40
- raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc'
41
- end
42
- serializer.new(self).serialize
37
+ # Serialize object into JSON string
38
+ #
39
+ # @param key [Symbol]
40
+ # @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
44
+ Alba.encoder.call(hash)
43
45
  end
44
46
 
47
+ # A Hash for serialization
48
+ #
49
+ # @return [Hash]
45
50
  def serializable_hash
46
51
  collection? ? @object.map(&converter) : converter.call(@object)
47
52
  end
48
53
  alias to_hash serializable_hash
49
54
 
50
- def key
51
- @_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym
52
- end
53
-
54
55
  private
55
56
 
57
+ # @return [String]
58
+ 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
66
+ end
67
+
56
68
  def converter
57
- lambda do |resource|
58
- @_attributes.transform_values do |attribute|
59
- case attribute
60
- when Symbol
61
- resource.public_send attribute
62
- when Proc
63
- instance_exec(resource, &attribute)
64
- when Alba::One, Alba::Many
65
- attribute.to_hash(resource, params: params)
69
+ lambda do |object|
70
+ arrays = @_attributes.map do |key, attribute|
71
+ key = transform_key(key)
72
+ if attribute.is_a?(Array) # Conditional
73
+ conditional_attribute(object, key, attribute)
66
74
  else
67
- raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
75
+ [key, fetch_attribute(object, attribute)]
68
76
  end
77
+ rescue ::Alba::Error, FrozenError
78
+ raise
79
+ rescue StandardError => e
80
+ handle_error(e, object, key, attribute)
69
81
  end
82
+ arrays.reject(&:empty?).to_h
70
83
  end
71
84
  end
72
85
 
73
- def empty_serializer
74
- klass = Class.new
75
- klass.include Alba::Serializer
76
- klass
86
+ def conditional_attribute(object, key, attribute)
87
+ condition = attribute.last
88
+ arity = condition.arity
89
+ return [] if arity <= 1 && !condition.call(object)
90
+
91
+ 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)
98
+
99
+ [key, fetched_attribute]
100
+ end
101
+
102
+ def handle_error(error, object, key, attribute)
103
+ on_error = @_on_error || Alba._on_error
104
+ 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)
113
+ else
114
+ raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
115
+ end
77
116
  end
78
117
 
79
- def inline_extended_serializer(with)
80
- klass = empty_serializer
81
- klass.class_eval(&with)
82
- klass
118
+ # Override this method to supply custom key transform method
119
+ def transform_key(key)
120
+ return key unless @_transform_keys
121
+
122
+ require_relative 'key_transformer'
123
+ KeyTransformer.transform(key, @_transform_keys)
124
+ end
125
+
126
+ def fetch_attribute(object, attribute)
127
+ 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)
134
+ else
135
+ raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
136
+ end
83
137
  end
84
138
 
85
139
  def collection?
@@ -91,43 +145,107 @@ module Alba
91
145
  module ClassMethods
92
146
  attr_reader(*DSLS.keys)
93
147
 
148
+ # @private
94
149
  def inherited(subclass)
95
150
  super
96
151
  DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
97
152
  end
98
153
 
99
- def attributes(*attrs)
100
- attrs.each { |attr_name| @_attributes[attr_name.to_sym] = attr_name.to_sym }
154
+ # Set multiple attributes at once
155
+ #
156
+ # @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)
159
+ attrs.each do |attr_name|
160
+ attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
161
+ @_attributes[attr_name.to_sym] = attr
162
+ end
101
163
  end
102
164
 
103
- def attribute(name, &block)
165
+ # Set an attribute with the given block
166
+ #
167
+ # @param name [String, Symbol] key name
168
+ # @param options [Hash] option hash including `if` that is a condition to render
169
+ # @param block [Block] the block called during serialization
170
+ # @raise [ArgumentError] if block is absent
171
+ def attribute(name, **options, &block)
104
172
  raise ArgumentError, 'No block given in attribute method' unless block
105
173
 
106
- @_attributes[name.to_sym] = block
107
- end
108
-
109
- def one(name, condition = nil, resource: nil, key: nil, &block)
110
- @_attributes[key&.to_sym || name.to_sym] = One.new(name: name, condition: condition, resource: resource, &block)
111
- end
112
-
113
- def many(name, condition = nil, resource: nil, key: nil, &block)
114
- @_attributes[key&.to_sym || name.to_sym] = Many.new(name: name, condition: condition, resource: resource, &block)
115
- end
116
-
117
- def serializer(name)
118
- @_serializer = name <= Alba::Serializer ? name : nil
174
+ @_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
175
+ end
176
+
177
+ # Set One association
178
+ #
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
184
+ # @param block [Block]
185
+ # @see Alba::One#initialize
186
+ def one(name, condition = nil, resource: nil, key: nil, **options, &block)
187
+ nesting = self.name&.rpartition('::')&.first
188
+ one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
189
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one
190
+ end
191
+ alias has_one one
192
+
193
+ # Set Many association
194
+ #
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
200
+ # @param block [Block]
201
+ # @see Alba::Many#initialize
202
+ def many(name, condition = nil, resource: nil, key: nil, **options, &block)
203
+ nesting = self.name&.rpartition('::')&.first
204
+ many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
205
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
206
+ end
207
+ alias has_many many
208
+
209
+ # Set key
210
+ #
211
+ # @param key [String, Symbol]
212
+ def key(key)
213
+ @_key = key.respond_to?(:to_sym) ? key.to_sym : key
119
214
  end
120
215
 
121
- def key(key)
122
- @_key = key.to_sym
216
+ # Set key to true
217
+ #
218
+ def key!
219
+ @_key = true
123
220
  end
124
221
 
222
+ # Delete attributes
125
223
  # Use this DSL in child class to ignore certain attributes
224
+ #
225
+ # @param attributes [Array<String, Symbol>]
126
226
  def ignoring(*attributes)
127
227
  attributes.each do |attr_name|
128
228
  @_attributes.delete(attr_name.to_sym)
129
229
  end
130
230
  end
231
+
232
+ # Transform keys as specified type
233
+ #
234
+ # @param type [String, Symbol]
235
+ def transform_keys(type)
236
+ @_transform_keys = type.to_sym
237
+ end
238
+
239
+ # Set error handler
240
+ #
241
+ # @param [Symbol] handler
242
+ # @param [Block]
243
+ def on_error(handler = nil, &block)
244
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
245
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
246
+
247
+ @_on_error = handler || block
248
+ end
131
249
  end
132
250
  end
133
251
  end