alba 1.1.0 → 1.5.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.
- 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