attributor 2.3.0 → 2.4.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/CHANGELOG.md +9 -0
- data/lib/attributor.rb +15 -5
- data/lib/attributor/dsl_compiler.rb +11 -12
- data/lib/attributor/example_mixin.rb +63 -0
- data/lib/attributor/type.rb +6 -1
- data/lib/attributor/types/collection.rb +5 -0
- data/lib/attributor/types/hash.rb +90 -38
- data/lib/attributor/types/model.rb +36 -191
- data/lib/attributor/types/string.rb +4 -1
- data/lib/attributor/types/struct.rb +3 -0
- data/lib/attributor/types/symbol.rb +21 -0
- data/lib/attributor/version.rb +1 -1
- data/spec/dsl_compiler_spec.rb +4 -4
- data/spec/support/hashes.rb +7 -0
- data/spec/support/models.rb +1 -0
- data/spec/types/collection_spec.rb +7 -9
- data/spec/types/model_spec.rb +26 -18
- data/spec/types/string_spec.rb +9 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e832c8023013ffad5fa5a40e13200e5dcfeb4c4
|
4
|
+
data.tar.gz: a1d2a7043187872b6cdc424b503b8ce08a5935ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26f1bd976df8c4ad47ea3e617e2abdcabd7d6d070a82dce44abb2e16ededcae6289ca7e074ba503f9ee7f6f4e79036bb49078509c2812f63ca02d3eab190466e
|
7
|
+
data.tar.gz: ee7b663e6d9095d2909f355cf0f78e76d0cc5e6c8c0a2b0f44338fed5776a9a36360aba0b81f9a2b7ac532b88c2b8e7c8168187503405ed1b8cff1a82fc36cbd
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
Attributor Changelog
|
2
2
|
============================
|
3
3
|
|
4
|
+
2.4.0
|
5
|
+
------
|
6
|
+
|
7
|
+
* `Model` is now a subclass of `Hash`.
|
8
|
+
* The interface for `Model` instances is almost entirely unchanged, except for the addition of `Hash`-like methods (i.e., you can now do `some_model[:key]` to access attributes).
|
9
|
+
* This fixes numerous incompatabilities between models and hashes, as well as confusing differences between the behavior when loading a model vs a hash.
|
10
|
+
* `String.load` now raises `IncompatibleTypeError` for `Enumerable` values.
|
11
|
+
* Added `Symbol` type, use with caution as it will automatically call `#to_sym` on anything loaded.
|
12
|
+
|
4
13
|
2.3.0
|
5
14
|
------
|
6
15
|
|
data/lib/attributor.rb
CHANGED
@@ -13,7 +13,8 @@ module Attributor
|
|
13
13
|
require_relative 'attributor/dsl_compiler'
|
14
14
|
require_relative 'attributor/attribute_resolver'
|
15
15
|
|
16
|
-
|
16
|
+
require_relative 'attributor/example_mixin'
|
17
|
+
|
17
18
|
require_relative 'attributor/extensions/randexp'
|
18
19
|
|
19
20
|
|
@@ -37,7 +38,7 @@ module Attributor
|
|
37
38
|
raise AttributorException.new("Could not find attribute type for: #{name} [klass: #{klass.name}]") unless klass < Attributor::Type
|
38
39
|
end
|
39
40
|
|
40
|
-
if klass.
|
41
|
+
if klass.constructable?
|
41
42
|
return klass.construct(constructor_block, options)
|
42
43
|
end
|
43
44
|
|
@@ -48,7 +49,15 @@ module Attributor
|
|
48
49
|
|
49
50
|
def self.humanize_context( context )
|
50
51
|
raise "NIL CONTEXT PASSED TO HUMANZE!!" unless context
|
51
|
-
|
52
|
+
|
53
|
+
if context.kind_of? ::String
|
54
|
+
context = Array(context)
|
55
|
+
end
|
56
|
+
|
57
|
+
unless context.is_a? Enumerable
|
58
|
+
raise "INVALID CONTEXT!!! (got: #{context.inspect})"
|
59
|
+
end
|
60
|
+
|
52
61
|
begin
|
53
62
|
return context.join('.')
|
54
63
|
rescue Exception => e
|
@@ -70,8 +79,7 @@ module Attributor
|
|
70
79
|
require_relative 'attributor/types/bigdecimal'
|
71
80
|
require_relative 'attributor/types/integer'
|
72
81
|
require_relative 'attributor/types/string'
|
73
|
-
require_relative 'attributor/types/
|
74
|
-
require_relative 'attributor/types/struct'
|
82
|
+
require_relative 'attributor/types/symbol'
|
75
83
|
require_relative 'attributor/types/boolean'
|
76
84
|
require_relative 'attributor/types/date'
|
77
85
|
require_relative 'attributor/types/date_time'
|
@@ -79,6 +87,8 @@ module Attributor
|
|
79
87
|
require_relative 'attributor/types/float'
|
80
88
|
require_relative 'attributor/types/collection'
|
81
89
|
require_relative 'attributor/types/hash'
|
90
|
+
require_relative 'attributor/types/model'
|
91
|
+
require_relative 'attributor/types/struct'
|
82
92
|
|
83
93
|
|
84
94
|
require_relative 'attributor/types/csv'
|
@@ -80,6 +80,7 @@ module Attributor
|
|
80
80
|
# end
|
81
81
|
# @api semiprivate
|
82
82
|
def define(name, attr_type=nil, **opts, &block)
|
83
|
+
# add to existing attribute if present
|
83
84
|
if (existing_attribute = attributes[name])
|
84
85
|
if existing_attribute.attributes
|
85
86
|
existing_attribute.type.attributes(&block)
|
@@ -87,28 +88,26 @@ module Attributor
|
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
91
|
+
# determine inherited attribute
|
92
|
+
inherited_attribute = nil
|
90
93
|
if (reference = self.options[:reference])
|
91
|
-
inherited_attribute = reference.attributes[name]
|
92
|
-
|
93
|
-
|
94
|
+
if (inherited_attribute = reference.attributes[name])
|
95
|
+
opts = inherited_attribute.options.merge(opts) unless attr_type
|
96
|
+
opts[:reference] = inherited_attribute.type if block_given?
|
97
|
+
end
|
94
98
|
end
|
95
99
|
|
100
|
+
# determine attribute type to use
|
96
101
|
if attr_type.nil?
|
97
|
-
if
|
98
|
-
attr_type = inherited_attribute.type
|
99
|
-
# Only inherit opts if no explicit attr_type was given.
|
100
|
-
opts = inherited_attribute.options.merge(opts)
|
101
|
-
elsif block_given?
|
102
|
+
if block_given?
|
102
103
|
attr_type = Attributor::Struct
|
104
|
+
elsif inherited_attribute
|
105
|
+
attr_type = inherited_attribute.type
|
103
106
|
else
|
104
107
|
raise AttributorException, "type for attribute with name: #{name} could not be determined"
|
105
108
|
end
|
106
109
|
end
|
107
110
|
|
108
|
-
if block_given? && inherited_attribute
|
109
|
-
opts[:reference] = inherited_attribute.type
|
110
|
-
end
|
111
|
-
|
112
111
|
Attributor::Attribute.new(attr_type, opts, &block)
|
113
112
|
end
|
114
113
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# poorly-optimized, but handy, mixin for Hash and Model examples.
|
2
|
+
# primarily enables support for lazy values.
|
3
|
+
|
4
|
+
module Attributor
|
5
|
+
|
6
|
+
module ExampleMixin
|
7
|
+
|
8
|
+
def self.extended(obj)
|
9
|
+
obj.class.attributes.each do |name, _|
|
10
|
+
obj.define_singleton_method(name) do
|
11
|
+
get(name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def lazy_attributes
|
17
|
+
@lazy_attributes ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def lazy_attributes=(val)
|
21
|
+
@lazy_attributes = val
|
22
|
+
end
|
23
|
+
|
24
|
+
def keys
|
25
|
+
@contents.keys | lazy_attributes.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def key?(key)
|
29
|
+
@contents.key?(key) || lazy_attributes.key?(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
|
33
|
+
key = self.class.key_attribute.load(key, context)
|
34
|
+
|
35
|
+
unless @contents.key? key
|
36
|
+
if lazy_attributes.key?(key)
|
37
|
+
proc = lazy_attributes.delete(key)
|
38
|
+
@contents[key] = proc.call(self)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
def attributes
|
46
|
+
lazy_attributes.keys.each do |name|
|
47
|
+
self.__send__(name)
|
48
|
+
end
|
49
|
+
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def contents
|
54
|
+
lazy_attributes.keys do |key|
|
55
|
+
proc = lazy_attributes.delete(key)
|
56
|
+
@contents[key] = proc.call(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
data/lib/attributor/type.rb
CHANGED
@@ -10,7 +10,12 @@ module Attributor
|
|
10
10
|
|
11
11
|
|
12
12
|
module ClassMethods
|
13
|
-
|
13
|
+
|
14
|
+
# Does this type support the generation of subtypes?
|
15
|
+
def constructable?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
14
19
|
# Generic decoding and coercion of the attribute.
|
15
20
|
def load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
16
21
|
return nil if value.nil?
|
@@ -2,6 +2,9 @@ module Attributor
|
|
2
2
|
class Hash
|
3
3
|
extend Forwardable
|
4
4
|
|
5
|
+
MAX_EXAMPLE_DEPTH = 5
|
6
|
+
CIRCULAR_REFERENCE_MARKER = '...'.freeze
|
7
|
+
|
5
8
|
include Container
|
6
9
|
include Enumerable
|
7
10
|
|
@@ -19,24 +22,15 @@ module Attributor
|
|
19
22
|
@key_attribute = Attribute.new(@key_type)
|
20
23
|
@value_attribute = Attribute.new(@value_type)
|
21
24
|
|
22
|
-
def self.key_type=(key_type)
|
23
|
-
resolved_key_type = Attributor.resolve_type(key_type)
|
24
|
-
unless resolved_key_type.ancestors.include?(Attributor::Type)
|
25
|
-
raise Attributor::AttributorException.new("Hashes only support key types that are Attributor::Types. Got #{resolved_key_type.name}")
|
26
|
-
end
|
27
25
|
|
28
|
-
|
26
|
+
def self.key_type=(key_type)
|
27
|
+
@key_type = Attributor.resolve_type(key_type)
|
29
28
|
@key_attribute = Attribute.new(@key_type)
|
30
29
|
@concrete=true
|
31
30
|
end
|
32
31
|
|
33
32
|
def self.value_type=(value_type)
|
34
|
-
|
35
|
-
unless resolved_value_type.ancestors.include?(Attributor::Type)
|
36
|
-
raise Attributor::AttributorException.new("Hashes only support value types that are Attributor::Types. Got #{resolved_value_type.name}")
|
37
|
-
end
|
38
|
-
|
39
|
-
@value_type = resolved_value_type
|
33
|
+
@value_type = Attributor.resolve_type(value_type)
|
40
34
|
@value_attribute = Attribute.new(@value_type)
|
41
35
|
@concrete=true
|
42
36
|
end
|
@@ -113,6 +107,10 @@ module Attributor
|
|
113
107
|
end
|
114
108
|
end
|
115
109
|
|
110
|
+
def self.constructable?
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
116
114
|
|
117
115
|
def self.construct(constructor_block, **options)
|
118
116
|
return self if constructor_block.nil?
|
@@ -131,34 +129,64 @@ module Attributor
|
|
131
129
|
end
|
132
130
|
|
133
131
|
|
134
|
-
def self.
|
135
|
-
return self.new if (key_type == Object && value_type == Object)
|
136
|
-
|
132
|
+
def self.example_contents(context, parent, **values)
|
137
133
|
hash = ::Hash.new
|
134
|
+
example_depth = context.size
|
135
|
+
|
136
|
+
self.keys.each do |sub_attribute_name, sub_attribute|
|
137
|
+
if sub_attribute.attributes
|
138
|
+
# TODO: add option to raise an exception in this case?
|
139
|
+
next if example_depth > MAX_EXAMPLE_DEPTH
|
140
|
+
end
|
141
|
+
|
142
|
+
sub_context = self.generate_subcontext(context,sub_attribute_name)
|
143
|
+
block = Proc.new do
|
144
|
+
value = values.fetch(sub_attribute_name) do
|
145
|
+
sub_attribute.example(sub_context, parent: parent)
|
146
|
+
end
|
147
|
+
|
148
|
+
sub_attribute.load(value,sub_context)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
hash[sub_attribute_name] = block
|
153
|
+
end
|
154
|
+
|
155
|
+
hash
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.example(context=nil, **values)
|
159
|
+
if (key_type == Object && value_type == Object && self.keys.empty?)
|
160
|
+
return self.new
|
161
|
+
end
|
162
|
+
|
138
163
|
context ||= ["#{Hash}-#{rand(10000000)}"]
|
139
164
|
context = Array(context)
|
140
165
|
|
141
166
|
if self.keys.any?
|
142
|
-
self.
|
143
|
-
|
144
|
-
|
145
|
-
|
167
|
+
result = self.new
|
168
|
+
result.extend(ExampleMixin)
|
169
|
+
|
170
|
+
result.lazy_attributes = self.example_contents(context, result, values)
|
146
171
|
else
|
147
|
-
|
172
|
+
hash = ::Hash.new
|
148
173
|
|
149
|
-
|
174
|
+
(rand(3) + 1).times do |i|
|
150
175
|
example_key = key_type.example(context + ["at(#{i})"])
|
151
176
|
subcontext = context + ["at(#{example_key})"]
|
152
177
|
hash[example_key] = value_type.example(subcontext)
|
153
178
|
end
|
179
|
+
|
180
|
+
result = self.new(hash)
|
154
181
|
end
|
155
182
|
|
156
|
-
|
183
|
+
result
|
157
184
|
end
|
158
185
|
|
159
186
|
|
160
187
|
def self.dump(value, **opts)
|
161
|
-
self.load(value).
|
188
|
+
self.load(value).
|
189
|
+
dump(**opts)
|
162
190
|
end
|
163
191
|
|
164
192
|
|
@@ -182,6 +210,8 @@ module Attributor
|
|
182
210
|
|
183
211
|
|
184
212
|
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
|
213
|
+
context = Array(context)
|
214
|
+
|
185
215
|
if value.nil?
|
186
216
|
if recurse
|
187
217
|
loaded_value = {}
|
@@ -191,6 +221,10 @@ module Attributor
|
|
191
221
|
elsif value.is_a?(self)
|
192
222
|
return value
|
193
223
|
elsif value.kind_of?(Attributor::Hash)
|
224
|
+
if (value.keys - self.attributes.keys).any?
|
225
|
+
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
226
|
+
end
|
227
|
+
|
194
228
|
loaded_value = value.contents
|
195
229
|
elsif value.is_a?(::Hash)
|
196
230
|
loaded_value = value
|
@@ -219,8 +253,9 @@ module Attributor
|
|
219
253
|
|
220
254
|
def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
|
221
255
|
key = self.class.key_attribute.load(key, context)
|
222
|
-
value = @contents[key]
|
223
256
|
|
257
|
+
value = @contents[key]
|
258
|
+
|
224
259
|
if (attribute = self.class.keys[key])
|
225
260
|
return self[key] = attribute.load(value, context)
|
226
261
|
end
|
@@ -236,12 +271,10 @@ module Attributor
|
|
236
271
|
else
|
237
272
|
extra_keys_key = self.class.extra_keys
|
238
273
|
|
239
|
-
|
240
|
-
|
241
|
-
@contents[extra_keys_key] = extra_keys_value
|
274
|
+
if @contents.key? extra_keys_key
|
275
|
+
return @contents[extra_keys_key].get(key, context: context)
|
242
276
|
end
|
243
277
|
|
244
|
-
return @contents[extra_keys_key].get(key, context: context)
|
245
278
|
end
|
246
279
|
end
|
247
280
|
|
@@ -249,11 +282,11 @@ module Attributor
|
|
249
282
|
end
|
250
283
|
|
251
284
|
|
252
|
-
def set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
|
285
|
+
def set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key), recurse: false)
|
253
286
|
key = self.class.key_attribute.load(key, context)
|
254
287
|
|
255
288
|
if (attribute = self.class.keys[key])
|
256
|
-
return self[key] = attribute.load(value, context)
|
289
|
+
return self[key] = attribute.load(value, context, recurse: recurse)
|
257
290
|
end
|
258
291
|
|
259
292
|
if self.class.options[:case_insensitive_load]
|
@@ -289,14 +322,14 @@ module Attributor
|
|
289
322
|
if self.extra_keys
|
290
323
|
sub_context = self.generate_subcontext(context,self.extra_keys)
|
291
324
|
v = object.fetch(self.extra_keys, {})
|
292
|
-
hash.set(self.extra_keys, v, context: sub_context)
|
325
|
+
hash.set(self.extra_keys, v, context: sub_context, recurse: recurse)
|
293
326
|
end
|
294
327
|
|
295
328
|
object.each do |k,v|
|
296
329
|
next if k == self.extra_keys
|
297
330
|
|
298
331
|
sub_context = self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,k)
|
299
|
-
hash.set(k, v, context: sub_context)
|
332
|
+
hash.set(k, v, context: sub_context, recurse: recurse)
|
300
333
|
end
|
301
334
|
|
302
335
|
# handle default values for missing keys
|
@@ -342,13 +375,26 @@ module Attributor
|
|
342
375
|
hash
|
343
376
|
end
|
344
377
|
|
345
|
-
# TODO:
|
346
|
-
# TODO: add a validate, which simply validates that the incoming keys and values are of the right type.
|
347
|
-
# Think about the format of the subcontexts to use: let's use .at(key.to_s)
|
378
|
+
# TODO: Think about the format of the subcontexts to use: let's use .at(key.to_s)
|
348
379
|
attr_reader :contents
|
349
|
-
|
380
|
+
|
381
|
+
def_delegators :@contents,
|
382
|
+
:[],
|
383
|
+
:[]=,
|
384
|
+
:each,
|
385
|
+
:size,
|
386
|
+
:keys,
|
387
|
+
:key?,
|
388
|
+
:values,
|
389
|
+
:empty?,
|
390
|
+
:has_key?
|
391
|
+
|
392
|
+
attr_reader :validating, :dumping
|
350
393
|
|
351
394
|
def initialize(contents={})
|
395
|
+
@validating = false
|
396
|
+
@dumping = false
|
397
|
+
|
352
398
|
@contents = contents
|
353
399
|
end
|
354
400
|
|
@@ -360,7 +406,6 @@ module Attributor
|
|
360
406
|
self.class.value_type
|
361
407
|
end
|
362
408
|
|
363
|
-
|
364
409
|
def key_attribute
|
365
410
|
self.class.key_attribute
|
366
411
|
end
|
@@ -412,10 +457,15 @@ module Attributor
|
|
412
457
|
end
|
413
458
|
end
|
414
459
|
|
460
|
+
|
415
461
|
def dump(**opts)
|
462
|
+
return CIRCULAR_REFERENCE_MARKER if @dumping
|
463
|
+
|
464
|
+
@dumping = true
|
465
|
+
|
416
466
|
@contents.each_with_object({}) do |(k,v),hash|
|
417
467
|
k = self.key_attribute.dump(k,opts)
|
418
|
-
|
468
|
+
|
419
469
|
if (attribute_for_value = self.class.keys[k])
|
420
470
|
v = attribute_for_value.dump(v,opts)
|
421
471
|
else
|
@@ -424,6 +474,8 @@ module Attributor
|
|
424
474
|
|
425
475
|
hash[k] = v
|
426
476
|
end
|
477
|
+
ensure
|
478
|
+
@dumping = false
|
427
479
|
end
|
428
480
|
|
429
481
|
end
|
@@ -1,27 +1,43 @@
|
|
1
1
|
module Attributor
|
2
|
-
class Model
|
3
|
-
include Attributor::Type
|
4
|
-
MAX_EXAMPLE_DEPTH = 5
|
5
|
-
CIRCULAR_REFERENCE_MARKER = '...'.freeze
|
2
|
+
class Model < Hash
|
6
3
|
|
7
4
|
# FIXME: this is not the way to fix this. Really we should add finalize! to Models.
|
8
5
|
undef :timeout
|
9
6
|
undef :format
|
10
7
|
undef :test rescue nil
|
11
8
|
|
9
|
+
# Remove undesired methods inherited from Hash
|
10
|
+
undef :size
|
11
|
+
undef :keys
|
12
|
+
undef :values
|
13
|
+
undef :empty?
|
14
|
+
undef :has_key?
|
15
|
+
|
16
|
+
@key_type = Symbol
|
17
|
+
@value_type = Object
|
18
|
+
|
19
|
+
@key_attribute = Attribute.new(@key_type)
|
20
|
+
@value_attribute = Attribute.new(@value_type)
|
21
|
+
|
12
22
|
def self.inherited(klass)
|
23
|
+
k = self.key_type
|
24
|
+
ka = self.key_attribute
|
25
|
+
|
26
|
+
v = self.value_type
|
27
|
+
va = self.value_attribute
|
28
|
+
|
13
29
|
klass.instance_eval do
|
14
30
|
@saved_blocks = []
|
15
31
|
@options = {}
|
16
|
-
@
|
17
|
-
|
18
|
-
|
32
|
+
@keys = {}
|
33
|
+
@key_type = k
|
34
|
+
@value_type = v
|
19
35
|
|
20
|
-
|
21
|
-
|
36
|
+
@key_attribute = ka
|
37
|
+
@value_attribute = va
|
38
|
+
end
|
22
39
|
end
|
23
40
|
|
24
|
-
|
25
41
|
# Define accessors for attribute of given name.
|
26
42
|
#
|
27
43
|
# @param name [::Symbol] attribute name
|
@@ -32,98 +48,30 @@ module Attributor
|
|
32
48
|
self.define_writer(name)
|
33
49
|
end
|
34
50
|
|
35
|
-
|
36
51
|
def self.define_reader(name)
|
37
52
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
38
53
|
def #{name}
|
39
|
-
|
40
|
-
|
41
|
-
@attributes[:#{name}] = begin
|
42
|
-
if (proc = @lazy_attributes.delete :#{name})
|
43
|
-
if proc.arity > 0
|
44
|
-
proc.call(self)
|
45
|
-
else
|
46
|
-
proc.call
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
54
|
+
@contents[:#{name}]
|
50
55
|
end
|
51
56
|
RUBY
|
52
57
|
end
|
53
58
|
|
54
59
|
|
55
60
|
def self.define_writer(name)
|
56
|
-
attribute = self.attributes[name]
|
57
61
|
context = ["assignment","of(#{name})"].freeze
|
58
|
-
# note: paradoxically, using define_method ends up being faster for the writer
|
59
|
-
# attribute is captured by the block, saving us from having to retrieve it from
|
60
|
-
# the class's attributes hash on each write.
|
61
62
|
module_eval do
|
62
63
|
define_method(name.to_s + "=") do |value|
|
63
|
-
|
64
|
-
# => for now this would report "assignment.of(field_name)" is that good?
|
65
|
-
@attributes[name] = attribute.load(value,context)
|
64
|
+
self.set(name, value, context: context)
|
66
65
|
end
|
67
66
|
end
|
68
67
|
end
|
69
68
|
|
70
|
-
|
71
69
|
def self.describe(shallow=false)
|
72
70
|
hash = super
|
73
|
-
|
74
|
-
# Spit attributes if it's the root or if it's an anonymous structures
|
75
|
-
if ( !shallow || self.name == nil) && self.attributes
|
76
|
-
hash[:attributes] = self.attributes.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
|
77
|
-
sub_attributes[sub_name] = sub_attribute.describe(true)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
71
|
+
hash[:attributes] = hash.delete :keys
|
81
72
|
hash
|
82
73
|
end
|
83
74
|
|
84
|
-
|
85
|
-
def self.example(context=nil, **values)
|
86
|
-
result = self.new
|
87
|
-
|
88
|
-
context = case context
|
89
|
-
when nil
|
90
|
-
["#{self.name}-#{result.object_id.to_s}"]
|
91
|
-
when ::String
|
92
|
-
[context]
|
93
|
-
else
|
94
|
-
context
|
95
|
-
end
|
96
|
-
|
97
|
-
example_depth = context.size
|
98
|
-
|
99
|
-
self.attributes.each do |sub_attribute_name,sub_attribute|
|
100
|
-
if sub_attribute.attributes
|
101
|
-
# TODO: add option to raise an exception in this case?
|
102
|
-
next if example_depth > MAX_EXAMPLE_DEPTH
|
103
|
-
end
|
104
|
-
|
105
|
-
sub_context = self.generate_subcontext(context,sub_attribute_name)
|
106
|
-
|
107
|
-
result.lazy_attributes[sub_attribute_name] = Proc.new do
|
108
|
-
value = values.fetch(sub_attribute_name) do
|
109
|
-
sub_attribute.example(sub_context, parent: result)
|
110
|
-
end
|
111
|
-
|
112
|
-
sub_attribute.load(value,sub_context)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
result
|
117
|
-
end
|
118
|
-
|
119
|
-
def self.dump(value, **opts)
|
120
|
-
self.load(value).dump(**opts)
|
121
|
-
end
|
122
|
-
|
123
|
-
def self.native_type
|
124
|
-
self
|
125
|
-
end
|
126
|
-
|
127
75
|
def self.check_option!(name, value)
|
128
76
|
case name
|
129
77
|
when :identity
|
@@ -140,115 +88,16 @@ module Attributor
|
|
140
88
|
end
|
141
89
|
end
|
142
90
|
|
143
|
-
|
144
|
-
|
145
|
-
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
|
146
|
-
if value.nil?
|
147
|
-
if recurse
|
148
|
-
value = {}
|
149
|
-
else
|
150
|
-
return value
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
return value if value.kind_of?(self.native_type)
|
155
|
-
|
156
|
-
context = Array(context)
|
157
|
-
|
158
|
-
hash = case value
|
159
|
-
when ::String
|
160
|
-
# Strings are assumed to be JSON-serialized for now.
|
161
|
-
begin
|
162
|
-
JSON.parse(value)
|
163
|
-
rescue
|
164
|
-
raise DeserializationError, context: context, from: value.class, encoding: "JSON" , value: value
|
165
|
-
end
|
166
|
-
when ::Hash
|
167
|
-
value
|
168
|
-
else
|
169
|
-
raise IncompatibleTypeError, context: context, value_type: value.class, type: self
|
170
|
-
end
|
171
|
-
|
172
|
-
self.from_hash(hash,context, recurse: recurse)
|
173
|
-
end
|
174
|
-
|
175
|
-
|
176
|
-
def self.from_hash(hash,context, recurse: false)
|
177
|
-
model = self.new
|
178
|
-
|
179
|
-
self.attributes.each do |attribute_name, attribute|
|
180
|
-
# p [attribute_name, attribute]
|
181
|
-
# OPTIMIZE: deleting the keys as we go is mucho faster, but also very risky
|
182
|
-
# Note: use "load" vs. attribute assignment so we can propagate the right context down.
|
183
|
-
sub_context = self.generate_subcontext(context,attribute_name)
|
184
|
-
model.attributes[attribute_name] = attribute.load(hash[attribute_name] || hash[attribute_name.to_s], sub_context, recurse: recurse)
|
185
|
-
end
|
186
|
-
|
187
|
-
unknown_keys = hash.keys.collect {|k| k.to_sym} - self.attributes.keys
|
188
|
-
|
189
|
-
if unknown_keys.any?
|
190
|
-
raise AttributorException, "Unknown attributes received: #{unknown_keys.inspect} while loading #{Attributor.humanize_context(context)}"
|
191
|
-
end
|
192
|
-
|
193
|
-
model
|
194
|
-
end
|
195
|
-
|
196
|
-
# method to only define the block of attributes for the model
|
197
|
-
# This will be a lazy definition. So we'll only save it in an instance class var for later.
|
198
|
-
def self.attributes(opts={},&block)
|
199
|
-
if block_given?
|
200
|
-
@saved_blocks.push(block)
|
201
|
-
@options.merge!(opts)
|
202
|
-
elsif @saved_blocks.any?
|
203
|
-
self.definition
|
204
|
-
end
|
205
|
-
|
206
|
-
@attributes
|
207
|
-
end
|
208
|
-
|
209
|
-
|
210
|
-
def self.validate(object,context=Attributor::DEFAULT_ROOT_CONTEXT,_attribute)
|
211
|
-
context = [context] if context.is_a? ::String
|
212
|
-
|
213
|
-
unless object.kind_of?(self)
|
214
|
-
raise ArgumentError, "#{self.name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
|
215
|
-
end
|
216
|
-
|
217
|
-
object.validate(context)
|
218
|
-
end
|
219
|
-
|
220
|
-
def self.valid_type?(type)
|
221
|
-
type.kind_of?(self) || type.kind_of?(::Hash)
|
222
|
-
end
|
223
|
-
|
224
|
-
def self.dsl_class
|
225
|
-
@options[:dsl_compiler] || DSLCompiler
|
226
|
-
end
|
227
|
-
|
228
|
-
# Returns the "compiled" definition for the model.
|
229
|
-
# By "compiled" I mean that it will create a new Compiler object with the saved options and saved block that has been passed in the 'attributes' method. This compiled object is memoized (remember, there's one instance of a compiled definition PER MODEL CLASS).
|
230
|
-
# TODO: rework this with Model.finalize! support.
|
231
|
-
def self.definition
|
232
|
-
blocks = @saved_blocks.shift(@saved_blocks.size)
|
233
|
-
|
234
|
-
compiler = dsl_class.new(self, self.options)
|
235
|
-
compiler.parse(*blocks)
|
236
|
-
|
237
|
-
nil
|
91
|
+
def self.generate_subcontext(context, subname)
|
92
|
+
context + [subname]
|
238
93
|
end
|
239
94
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
def initialize( data = nil)
|
244
|
-
@lazy_attributes = ::Hash.new
|
245
|
-
@validating = false
|
246
|
-
@dumping = false
|
95
|
+
def initialize(data = nil)
|
247
96
|
if data
|
248
97
|
loaded = self.class.load(data)
|
249
|
-
@
|
98
|
+
@contents = loaded.attributes
|
250
99
|
else
|
251
|
-
@
|
100
|
+
@contents = {}
|
252
101
|
end
|
253
102
|
end
|
254
103
|
|
@@ -279,10 +128,7 @@ module Attributor
|
|
279
128
|
|
280
129
|
|
281
130
|
def attributes
|
282
|
-
@
|
283
|
-
self.__send__(name)
|
284
|
-
end
|
285
|
-
@attributes
|
131
|
+
@contents
|
286
132
|
end
|
287
133
|
|
288
134
|
|
@@ -311,12 +157,10 @@ module Attributor
|
|
311
157
|
|
312
158
|
def dump(context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
|
313
159
|
return CIRCULAR_REFERENCE_MARKER if @dumping
|
314
|
-
|
315
160
|
@dumping = true
|
316
161
|
|
317
162
|
self.attributes.each_with_object({}) do |(name, value), result|
|
318
163
|
attribute = self.class.attributes[name]
|
319
|
-
|
320
164
|
result[name.to_sym] = attribute.dump(value, context: context + [name] )
|
321
165
|
end
|
322
166
|
ensure
|
@@ -324,4 +168,5 @@ module Attributor
|
|
324
168
|
end
|
325
169
|
|
326
170
|
end
|
171
|
+
|
327
172
|
end
|
@@ -6,8 +6,11 @@ module Attributor
|
|
6
6
|
return ::String
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
9
|
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
10
|
+
if value.kind_of?(Enumerable)
|
11
|
+
raise IncompatibleTypeError, context: context, value_type: value.class, type: self
|
12
|
+
end
|
13
|
+
|
11
14
|
value && String(value)
|
12
15
|
rescue
|
13
16
|
super
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Attributor
|
2
|
+
class Symbol
|
3
|
+
include Type
|
4
|
+
|
5
|
+
def self.native_type
|
6
|
+
return ::Symbol
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
11
|
+
value.to_sym
|
12
|
+
rescue
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.example(context=nil, options:{})
|
17
|
+
:example
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/lib/attributor/version.rb
CHANGED
data/spec/dsl_compiler_spec.rb
CHANGED
@@ -110,10 +110,10 @@ describe Attributor::DSLCompiler do
|
|
110
110
|
attribute_options.merge(:reference => reference_type)
|
111
111
|
end
|
112
112
|
|
113
|
-
it '
|
114
|
-
|
115
|
-
|
116
|
-
|
113
|
+
it 'sets the type of the attribute to Struct' do
|
114
|
+
Attributor::Attribute.should_receive(:new).
|
115
|
+
with(expected_type, {:description=>"The turkey", :reference=>Turkey})
|
116
|
+
dsl_compiler.attribute(attribute_name, attribute_options, &attribute_block)
|
117
117
|
end
|
118
118
|
|
119
119
|
it 'passes the correct reference to the created attribute' do
|
data/spec/support/hashes.rb
CHANGED
data/spec/support/models.rb
CHANGED
@@ -147,16 +147,14 @@ describe Attributor::Collection do
|
|
147
147
|
end
|
148
148
|
|
149
149
|
context 'for invalid values' do
|
150
|
-
{
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
it "raises error when member_type is #{member_type} and value is #{value.inspect}" do
|
157
|
-
expect { type.of(member_type).load(value).should == value }.to raise_error(Attributor::AttributorException)
|
158
|
-
end
|
150
|
+
let(:member_type) { ::Chicken }
|
151
|
+
let(:value) { [::Turducken.example] }
|
152
|
+
it "raises error when incoming value is not of member_type" do
|
153
|
+
expect {
|
154
|
+
val = type.of(member_type).load(value)
|
155
|
+
}.to raise_error(Attributor::AttributorException)
|
159
156
|
end
|
157
|
+
|
160
158
|
end
|
161
159
|
end
|
162
160
|
|
data/spec/types/model_spec.rb
CHANGED
@@ -66,7 +66,7 @@ describe Attributor::Model do
|
|
66
66
|
context 'with infinitely-expanding sub-attributes' do
|
67
67
|
let(:model_class) do
|
68
68
|
Class.new(Attributor::Model) do
|
69
|
-
this = self
|
69
|
+
this = self
|
70
70
|
attributes do
|
71
71
|
attribute :name, String
|
72
72
|
attribute :child, this
|
@@ -167,6 +167,15 @@ describe Attributor::Model do
|
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
170
|
+
context 'with an instance of different model' do
|
171
|
+
it 'raises some sort of error' do
|
172
|
+
expect {
|
173
|
+
turducken = Turducken.example
|
174
|
+
chicken = Chicken.load(turducken,context)
|
175
|
+
}.to raise_error(Attributor::IncompatibleTypeError, /Type Chicken cannot load values of type Turducken.*#{context.join('.')}/)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
170
179
|
context "with a hash" do
|
171
180
|
context 'for a complete set of attributes' do
|
172
181
|
it 'loads the given attributes' do
|
@@ -190,13 +199,12 @@ describe Attributor::Model do
|
|
190
199
|
it 'raises an error' do
|
191
200
|
expect {
|
192
201
|
Chicken.load(hash, context)
|
193
|
-
}.to raise_error(Attributor::AttributorException, /Unknown
|
202
|
+
}.to raise_error(Attributor::AttributorException, /Unknown key received/)
|
203
|
+
#raise_error(Attributor::AttributorException, /Unknown attributes.*#{context.join('.')}/)
|
194
204
|
end
|
195
205
|
end
|
196
206
|
end
|
197
207
|
|
198
|
-
|
199
|
-
|
200
208
|
end
|
201
209
|
|
202
210
|
end
|
@@ -213,7 +221,7 @@ describe Attributor::Model do
|
|
213
221
|
|
214
222
|
context 'initialize' do
|
215
223
|
|
216
|
-
subject(:chicken) { Chicken.new(
|
224
|
+
subject(:chicken) { Chicken.new(attributes_data) }
|
217
225
|
context 'supports passing an initial hash object for attribute values' do
|
218
226
|
let(:attributes_data){ {age: '1', email:'rooster@coup.com'} }
|
219
227
|
it 'and sets them in loaded format onto the instance attributes' do
|
@@ -228,23 +236,23 @@ describe Attributor::Model do
|
|
228
236
|
context 'supports passing a JSON encoded data object' do
|
229
237
|
let(:attributes_hash){ {age: 1, email:'rooster@coup.com'} }
|
230
238
|
let(:attributes_data){ JSON.dump(attributes_hash) }
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
end
|
236
|
-
subject.age.should be(1)
|
237
|
-
subject.email.should == attributes_hash[:email]
|
239
|
+
it 'and sets them in loaded format onto the instance attributes' do
|
240
|
+
Chicken.should_receive(:load).with(attributes_data).and_call_original
|
241
|
+
attributes_hash.keys.each do |attr_name|
|
242
|
+
Chicken.attributes[attr_name].should_receive(:load).with(attributes_hash[attr_name],instance_of(Array), recurse: false).and_call_original
|
238
243
|
end
|
244
|
+
subject.age.should be(1)
|
245
|
+
subject.email.should == attributes_hash[:email]
|
239
246
|
end
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
247
|
+
end
|
248
|
+
context 'supports passing a native model for the data object' do
|
249
|
+
let(:attributes_data){ Chicken.example }
|
250
|
+
it 'sets a new instance pointing to the exact same attributes (careful about modifications!)' do
|
251
|
+
attributes_data.attributes.each do |attr_name, attr_value|
|
252
|
+
subject.send(attr_name).should be(attr_value)
|
246
253
|
end
|
247
254
|
end
|
255
|
+
end
|
248
256
|
end
|
249
257
|
|
250
258
|
context 'getting and setting attributes' do
|
data/spec/types/string_spec.rb
CHANGED
@@ -56,4 +56,13 @@ describe Attributor::String do
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
+
context 'for Enumerable values' do
|
60
|
+
let(:value) { [1] }
|
61
|
+
|
62
|
+
it 'raises IncompatibleTypeError' do
|
63
|
+
expect {
|
64
|
+
type.load(value)
|
65
|
+
}.to raise_error(Attributor::IncompatibleTypeError)
|
66
|
+
end
|
67
|
+
end
|
59
68
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attributor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -257,6 +257,7 @@ files:
|
|
257
257
|
- lib/attributor/attribute.rb
|
258
258
|
- lib/attributor/attribute_resolver.rb
|
259
259
|
- lib/attributor/dsl_compiler.rb
|
260
|
+
- lib/attributor/example_mixin.rb
|
260
261
|
- lib/attributor/exceptions.rb
|
261
262
|
- lib/attributor/extensions/randexp.rb
|
262
263
|
- lib/attributor/type.rb
|
@@ -276,6 +277,7 @@ files:
|
|
276
277
|
- lib/attributor/types/object.rb
|
277
278
|
- lib/attributor/types/string.rb
|
278
279
|
- lib/attributor/types/struct.rb
|
280
|
+
- lib/attributor/types/symbol.rb
|
279
281
|
- lib/attributor/types/tempfile.rb
|
280
282
|
- lib/attributor/types/time.rb
|
281
283
|
- lib/attributor/version.rb
|