alba 1.1.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/dependabot.yml +26 -0
- data/.github/workflows/perf.yml +21 -0
- data/.rubocop.yml +18 -8
- data/.yardopts +2 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +4 -3
- data/README.md +377 -14
- data/SECURITY.md +12 -0
- data/alba.gemspec +2 -2
- data/benchmark/collection.rb +441 -0
- data/benchmark/{local.rb → single_resource.rb} +120 -15
- data/codecov.yml +3 -0
- data/docs/migrate_from_active_model_serializers.md +359 -0
- data/docs/migrate_from_jbuilder.md +223 -0
- data/gemfiles/all.gemfile +1 -1
- data/gemfiles/without_active_support.gemfile +1 -1
- data/gemfiles/without_oj.gemfile +1 -1
- data/lib/alba/association.rb +14 -17
- data/lib/alba/default_inflector.rb +36 -0
- data/lib/alba/deprecation.rb +14 -0
- data/lib/alba/key_transform_factory.rb +33 -0
- data/lib/alba/many.rb +1 -1
- data/lib/alba/resource.rb +226 -83
- data/lib/alba/typed_attribute.rb +61 -0
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +82 -21
- data/script/perf_check.rb +174 -0
- data/sider.yml +2 -4
- metadata +20 -9
- data/lib/alba/key_transformer.rb +0 -32
@@ -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
data/lib/alba/resource.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
require_relative 'one'
|
2
2
|
require_relative 'many'
|
3
|
+
require_relative 'key_transform_factory'
|
4
|
+
require_relative 'typed_attribute'
|
5
|
+
require_relative 'deprecation'
|
3
6
|
|
4
7
|
module Alba
|
5
8
|
# This module represents what should be serialized
|
6
9
|
module Resource
|
7
10
|
# @!parse include InstanceMethods
|
8
11
|
# @!parse extend ClassMethods
|
9
|
-
DSLS = {_attributes: {}, _key: nil,
|
12
|
+
DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil, _on_nil: nil, _layout: nil}.freeze # rubocop:disable Layout/LineLength
|
10
13
|
private_constant :DSLS
|
11
14
|
|
15
|
+
WITHIN_DEFAULT = Object.new.freeze
|
16
|
+
private_constant :WITHIN_DEFAULT
|
17
|
+
|
12
18
|
# @private
|
13
19
|
def self.included(base)
|
14
20
|
super
|
@@ -28,8 +34,8 @@ module Alba
|
|
28
34
|
|
29
35
|
# @param object [Object] the object to be serialized
|
30
36
|
# @param params [Hash] user-given Hash for arbitrary data
|
31
|
-
# @param within [
|
32
|
-
def initialize(object, params: {}, within:
|
37
|
+
# @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
|
38
|
+
def initialize(object, params: {}, within: WITHIN_DEFAULT)
|
33
39
|
@object = object
|
34
40
|
@params = params.freeze
|
35
41
|
@within = within
|
@@ -38,13 +44,22 @@ module Alba
|
|
38
44
|
|
39
45
|
# Serialize object into JSON string
|
40
46
|
#
|
41
|
-
# @param key [Symbol]
|
47
|
+
# @param key [Symbol, nil, true] DEPRECATED, use root_key instead
|
48
|
+
# @param root_key [Symbol, nil, true]
|
49
|
+
# @param meta [Hash] metadata for this seialization
|
42
50
|
# @return [String] serialized JSON string
|
43
|
-
def serialize(key: nil)
|
44
|
-
key
|
45
|
-
|
46
|
-
|
51
|
+
def serialize(key: nil, root_key: nil, meta: {})
|
52
|
+
Alba::Deprecation.warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
|
53
|
+
key = key.nil? && root_key.nil? ? fetch_key : root_key || key
|
54
|
+
hash = if key && key != ''
|
55
|
+
h = {key.to_s => serializable_hash}
|
56
|
+
hash_with_metadata(h, meta)
|
57
|
+
else
|
58
|
+
serializable_hash
|
59
|
+
end
|
60
|
+
serialize_with(hash)
|
47
61
|
end
|
62
|
+
alias to_json serialize
|
48
63
|
|
49
64
|
# A Hash for serialization
|
50
65
|
#
|
@@ -56,27 +71,63 @@ module Alba
|
|
56
71
|
|
57
72
|
private
|
58
73
|
|
74
|
+
attr_reader :serialized_json # Mainly for layout
|
75
|
+
|
76
|
+
def encode(hash)
|
77
|
+
Alba.encoder.call(hash)
|
78
|
+
end
|
79
|
+
|
80
|
+
def serialize_with(hash)
|
81
|
+
@serialized_json = encode(hash)
|
82
|
+
case @_layout
|
83
|
+
when String # file
|
84
|
+
ERB.new(File.read(@_layout)).result(binding)
|
85
|
+
when Proc # inline
|
86
|
+
inline = instance_eval(&@_layout)
|
87
|
+
inline.is_a?(Hash) ? encode(inline) : inline
|
88
|
+
else # no layout
|
89
|
+
@serialized_json
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def hash_with_metadata(hash, meta)
|
94
|
+
base = @_meta ? instance_eval(&@_meta) : {}
|
95
|
+
metadata = base.merge(meta)
|
96
|
+
hash[:meta] = metadata unless metadata.empty?
|
97
|
+
hash
|
98
|
+
end
|
99
|
+
|
100
|
+
def fetch_key
|
101
|
+
collection? ? _key_for_collection : _key
|
102
|
+
end
|
103
|
+
|
104
|
+
def _key_for_collection
|
105
|
+
return @_key_for_collection.to_s unless @_key_for_collection == true && Alba.inferring
|
106
|
+
|
107
|
+
key = resource_name.pluralize
|
108
|
+
transforming_root_key? ? transform_key(key) : key
|
109
|
+
end
|
110
|
+
|
59
111
|
# @return [String]
|
60
112
|
def _key
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
113
|
+
return @_key.to_s unless @_key == true && Alba.inferring
|
114
|
+
|
115
|
+
transforming_root_key? ? transform_key(resource_name) : resource_name
|
116
|
+
end
|
117
|
+
|
118
|
+
def resource_name
|
119
|
+
self.class.name.demodulize.delete_suffix('Resource').underscore
|
120
|
+
end
|
121
|
+
|
122
|
+
def transforming_root_key?
|
123
|
+
@_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
|
68
124
|
end
|
69
125
|
|
70
126
|
def converter
|
71
127
|
lambda do |object|
|
72
128
|
arrays = @_attributes.map do |key, attribute|
|
73
|
-
key
|
74
|
-
|
75
|
-
conditional_attribute(object, key, attribute)
|
76
|
-
else
|
77
|
-
[key, fetch_attribute(object, attribute)]
|
78
|
-
end
|
79
|
-
rescue ::Alba::Error, FrozenError
|
129
|
+
key_and_attribute_body_from(object, key, attribute)
|
130
|
+
rescue ::Alba::Error, FrozenError, TypeError
|
80
131
|
raise
|
81
132
|
rescue StandardError => e
|
82
133
|
handle_error(e, object, key, attribute)
|
@@ -85,33 +136,50 @@ module Alba
|
|
85
136
|
end
|
86
137
|
end
|
87
138
|
|
139
|
+
def key_and_attribute_body_from(object, key, attribute)
|
140
|
+
key = transform_key(key)
|
141
|
+
if attribute.is_a?(Array) # Conditional
|
142
|
+
conditional_attribute(object, key, attribute)
|
143
|
+
else
|
144
|
+
fetched_attribute = fetch_attribute(object, key, attribute)
|
145
|
+
[key, fetched_attribute]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
88
149
|
def conditional_attribute(object, key, attribute)
|
89
150
|
condition = attribute.last
|
151
|
+
if condition.is_a?(Proc)
|
152
|
+
conditional_attribute_with_proc(object, key, attribute.first, condition)
|
153
|
+
else
|
154
|
+
conditional_attribute_with_symbol(object, key, attribute.first, condition)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def conditional_attribute_with_proc(object, key, attribute, condition)
|
90
159
|
arity = condition.arity
|
91
|
-
|
160
|
+
# We can return early to skip fetch_attribute
|
161
|
+
return [] if arity <= 1 && !instance_exec(object, &condition)
|
92
162
|
|
93
|
-
fetched_attribute = fetch_attribute(object, attribute
|
94
|
-
attr =
|
95
|
-
|
96
|
-
else
|
97
|
-
fetched_attribute
|
98
|
-
end
|
99
|
-
return [] if arity >= 2 && !condition.call(object, attr)
|
163
|
+
fetched_attribute = fetch_attribute(object, key, attribute)
|
164
|
+
attr = attribute.is_a?(Alba::Association) ? attribute.object : fetched_attribute
|
165
|
+
return [] if arity >= 2 && !instance_exec(object, attr, &condition)
|
100
166
|
|
101
167
|
[key, fetched_attribute]
|
102
168
|
end
|
103
169
|
|
170
|
+
def conditional_attribute_with_symbol(object, key, attribute, condition)
|
171
|
+
return [] unless __send__(condition)
|
172
|
+
|
173
|
+
[key, fetch_attribute(object, key, attribute)]
|
174
|
+
end
|
175
|
+
|
104
176
|
def handle_error(error, object, key, attribute)
|
105
177
|
on_error = @_on_error || Alba._on_error
|
106
178
|
case on_error
|
107
|
-
when :raise, nil
|
108
|
-
|
109
|
-
when :
|
110
|
-
|
111
|
-
when :ignore
|
112
|
-
[]
|
113
|
-
when Proc
|
114
|
-
on_error.call(error, object, key, attribute, self.class)
|
179
|
+
when :raise, nil then raise
|
180
|
+
when :nullify then [key, nil]
|
181
|
+
when :ignore then []
|
182
|
+
when Proc then on_error.call(error, object, key, attribute, self.class)
|
115
183
|
else
|
116
184
|
raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
|
117
185
|
end
|
@@ -119,40 +187,39 @@ module Alba
|
|
119
187
|
|
120
188
|
# Override this method to supply custom key transform method
|
121
189
|
def transform_key(key)
|
122
|
-
return key
|
190
|
+
return key if @_transform_key_function.nil?
|
123
191
|
|
124
|
-
|
125
|
-
KeyTransformer.transform(key, @_transform_keys)
|
192
|
+
@_transform_key_function.call(key.to_s)
|
126
193
|
end
|
127
194
|
|
128
|
-
def fetch_attribute(object, attribute)
|
129
|
-
case attribute
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
195
|
+
def fetch_attribute(object, key, attribute)
|
196
|
+
value = case attribute
|
197
|
+
when Symbol then object.public_send attribute
|
198
|
+
when Proc then instance_exec(object, &attribute)
|
199
|
+
when Alba::One, Alba::Many then yield_if_within(attribute.name.to_sym) { |within| attribute.to_hash(object, params: params, within: within) }
|
200
|
+
when TypedAttribute then attribute.value(object)
|
201
|
+
else
|
202
|
+
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
203
|
+
end
|
204
|
+
value.nil? && nil_handler ? instance_exec(object, key, attribute, &nil_handler) : value
|
205
|
+
end
|
137
206
|
|
138
|
-
|
139
|
-
|
140
|
-
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
141
|
-
end
|
207
|
+
def nil_handler
|
208
|
+
@nil_handler ||= (@_on_nil || Alba._on_nil)
|
142
209
|
end
|
143
210
|
|
144
|
-
def
|
211
|
+
def yield_if_within(association_name)
|
212
|
+
within = check_within(association_name)
|
213
|
+
yield(within) if within
|
214
|
+
end
|
215
|
+
|
216
|
+
def check_within(association_name)
|
145
217
|
case @within
|
146
|
-
when
|
147
|
-
|
148
|
-
when Array
|
149
|
-
|
150
|
-
when
|
151
|
-
@within == _key.to_sym # Check if the symbol matches current resource
|
152
|
-
when true # In this case, Alba serializes all associations.
|
153
|
-
true
|
154
|
-
when nil, false # In these cases, Alba stops serialization here.
|
155
|
-
false
|
218
|
+
when WITHIN_DEFAULT then WITHIN_DEFAULT # Default value, doesn't check within tree
|
219
|
+
when Hash then @within.fetch(association_name, nil) # Traverse within tree
|
220
|
+
when Array then @within.find { |item| item.to_sym == association_name }
|
221
|
+
when Symbol then @within == association_name
|
222
|
+
when nil, true, false then false # Stop here
|
156
223
|
else
|
157
224
|
raise Alba::Error, "Unknown type for within option: #{@within.class}"
|
158
225
|
end
|
@@ -173,23 +240,49 @@ module Alba
|
|
173
240
|
DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
|
174
241
|
end
|
175
242
|
|
243
|
+
# Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine
|
244
|
+
# rubocop:disable Metrics/ParameterLists
|
245
|
+
|
176
246
|
# Set multiple attributes at once
|
177
247
|
#
|
178
248
|
# @param attrs [Array<String, Symbol>]
|
179
|
-
# @param
|
180
|
-
|
249
|
+
# @param if [Proc] condition to decide if it should serialize these attributes
|
250
|
+
# @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
|
251
|
+
# attributes with name in its key and type and optional type converter in its value
|
252
|
+
# @return [void]
|
253
|
+
def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
|
254
|
+
if_value = binding.local_variable_get(:if)
|
255
|
+
assign_attributes(attrs, if_value)
|
256
|
+
assign_attributes_with_types(attrs_with_types, if_value)
|
257
|
+
end
|
258
|
+
|
259
|
+
def assign_attributes(attrs, if_value)
|
181
260
|
attrs.each do |attr_name|
|
182
|
-
attr =
|
261
|
+
attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
|
183
262
|
@_attributes[attr_name.to_sym] = attr
|
184
263
|
end
|
185
264
|
end
|
265
|
+
private :assign_attributes
|
266
|
+
|
267
|
+
def assign_attributes_with_types(attrs_with_types, if_value)
|
268
|
+
attrs_with_types.each do |attr_name, type_and_converter|
|
269
|
+
attr_name = attr_name.to_sym
|
270
|
+
type, type_converter = type_and_converter
|
271
|
+
typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter)
|
272
|
+
attr = if_value ? [typed_attr, if_value] : typed_attr
|
273
|
+
@_attributes[attr_name] = attr
|
274
|
+
end
|
275
|
+
end
|
276
|
+
private :assign_attributes_with_types
|
186
277
|
|
187
278
|
# Set an attribute with the given block
|
188
279
|
#
|
189
280
|
# @param name [String, Symbol] key name
|
190
|
-
# @param options [Hash]
|
281
|
+
# @param options [Hash<Symbol, Proc>]
|
282
|
+
# @option options [Proc] if a condition to decide if this attribute should be serialized
|
191
283
|
# @param block [Block] the block called during serialization
|
192
284
|
# @raise [ArgumentError] if block is absent
|
285
|
+
# @return [void]
|
193
286
|
def attribute(name, **options, &block)
|
194
287
|
raise ArgumentError, 'No block given in attribute method' unless block
|
195
288
|
|
@@ -198,12 +291,14 @@ module Alba
|
|
198
291
|
|
199
292
|
# Set One association
|
200
293
|
#
|
201
|
-
# @param name [String, Symbol]
|
202
|
-
# @param condition [Proc]
|
203
|
-
# @param resource [Class<Alba::Resource
|
204
|
-
# @param key [String, Symbol] used as key when given
|
205
|
-
# @param options [Hash]
|
294
|
+
# @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
|
295
|
+
# @param condition [Proc, nil] a Proc to modify the association
|
296
|
+
# @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
|
297
|
+
# @param key [String, Symbol, nil] used as key when given
|
298
|
+
# @param options [Hash<Symbol, Proc>]
|
299
|
+
# @option options [Proc] if a condition to decide if this association should be serialized
|
206
300
|
# @param block [Block]
|
301
|
+
# @return [void]
|
207
302
|
# @see Alba::One#initialize
|
208
303
|
def one(name, condition = nil, resource: nil, key: nil, **options, &block)
|
209
304
|
nesting = self.name&.rpartition('::')&.first
|
@@ -214,12 +309,14 @@ module Alba
|
|
214
309
|
|
215
310
|
# Set Many association
|
216
311
|
#
|
217
|
-
# @param name [String, Symbol]
|
218
|
-
# @param condition [Proc]
|
219
|
-
# @param resource [Class<Alba::Resource
|
220
|
-
# @param key [String, Symbol] used as key when given
|
221
|
-
# @param options [Hash]
|
312
|
+
# @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
|
313
|
+
# @param condition [Proc, nil] a Proc to filter the collection
|
314
|
+
# @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
|
315
|
+
# @param key [String, Symbol, nil] used as key when given
|
316
|
+
# @param options [Hash<Symbol, Proc>]
|
317
|
+
# @option options [Proc] if a condition to decide if this association should be serialized
|
222
318
|
# @param block [Block]
|
319
|
+
# @return [void]
|
223
320
|
# @see Alba::Many#initialize
|
224
321
|
def many(name, condition = nil, resource: nil, key: nil, **options, &block)
|
225
322
|
nesting = self.name&.rpartition('::')&.first
|
@@ -231,14 +328,48 @@ module Alba
|
|
231
328
|
# Set key
|
232
329
|
#
|
233
330
|
# @param key [String, Symbol]
|
331
|
+
# @deprecated Use {#root_key} instead
|
234
332
|
def key(key)
|
333
|
+
Alba::Deprecation.warn '[DEPRECATION] `key` is deprecated, use `root_key` instead.'
|
235
334
|
@_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
236
335
|
end
|
237
336
|
|
337
|
+
# Set root key
|
338
|
+
#
|
339
|
+
# @param key [String, Symbol]
|
340
|
+
# @param key_for_collection [String, Symbol]
|
341
|
+
# @raise [NoMethodError] when key doesn't respond to `to_sym` method
|
342
|
+
def root_key(key, key_for_collection = nil)
|
343
|
+
@_key = key.to_sym
|
344
|
+
@_key_for_collection = key_for_collection&.to_sym
|
345
|
+
end
|
346
|
+
|
238
347
|
# Set key to true
|
239
348
|
#
|
349
|
+
# @deprecated Use {#root_key!} instead
|
240
350
|
def key!
|
351
|
+
Alba::Deprecation.warn '[DEPRECATION] `key!` is deprecated, use `root_key!` instead.'
|
352
|
+
@_key = true
|
353
|
+
@_key_for_collection = true
|
354
|
+
end
|
355
|
+
|
356
|
+
# Set root key to true
|
357
|
+
def root_key!
|
241
358
|
@_key = true
|
359
|
+
@_key_for_collection = true
|
360
|
+
end
|
361
|
+
|
362
|
+
# Set metadata
|
363
|
+
def meta(&block)
|
364
|
+
@_meta = block
|
365
|
+
end
|
366
|
+
|
367
|
+
# Set layout
|
368
|
+
#
|
369
|
+
# @params file [String] name of the layout file
|
370
|
+
# @params inline [Proc] a proc returning JSON string or a Hash representing JSON
|
371
|
+
def layout(file: nil, inline: nil)
|
372
|
+
@_layout = file || inline
|
242
373
|
end
|
243
374
|
|
244
375
|
# Delete attributes
|
@@ -254,20 +385,32 @@ module Alba
|
|
254
385
|
# Transform keys as specified type
|
255
386
|
#
|
256
387
|
# @param type [String, Symbol]
|
257
|
-
|
258
|
-
|
388
|
+
# @param root [Boolean] decides if root key also should be transformed
|
389
|
+
def transform_keys(type, root: nil)
|
390
|
+
@_transform_key_function = KeyTransformFactory.create(type.to_sym)
|
391
|
+
@_transforming_root_key = root
|
259
392
|
end
|
260
393
|
|
261
394
|
# Set error handler
|
395
|
+
# If this is set it's used as a error handler overriding global one
|
262
396
|
#
|
263
|
-
# @param [Symbol]
|
264
|
-
# @param [Block]
|
397
|
+
# @param handler [Symbol] `:raise`, `:ignore` or `:nullify`
|
398
|
+
# @param block [Block]
|
265
399
|
def on_error(handler = nil, &block)
|
266
400
|
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
267
401
|
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
268
402
|
|
269
403
|
@_on_error = handler || block
|
270
404
|
end
|
405
|
+
|
406
|
+
# Set nil handler
|
407
|
+
#
|
408
|
+
# @param block [Block]
|
409
|
+
def on_nil(&block)
|
410
|
+
@_on_nil = block
|
411
|
+
end
|
412
|
+
|
413
|
+
# rubocop:enable Metrics/ParameterLists
|
271
414
|
end
|
272
415
|
end
|
273
416
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Alba
|
2
|
+
# Representing typed attributes to encapsulate logic about types
|
3
|
+
class TypedAttribute
|
4
|
+
# @param name [Symbol, String]
|
5
|
+
# @param type [Symbol, Class]
|
6
|
+
# @param converter [Proc]
|
7
|
+
def initialize(name:, type:, converter:)
|
8
|
+
@name = name
|
9
|
+
@type = type
|
10
|
+
@converter = case converter
|
11
|
+
when true then default_converter
|
12
|
+
when false, nil then null_converter
|
13
|
+
else converter
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param object [Object] target to check and convert type with
|
18
|
+
# @return [String, Integer, Boolean] type-checked or type-converted object
|
19
|
+
def value(object)
|
20
|
+
value, result = check(object)
|
21
|
+
result ? value : @converter.call(value)
|
22
|
+
rescue TypeError
|
23
|
+
raise TypeError, "Attribute #{@name} is expected to be #{@type} but actually #{display_value_for(value)}."
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def check(object)
|
29
|
+
value = object.public_send(@name)
|
30
|
+
type_correct = case @type
|
31
|
+
when :String, ->(klass) { klass == String } then value.is_a?(String)
|
32
|
+
when :Integer, ->(klass) { klass == Integer } then value.is_a?(Integer)
|
33
|
+
when :Boolean then [true, false].include?(value)
|
34
|
+
else
|
35
|
+
raise Alba::UnsupportedType, "Unknown type: #{@type}"
|
36
|
+
end
|
37
|
+
[value, type_correct]
|
38
|
+
end
|
39
|
+
|
40
|
+
def default_converter
|
41
|
+
case @type
|
42
|
+
when :String, ->(klass) { klass == String }
|
43
|
+
->(object) { object.to_s }
|
44
|
+
when :Integer, ->(klass) { klass == Integer }
|
45
|
+
->(object) { Integer(object) }
|
46
|
+
when :Boolean
|
47
|
+
->(object) { !!object }
|
48
|
+
else
|
49
|
+
raise Alba::UnsupportedType, "Unknown type: #{@type}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def null_converter
|
54
|
+
->(_) { raise TypeError }
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_value_for(value)
|
58
|
+
value.nil? ? 'nil' : value.class.name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/alba/version.rb
CHANGED