alba 0.11.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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