attributor 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eaf11f67f19da1559c3abdbcebed02193ac9e5e3
4
- data.tar.gz: 6ba6a1dbf45d4e2d43450036651f0d4c0fde9829
3
+ metadata.gz: 6e832c8023013ffad5fa5a40e13200e5dcfeb4c4
4
+ data.tar.gz: a1d2a7043187872b6cdc424b503b8ce08a5935ab
5
5
  SHA512:
6
- metadata.gz: 410e51a836135f981bee0cc40aabb254c526ade79aafa224a96881dac78b8c1986ff088b33f89e80da8a0899ebc086a9ff589768130adf78ca3cb84ae7a8b350
7
- data.tar.gz: 22d78b19144db5a3c19b778a6c9c32e66ce819f86d3a71dc443462ab0f4a8911345cefe193995b43897c8fd30224de0a67a7e000fa79a5b18a66586e70adbb67
6
+ metadata.gz: 26f1bd976df8c4ad47ea3e617e2abdcabd7d6d070a82dce44abb2e16ededcae6289ca7e074ba503f9ee7f6f4e79036bb49078509c2812f63ca02d3eab190466e
7
+ data.tar.gz: ee7b663e6d9095d2909f355cf0f78e76d0cc5e6c8c0a2b0f44338fed5776a9a36360aba0b81f9a2b7ac532b88c2b8e7c8168187503405ed1b8cff1a82fc36cbd
@@ -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
 
@@ -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.respond_to?(:construct)
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
- raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
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/model'
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
- else
93
- inherited_attribute = nil
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 inherited_attribute
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
@@ -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?
@@ -90,6 +90,11 @@ module Attributor
90
90
  hash
91
91
  end
92
92
 
93
+
94
+ def self.constructable?
95
+ true
96
+ end
97
+
93
98
  def self.construct(constructor_block, options)
94
99
 
95
100
  member_options = (options[:member_options] || {} ).clone
@@ -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
- @key_type = resolved_key_type
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
- resolved_value_type = Attributor.resolve_type(value_type)
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.example(context=nil, options: {})
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.keys.each do |sub_name, sub_attribute|
143
- subcontext = context + ["at(#{sub_name})"]
144
- hash[sub_name] = sub_attribute.example(subcontext)
145
- end
167
+ result = self.new
168
+ result.extend(ExampleMixin)
169
+
170
+ result.lazy_attributes = self.example_contents(context, result, values)
146
171
  else
147
- size = rand(3) + 1
172
+ hash = ::Hash.new
148
173
 
149
- size.times do |i|
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
- self.new(hash)
183
+ result
157
184
  end
158
185
 
159
186
 
160
187
  def self.dump(value, **opts)
161
- self.load(value).dump(**opts)
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
- unless @contents.key? extra_keys_key
240
- extra_keys_value = self.class.keys[extra_keys_key].load({})
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: chance value_type and key_type to be attributes?
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
- def_delegators :@contents, :[], :[]=, :each, :size, :keys, :key?, :values, :empty?, :has_key?
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
- @attributes = {}
17
- end
18
- end
32
+ @keys = {}
33
+ @key_type = k
34
+ @value_type = v
19
35
 
20
- class << self
21
- attr_reader :options
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
- return @attributes[:#{name}] if @attributes.has_key?(:#{name})
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
- # TODO: what type of context do we report with unscoped assignments?
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
- # Model-specific decoding and coercion of the attribute.
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
- attr_reader :lazy_attributes, :validating, :dumping
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
- @attributes = loaded.attributes
98
+ @contents = loaded.attributes
250
99
  else
251
- @attributes = ::Hash.new
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
- @lazy_attributes.keys.each do |name|
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
@@ -2,6 +2,9 @@
2
2
  module Attributor
3
3
  class Struct < Attributor::Model
4
4
 
5
+ def self.constructable?
6
+ true
7
+ end
5
8
 
6
9
  # Construct a new subclass, using attribute_definition to define attributes.
7
10
  def self.construct(attribute_definition, options={})
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = "2.3.0"
2
+ VERSION = "2.4.0"
3
3
  end
@@ -110,10 +110,10 @@ describe Attributor::DSLCompiler do
110
110
  attribute_options.merge(:reference => reference_type)
111
111
  end
112
112
 
113
- it 'is unhappy from somewhere else if you do not specify a type' do
114
- expect {
115
- dsl_compiler.attribute(attribute_name, attribute_options, &attribute_block)
116
- }.to raise_error(/does not support anonymous generation/)
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
@@ -5,3 +5,10 @@ class HashWithModel < Attributor::Hash
5
5
  end
6
6
  end
7
7
 
8
+
9
+ class HashWithStrings < Attributor::Hash
10
+ keys do
11
+ key :name, String
12
+ key :something, String
13
+ end
14
+ end
@@ -42,6 +42,7 @@ end
42
42
 
43
43
 
44
44
  # http://en.wikipedia.org/wiki/Cormorant
45
+
45
46
  class Cormorant < Attributor::Model
46
47
  attributes do
47
48
  # This will be a collection of arbitrary Ruby Objects
@@ -147,16 +147,14 @@ describe Attributor::Collection do
147
147
  end
148
148
 
149
149
  context 'for invalid values' do
150
- {
151
- # FIXME: https://github.com/rightscale/attributor/issues/24
152
- #::String => ["foo", "bar"],
153
- #::Object => [::Object.new]
154
- ::Chicken => [::Turkey.new]
155
- }.each do |member_type, value|
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
 
@@ -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 attributes.*#{context.join('.')}/)
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( attributes_data ) }
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
- it 'and sets them in loaded format onto the instance attributes' do
232
- Chicken.should_receive(:load).with(attributes_data).and_call_original
233
- attributes_hash.keys.each do |attr_name|
234
- Chicken.attributes[attr_name].should_receive(:load).with(attributes_hash[attr_name],instance_of(Array), recurse: false).and_call_original
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
- context 'supports passing a native model for the data object' do
241
- let(:attributes_data){ Chicken.example }
242
- it 'sets a new instance pointing to the exact same attributes (careful about modifications!)' do
243
- attributes_data.attributes.each do |attr_name, attr_value|
244
- subject.send(attr_name).should be(attr_value)
245
- end
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
@@ -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.3.0
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