alba 0.13.0 → 1.2.0

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