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 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