attributor 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -0
  3. data/.travis.yml +6 -4
  4. data/CHANGELOG.md +6 -1
  5. data/Gemfile +1 -1
  6. data/Guardfile +14 -8
  7. data/Rakefile +4 -5
  8. data/attributor.gemspec +34 -29
  9. data/lib/attributor.rb +23 -29
  10. data/lib/attributor/attribute.rb +108 -127
  11. data/lib/attributor/attribute_resolver.rb +12 -26
  12. data/lib/attributor/dsl_compiler.rb +17 -21
  13. data/lib/attributor/dumpable.rb +1 -2
  14. data/lib/attributor/example_mixin.rb +5 -8
  15. data/lib/attributor/exceptions.rb +5 -6
  16. data/lib/attributor/extensions/randexp.rb +3 -5
  17. data/lib/attributor/extras/field_selector.rb +4 -4
  18. data/lib/attributor/extras/field_selector/transformer.rb +6 -7
  19. data/lib/attributor/families/numeric.rb +0 -2
  20. data/lib/attributor/families/temporal.rb +1 -4
  21. data/lib/attributor/hash_dsl_compiler.rb +22 -25
  22. data/lib/attributor/type.rb +24 -32
  23. data/lib/attributor/types/bigdecimal.rb +7 -14
  24. data/lib/attributor/types/boolean.rb +5 -8
  25. data/lib/attributor/types/class.rb +9 -10
  26. data/lib/attributor/types/collection.rb +34 -44
  27. data/lib/attributor/types/container.rb +9 -15
  28. data/lib/attributor/types/csv.rb +7 -10
  29. data/lib/attributor/types/date.rb +20 -25
  30. data/lib/attributor/types/date_time.rb +7 -14
  31. data/lib/attributor/types/float.rb +4 -6
  32. data/lib/attributor/types/hash.rb +171 -196
  33. data/lib/attributor/types/ids.rb +2 -6
  34. data/lib/attributor/types/integer.rb +12 -17
  35. data/lib/attributor/types/model.rb +39 -48
  36. data/lib/attributor/types/object.rb +2 -4
  37. data/lib/attributor/types/polymorphic.rb +118 -0
  38. data/lib/attributor/types/regexp.rb +4 -5
  39. data/lib/attributor/types/string.rb +6 -7
  40. data/lib/attributor/types/struct.rb +8 -15
  41. data/lib/attributor/types/symbol.rb +3 -6
  42. data/lib/attributor/types/tempfile.rb +5 -6
  43. data/lib/attributor/types/time.rb +11 -11
  44. data/lib/attributor/types/uri.rb +9 -10
  45. data/lib/attributor/version.rb +1 -1
  46. data/spec/attribute_resolver_spec.rb +57 -78
  47. data/spec/attribute_spec.rb +174 -216
  48. data/spec/attributor_spec.rb +11 -15
  49. data/spec/dsl_compiler_spec.rb +19 -33
  50. data/spec/dumpable_spec.rb +6 -7
  51. data/spec/extras/field_selector/field_selector_spec.rb +1 -1
  52. data/spec/families_spec.rb +1 -3
  53. data/spec/hash_dsl_compiler_spec.rb +65 -74
  54. data/spec/spec_helper.rb +9 -3
  55. data/spec/support/hashes.rb +2 -3
  56. data/spec/support/models.rb +30 -36
  57. data/spec/support/polymorphics.rb +10 -0
  58. data/spec/type_spec.rb +38 -61
  59. data/spec/types/bigdecimal_spec.rb +11 -15
  60. data/spec/types/boolean_spec.rb +12 -39
  61. data/spec/types/class_spec.rb +10 -11
  62. data/spec/types/collection_spec.rb +72 -81
  63. data/spec/types/container_spec.rb +22 -26
  64. data/spec/types/csv_spec.rb +15 -16
  65. data/spec/types/date_spec.rb +16 -33
  66. data/spec/types/date_time_spec.rb +16 -33
  67. data/spec/types/file_upload_spec.rb +1 -2
  68. data/spec/types/float_spec.rb +7 -14
  69. data/spec/types/hash_spec.rb +285 -289
  70. data/spec/types/ids_spec.rb +5 -7
  71. data/spec/types/integer_spec.rb +37 -46
  72. data/spec/types/model_spec.rb +111 -128
  73. data/spec/types/polymorphic_spec.rb +134 -0
  74. data/spec/types/regexp_spec.rb +4 -7
  75. data/spec/types/string_spec.rb +17 -21
  76. data/spec/types/struct_spec.rb +40 -47
  77. data/spec/types/tempfile_spec.rb +1 -2
  78. data/spec/types/temporal_spec.rb +9 -0
  79. data/spec/types/time_spec.rb +16 -32
  80. data/spec/types/type_spec.rb +15 -0
  81. data/spec/types/uri_spec.rb +6 -7
  82. metadata +77 -25
@@ -9,9 +9,8 @@ module Attributor
9
9
  end
10
10
 
11
11
  module ClassMethods
12
-
13
- def decode_string(value, context=Attributor::DEFAULT_ROOT_CONTEXT)
14
- raise "#{self.name}.decode_string is not implemented. (when decoding #{Attributor.humanize_context(context)})"
12
+ def decode_string(_value, context = Attributor::DEFAULT_ROOT_CONTEXT)
13
+ raise "#{name}.decode_string is not implemented. (when decoding #{Attributor.humanize_context(context)})"
15
14
  end
16
15
 
17
16
  # Decode JSON string that encapsulates an array
@@ -19,24 +18,19 @@ module Attributor
19
18
  # @param value [String] JSON string
20
19
  # @return [Array] a normal Ruby Array
21
20
  #
22
- def decode_json(value, context=Attributor::DEFAULT_ROOT_CONTEXT)
23
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: "JSON" , value: value unless value.kind_of? ::String
21
+ def decode_json(value, context = Attributor::DEFAULT_ROOT_CONTEXT)
22
+ raise Attributor::DeserializationError, context: context, from: value.class, encoding: 'JSON', value: value unless value.is_a? ::String
24
23
 
25
24
  # attempt to parse as JSON
26
25
  parsed_value = JSON.parse(value)
27
-
28
- if self.valid_type?(parsed_value)
29
- value = parsed_value
30
- else
31
- raise Attributor::CoercionError, context: context, from: parsed_value.class, to: self.name, value: parsed_value
26
+ unless valid_type?(parsed_value)
27
+ raise Attributor::CoercionError, context: context, from: parsed_value.class, to: name, value: parsed_value
32
28
  end
33
- return value
34
29
 
35
- rescue JSON::JSONError => e
36
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: "JSON" , value: value
30
+ parsed_value
31
+ rescue JSON::JSONError
32
+ raise Attributor::DeserializationError, context: context, from: value.class, encoding: 'JSON', value: value
37
33
  end
38
-
39
34
  end
40
-
41
35
  end
42
36
  end
@@ -1,8 +1,6 @@
1
1
  module Attributor
2
-
3
2
  class CSV < Collection
4
-
5
- def self.decode_string(value,context)
3
+ def self.decode_string(value, _context)
6
4
  value.split(',')
7
5
  end
8
6
 
@@ -11,25 +9,25 @@ module Attributor
11
9
  when ::String
12
10
  values
13
11
  when ::Array
14
- values.collect { |value| member_attribute.dump(value,opts).to_s }.join(',')
12
+ values.collect { |value| member_attribute.dump(value, opts).to_s }.join(',')
15
13
  when nil
16
14
  nil
17
15
  else
18
16
  context = opts[:context] || DEFAULT_ROOT_CONTEXT
19
17
  name = context.last.to_s
20
18
  type = values.class.name
21
- reason = "Attributor::CSV only supports dumping values of type " +
19
+ reason = 'Attributor::CSV only supports dumping values of type ' \
22
20
  "Array or String, not #{values.class.name}."
23
- raise DumpError.new(context: context, name: name, type: type, original_exception: reason)
21
+ raise DumpError, context: context, name: name, type: type, original_exception: reason
24
22
  end
25
23
  end
26
24
 
27
- def self.example(context=nil, options: {})
25
+ def self.example(context = nil, options: {})
28
26
  collection = super(context, options: options.merge(size: (2..4)))
29
- return collection.join(',')
27
+ collection.join(',')
30
28
  end
31
29
 
32
- def self.describe(shallow=false, example: nil)
30
+ def self.describe(shallow = false, example: nil)
33
31
  hash = super(shallow)
34
32
  hash.delete(:member_attribute)
35
33
  hash[:example] = example if example
@@ -39,6 +37,5 @@ module Attributor
39
37
  def self.family
40
38
  Collection.family
41
39
  end
42
-
43
40
  end
44
41
  end
@@ -1,36 +1,31 @@
1
1
  require 'date'
2
2
 
3
3
  module Attributor
4
+ class Date < Temporal
5
+ def self.native_type
6
+ ::Date
7
+ end
4
8
 
5
- class Date < Temporal
6
-
7
- def self.native_type
8
- return ::Date
9
- end
9
+ def self.example(context = nil, options: {})
10
+ load(Randgen.date, context)
11
+ end
10
12
 
11
- def self.example(context=nil, options: {})
12
- return self.load(/[:date:]/.gen, context)
13
- end
13
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
14
+ return value if value.is_a?(native_type)
15
+ return nil if value.nil?
14
16
 
15
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
16
- return value if value.is_a?(self.native_type)
17
- return nil if value.nil?
18
-
19
- return value.to_date if value.respond_to?(:to_date)
17
+ return value.to_date if value.respond_to?(:to_date)
20
18
 
21
- case value
22
- when ::String
23
- begin
24
- return ::Date.parse(value)
25
- rescue ArgumentError => e
26
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: "Date" , value: value
27
- end
28
- else
29
- raise CoercionError, context: context, from: value.class, to: self, value: value
19
+ case value
20
+ when ::String
21
+ begin
22
+ return ::Date.parse(value)
23
+ rescue ArgumentError
24
+ raise Attributor::DeserializationError, context: context, from: value.class, encoding: 'Date', value: value
30
25
  end
26
+ else
27
+ raise CoercionError, context: context, from: value.class, to: self, value: value
31
28
  end
32
-
33
29
  end
34
-
35
30
  end
36
-
31
+ end
@@ -5,34 +5,27 @@ require_relative '../exceptions'
5
5
  require 'date'
6
6
 
7
7
  module Attributor
8
-
9
8
  class DateTime < Temporal
10
-
11
9
  def self.native_type
12
- return ::DateTime
10
+ ::DateTime
13
11
  end
14
12
 
15
- def self.example(context=nil, options: {})
16
- return self.load(/[:date:]/.gen, context)
13
+ def self.example(context = nil, options: {})
14
+ load(Randgen.date, context)
17
15
  end
18
16
 
19
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
17
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
20
18
  # We assume that if the value is already in the right type, we've decoded it already
21
- return value if value.is_a?(self.native_type)
19
+ return value if value.is_a?(native_type)
22
20
  return value.to_datetime if value.respond_to?(:to_datetime)
23
21
  return nil unless value.is_a?(::String)
24
22
  # TODO: we should be able to convert not only from String but Time...etc
25
23
  # Else, we'll decode it from String.
26
24
  begin
27
25
  return ::DateTime.parse(value)
28
- rescue ArgumentError => e
29
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: "DateTime" , value: value
26
+ rescue ArgumentError
27
+ raise Attributor::DeserializationError, context: context, from: value.class, encoding: 'DateTime', value: value
30
28
  end
31
29
  end
32
-
33
-
34
-
35
-
36
30
  end
37
-
38
31
  end
@@ -2,22 +2,21 @@
2
2
  # See: http://ruby-doc.org/core-2.1.0/Float.html
3
3
 
4
4
  module Attributor
5
-
6
5
  class Float
7
6
  include Type
8
7
 
9
8
  def self.native_type
10
- return ::Float
9
+ ::Float
11
10
  end
12
11
 
13
- def self.example(context=nil, options: {})
12
+ def self.example(_context = nil, options: {})
14
13
  min = options[:min].to_f || 0.0
15
14
  max = options[:max].to_f || Math.PI
16
15
 
17
- rand * (max-min) + min
16
+ rand * (max - min) + min
18
17
  end
19
18
 
20
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
19
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
21
20
  Float(value)
22
21
  rescue TypeError
23
22
  super
@@ -26,6 +25,5 @@ module Attributor
26
25
  def self.family
27
26
  'numeric'
28
27
  end
29
-
30
28
  end
31
29
  end
@@ -2,10 +2,10 @@ module Attributor
2
2
  class InvalidDefinition < StandardError
3
3
  def initialize(type, cause)
4
4
  type_name = if type.name
5
- type.name
6
- else
7
- type.inspect
8
- end
5
+ type.name
6
+ else
7
+ type.inspect
8
+ end
9
9
 
10
10
  msg = "Structure definition for type #{type_name} is invalid. The following exception has occurred: #{cause.inspect}"
11
11
  super(msg)
@@ -16,7 +16,6 @@ module Attributor
16
16
  end
17
17
 
18
18
  class Hash
19
-
20
19
  MAX_EXAMPLE_DEPTH = 5
21
20
  CIRCULAR_REFERENCE_MARKER = '...'.freeze
22
21
 
@@ -45,13 +44,13 @@ module Attributor
45
44
  def self.key_type=(key_type)
46
45
  @key_type = Attributor.resolve_type(key_type)
47
46
  @key_attribute = Attribute.new(@key_type)
48
- @concrete=true
47
+ @concrete = true
49
48
  end
50
49
 
51
50
  def self.value_type=(value_type)
52
51
  @value_type = Attributor.resolve_type(value_type)
53
52
  @value_attribute = Attribute.new(@value_type)
54
- @concrete=true
53
+ @concrete = true
55
54
  end
56
55
 
57
56
  def self.family
@@ -59,16 +58,16 @@ module Attributor
59
58
  end
60
59
 
61
60
  @saved_blocks = []
62
- @options = {allow_extra: false}
61
+ @options = { allow_extra: false }
63
62
  @keys = {}
64
63
 
65
64
  def self.inherited(klass)
66
- k = self.key_type
67
- v = self.value_type
65
+ k = key_type
66
+ v = value_type
68
67
 
69
68
  klass.instance_eval do
70
69
  @saved_blocks = []
71
- @options = {allow_extra: false}
70
+ @options = { allow_extra: false }
72
71
  @keys = {}
73
72
  @key_type = k
74
73
  @value_type = v
@@ -83,7 +82,7 @@ module Attributor
83
82
  def self.attributes(**options, &key_spec)
84
83
  raise @error if @error
85
84
 
86
- self.keys(options, &key_spec)
85
+ keys(options, &key_spec)
87
86
  end
88
87
 
89
88
  def self.keys(**options, &key_spec)
@@ -93,15 +92,15 @@ module Attributor
93
92
  @saved_blocks << key_spec
94
93
  @options.merge!(options)
95
94
  elsif @saved_blocks.any?
96
- self.definition
95
+ definition
97
96
  end
98
97
  @keys
99
98
  end
100
99
 
101
100
  def self.definition
102
101
  opts = {
103
- :key_type => @key_type,
104
- :value_type => @value_type
102
+ key_type: @key_type,
103
+ value_type: @value_type
105
104
  }.merge(@options)
106
105
 
107
106
  blocks = @saved_blocks.shift(@saved_blocks.size)
@@ -109,7 +108,7 @@ module Attributor
109
108
  compiler.parse(*blocks)
110
109
 
111
110
  if opts[:case_insensitive_load] == true
112
- @insensitive_map = self.keys.keys.each_with_object({}) do |k, map|
111
+ @insensitive_map = keys.keys.each_with_object({}) do |k, map|
113
112
  map[k.downcase] = k
114
113
  end
115
114
  end
@@ -127,7 +126,7 @@ module Attributor
127
126
  end
128
127
 
129
128
  def self.valid_type?(type)
130
- type.kind_of?(self) || type.kind_of?(::Hash)
129
+ type.is_a?(self) || type.is_a?(::Hash)
131
130
  end
132
131
 
133
132
  # @example Hash.of(key: String, value: Integer)
@@ -146,74 +145,64 @@ module Attributor
146
145
  def self.add_requirement(req)
147
146
  @requirements << req
148
147
  return unless req.attr_names
149
- non_existing = req.attr_names - self.attributes.keys
148
+ non_existing = req.attr_names - attributes.keys
150
149
  unless non_existing.empty?
151
- raise "Invalid attribute name(s) found (#{non_existing.join(', ')}) when defining a requirement of type #{req.type} for #{Attributor.type_name(self)} ." +
152
- "The only existing attributes are #{self.attributes.keys}"
150
+ raise "Invalid attribute name(s) found (#{non_existing.join(', ')}) when defining a requirement of type #{req.type} for #{Attributor.type_name(self)} ." \
151
+ "The only existing attributes are #{attributes.keys}"
153
152
  end
154
-
155
153
  end
156
154
 
157
155
  def self.construct(constructor_block, **options)
158
156
  return self if constructor_block.nil?
159
157
 
160
158
  unless @concrete
161
- return self.of(key:self.key_type, value: self.value_type)
162
- .construct(constructor_block,**options)
159
+ return of(key: key_type, value: value_type)
160
+ .construct(constructor_block, **options)
163
161
  end
164
162
 
165
- if options[:case_insensitive_load] && !(self.key_type <= String)
166
- raise Attributor::AttributorException.new(":case_insensitive_load may not be used with keys of type #{self.key_type.name}")
163
+ if options[:case_insensitive_load] && !(key_type <= String)
164
+ raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{key_type.name}"
167
165
  end
168
166
 
169
- self.keys(options, &constructor_block)
167
+ keys(options, &constructor_block)
170
168
  self
171
169
  end
172
170
 
173
-
174
171
  def self.example_contents(context, parent, **values)
175
-
176
172
  hash = ::Hash.new
177
173
  example_depth = context.size
178
174
 
179
- self.keys.each do |sub_attribute_name, sub_attribute|
180
-
181
-
175
+ keys.each do |sub_attribute_name, sub_attribute|
182
176
  if sub_attribute.attributes
183
177
  # TODO: add option to raise an exception in this case?
184
178
  next if example_depth > MAX_EXAMPLE_DEPTH
185
179
  end
186
180
 
187
- sub_context = self.generate_subcontext(context,sub_attribute_name)
188
- block = Proc.new do
181
+ sub_context = generate_subcontext(context, sub_attribute_name)
182
+ block = proc do
189
183
  value = values.fetch(sub_attribute_name) do
190
184
  sub_attribute.example(sub_context, parent: parent)
191
185
  end
192
- sub_attribute.load(value,sub_context)
193
-
186
+ sub_attribute.load(value, sub_context)
194
187
  end
195
188
 
196
-
197
189
  hash[sub_attribute_name] = block
198
190
  end
199
191
 
200
192
  hash
201
193
  end
202
194
 
203
- def self.example(context=nil, **values)
204
-
205
- if (key_type == Object && value_type == Object && self.keys.empty?)
206
- return self.new
207
- end
195
+ def self.example(context = nil, **values)
196
+ return new if key_type == Object && value_type == Object && keys.empty?
208
197
 
209
- context ||= ["#{Hash}-#{rand(10000000)}"]
198
+ context ||= ["#{Hash}-#{rand(10_000_000)}"]
210
199
  context = Array(context)
211
200
 
212
- if self.keys.any?
213
- result = self.new
201
+ if keys.any?
202
+ result = new
214
203
  result.extend(ExampleMixin)
215
204
 
216
- result.lazy_attributes = self.example_contents(context, result, values)
205
+ result.lazy_attributes = example_contents(context, result, values)
217
206
  else
218
207
  hash = ::Hash.new
219
208
 
@@ -223,31 +212,27 @@ module Attributor
223
212
  hash[example_key] = value_type.example(subcontext)
224
213
  end
225
214
 
226
- result = self.new(hash)
215
+ result = new(hash)
227
216
  end
228
217
 
229
218
  result
230
219
  end
231
220
 
232
-
233
221
  def self.dump(value, **opts)
234
- if loaded = self.load(value)
222
+ if (loaded = load(value))
235
223
  loaded.dump(**opts)
236
- else
237
- nil
238
224
  end
239
225
  end
240
226
 
241
-
242
- def self.check_option!(name, definition)
227
+ def self.check_option!(name, _definition)
243
228
  case name
244
229
  when :reference
245
- :ok # FIXME ... actually do something smart
230
+ :ok # FIXME: ... actually do something smart
246
231
  when :dsl_compiler
247
232
  :ok
248
233
  when :case_insensitive_load
249
- unless self.key_type <= String
250
- raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{self.key_type.name}"
234
+ unless key_type <= String
235
+ raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{key_type.name}"
251
236
  end
252
237
  :ok
253
238
  when :allow_extra
@@ -257,37 +242,40 @@ module Attributor
257
242
  end
258
243
  end
259
244
 
260
-
261
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
245
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **_options)
262
246
  context = Array(context)
263
247
 
248
+ return value if value.is_a?(self)
249
+ return nil if value.nil? && !recurse
250
+
251
+ loaded_value = self.parse(value, context)
252
+
253
+ return from_hash(loaded_value, context, recurse: recurse) if keys.any?
254
+ load_generic(loaded_value, context)
255
+ end
256
+
257
+ def self.parse(value, context)
264
258
  if value.nil?
265
- if recurse
266
- loaded_value = {}
267
- else
268
- return nil
269
- end
270
- elsif value.is_a?(self)
271
- return value
272
- elsif value.kind_of?(Attributor::Hash)
273
- loaded_value = value.contents
259
+ {}
260
+ elsif value.is_a?(Attributor::Hash)
261
+ value.contents
274
262
  elsif value.is_a?(::Hash)
275
- loaded_value = value
263
+ value
276
264
  elsif value.is_a?(::String)
277
- loaded_value = decode_json(value,context)
265
+ decode_json(value, context)
278
266
  elsif value.respond_to?(:to_hash)
279
- loaded_value = value.to_hash
267
+ value.to_hash
280
268
  else
281
269
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
282
270
  end
271
+ end
283
272
 
284
- return self.from_hash(loaded_value,context, recurse: recurse) if self.keys.any?
285
- return self.new(loaded_value) if (key_type == Object && value_type == Object)
273
+ def self.load_generic(value, context)
274
+ return new(value) if key_type == Object && value_type == Object
286
275
 
287
- loaded_value.each_with_object(self.new) do| (k, v), obj |
288
- obj[self.key_type.load(k,context)] = self.value_type.load(v,context)
276
+ value.each_with_object(new) do |(k, v), obj|
277
+ obj[key_type.load(k, context)] = value_type.load(v, context)
289
278
  end
290
-
291
279
  end
292
280
 
293
281
  def self.generate_subcontext(context, key_name)
@@ -295,66 +283,55 @@ module Attributor
295
283
  end
296
284
 
297
285
  def generate_subcontext(context, key_name)
298
- self.class.generate_subcontext(context,key_name)
286
+ self.class.generate_subcontext(context, key_name)
299
287
  end
300
288
 
301
- def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
289
+ def get(key, context: generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT, key))
302
290
  key = self.class.key_attribute.load(key, context)
303
291
 
304
- if self.class.keys.empty?
305
- if @contents.key? key
306
- value = @contents[key]
307
- loaded_value = value_attribute.load(value, context)
308
- return self[key] = loaded_value
309
- else
310
- if self.class.options[:case_insensitive_load]
311
- key = key.downcase
312
- @contents.each do |k,v|
313
- if key == k.downcase
314
- return self.get(key, context: context)
315
- end
316
- end
317
- end
318
- end
319
- return nil
320
- end
321
-
292
+ return self.get_generic(key, context) if self.class.keys.empty?
322
293
  value = @contents[key]
323
294
 
324
295
  # FIXME: getting an unset value here should not force it in the hash
325
296
  if (attribute = self.class.keys[key])
326
297
  loaded_value = attribute.load(value, context)
327
- if loaded_value.nil?
328
- return nil
329
- else
330
- return self[key] = loaded_value
331
- end
298
+ return nil if loaded_value.nil?
299
+ return self[key] = loaded_value
332
300
  end
333
301
 
334
302
  if self.class.options[:case_insensitive_load]
335
303
  key = self.class.insensitive_map[key.downcase]
336
- return self.get(key, context: context)
304
+ return get(key, context: context)
337
305
  end
338
306
 
339
307
  if self.class.options[:allow_extra]
340
- if self.class.extra_keys.nil?
341
- return @contents[key] = self.class.value_attribute.load(value, context)
342
- else
343
- extra_keys_key = self.class.extra_keys
344
-
345
- if @contents.key? extra_keys_key
346
- return @contents[extra_keys_key].get(key, context: context)
347
- end
308
+ return @contents[key] = self.class.value_attribute.load(value, context) if self.class.extra_keys.nil?
309
+ extra_keys_key = self.class.extra_keys
348
310
 
311
+ if @contents.key? extra_keys_key
312
+ return @contents[extra_keys_key].get(key, context: context)
349
313
  end
350
- end
351
314
 
315
+ end
352
316
 
353
317
  raise LoadError, "Unknown key received: #{key.inspect} for #{Attributor.humanize_context(context)}"
354
318
  end
355
319
 
320
+ def get_generic(key, context)
321
+ if @contents.key? key
322
+ value = @contents[key]
323
+ loaded_value = value_attribute.load(value, context)
324
+ return self[key] = loaded_value
325
+ elsif self.class.options[:case_insensitive_load]
326
+ key = key.downcase
327
+ @contents.each do |k, _v|
328
+ return get(key, context: context) if key == k.downcase
329
+ end
330
+ end
331
+ nil
332
+ end
356
333
 
357
- def set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key), recurse: false)
334
+ def set(key, value, context: generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT, key), recurse: false)
358
335
  key = self.class.key_attribute.load(key, context)
359
336
 
360
337
  if self.class.keys.empty?
@@ -367,51 +344,50 @@ module Attributor
367
344
 
368
345
  if self.class.options[:case_insensitive_load]
369
346
  key = self.class.insensitive_map[key.downcase]
370
- return self.set(key, value, context: context)
347
+ return set(key, value, context: context)
371
348
  end
372
349
 
373
350
  if self.class.options[:allow_extra]
374
- if self.class.extra_keys.nil?
375
- return self[key] = self.class.value_attribute.load(value, context)
376
- else
377
- extra_keys_key = self.class.extra_keys
378
-
379
- unless @contents.key? extra_keys_key
380
- extra_keys_value = self.class.keys[extra_keys_key].load({})
381
- @contents[extra_keys_key] = extra_keys_value
382
- end
351
+ return self[key] = self.class.value_attribute.load(value, context) if self.class.extra_keys.nil?
383
352
 
384
- return self[extra_keys_key].set(key, value, context: context)
353
+ extra_keys_key = self.class.extra_keys
354
+
355
+ unless @contents.key? extra_keys_key
356
+ extra_keys_value = self.class.keys[extra_keys_key].load({})
357
+ @contents[extra_keys_key] = extra_keys_value
385
358
  end
359
+
360
+ return self[extra_keys_key].set(key, value, context: context)
361
+
386
362
  end
387
363
 
388
364
  raise LoadError, "Unknown key received: #{key.inspect} while loading #{Attributor.humanize_context(context)}"
389
365
  end
390
366
 
391
- def self.from_hash(object,context, recurse: false)
392
- hash = self.new
367
+ def self.from_hash(object, context, recurse: false)
368
+ hash = new
393
369
 
394
370
  # if the hash definition includes named extra keys, initialize
395
371
  # its value from the object in case it provides some already.
396
372
  # this is to ensure it exists when we handle any extra keys
397
373
  # that may exist in the object later
398
- if self.extra_keys
399
- sub_context = self.generate_subcontext(context,self.extra_keys)
400
- v = object.fetch(self.extra_keys, {})
401
- hash.set(self.extra_keys, v, context: sub_context, recurse: recurse)
374
+ if extra_keys
375
+ sub_context = generate_subcontext(context, extra_keys)
376
+ v = object.fetch(extra_keys, {})
377
+ hash.set(extra_keys, v, context: sub_context, recurse: recurse)
402
378
  end
403
379
 
404
- object.each do |k,val|
405
- next if k == self.extra_keys
380
+ object.each do |k, val|
381
+ next if k == extra_keys
406
382
 
407
- sub_context = self.generate_subcontext(context,k)
383
+ sub_context = generate_subcontext(context, k)
408
384
  hash.set(k, val, context: sub_context, recurse: recurse)
409
385
  end
410
386
 
411
387
  # handle default values for missing keys
412
- self.keys.each do |key_name, attribute|
388
+ keys.each do |key_name, attribute|
413
389
  next if hash.key?(key_name)
414
- sub_context = self.generate_subcontext(context,key_name)
390
+ sub_context = generate_subcontext(context, key_name)
415
391
  default = attribute.load(nil, sub_context, recurse: recurse)
416
392
  hash[key_name] = default unless default.nil?
417
393
  end
@@ -419,35 +395,32 @@ module Attributor
419
395
  hash
420
396
  end
421
397
 
422
-
423
- def self.validate(object,context=Attributor::DEFAULT_ROOT_CONTEXT,_attribute)
398
+ def self.validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute)
424
399
  context = [context] if context.is_a? ::String
425
400
 
426
- unless object.kind_of?(self)
427
- raise ArgumentError, "#{self.name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
401
+ unless object.is_a?(self)
402
+ raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
428
403
  end
429
404
 
430
405
  object.validate(context)
431
406
  end
432
407
 
433
- def self.describe(shallow=false, example: nil)
408
+ def self.describe(shallow = false, example: nil)
434
409
  hash = super(shallow)
435
410
 
436
- if key_type
437
- hash[:key] = {type: key_type.describe(true)}
438
- end
411
+ hash[:key] = { type: key_type.describe(true) } if key_type
439
412
 
440
- if self.keys.any?
413
+ if keys.any?
441
414
  # Spit keys if it's the root or if it's an anonymous structures
442
- if ( !shallow || self.name == nil)
415
+ if !shallow || name.nil?
443
416
  required_names = []
444
417
  # FIXME: change to :keys when the praxis doc browser supports displaying those
445
- hash[:attributes] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
418
+ hash[:attributes] = keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
446
419
  required_names << sub_name if sub_attribute.options[:required] == true
447
420
  sub_example = example.get(sub_name) if example
448
421
  sub_attributes[sub_name] = sub_attribute.describe(true, example: sub_example)
449
422
  end
450
- hash[:requirements] = self.requirements.each_with_object([]) do |req, list|
423
+ hash[:requirements] = requirements.each_with_object([]) do |req, list|
451
424
  described_req = req.describe(shallow)
452
425
  if described_req[:type] == :all
453
426
  # Add the names of the attributes that have the required flag too
@@ -458,11 +431,11 @@ module Attributor
458
431
  end
459
432
  # Make sure we create an :all requirement, if there wasn't one so we can add the required: true attributes
460
433
  unless required_names.empty?
461
- hash[:requirements] << {type: :all, attributes: required_names }
434
+ hash[:requirements] << { type: :all, attributes: required_names }
462
435
  end
463
436
  end
464
437
  else
465
- hash[:value] = {type: value_type.describe(true)}
438
+ hash[:value] = { type: value_type.describe(true) }
466
439
  end
467
440
 
468
441
  hash
@@ -479,7 +452,7 @@ module Attributor
479
452
  self[k]
480
453
  end
481
454
 
482
- def []=(k,v)
455
+ def []=(k, v)
483
456
  @contents[k] = v
484
457
  end
485
458
 
@@ -487,7 +460,7 @@ module Attributor
487
460
  @contents.each(&block)
488
461
  end
489
462
 
490
- alias_method :each_pair, :each
463
+ alias each_pair each
491
464
 
492
465
  def size
493
466
  @contents.size
@@ -508,14 +481,14 @@ module Attributor
508
481
  def key?(k)
509
482
  @contents.key?(k)
510
483
  end
511
- alias_method :has_key?, :key?
484
+ alias has_key? key?
512
485
 
513
486
  def merge(h)
514
487
  case h
515
488
  when self.class
516
489
  self.class.new(contents.merge(h.contents))
517
490
  when Attributor::Hash
518
- raise ArgumentError, "cannot merge Attributor::Hash instances of different types" unless h.is_a?(self.class)
491
+ raise ArgumentError, 'cannot merge Attributor::Hash instances of different types' unless h.is_a?(self.class)
519
492
  else
520
493
  raise TypeError, "no implicit conversion of #{h.class} into Attributor::Hash"
521
494
  end
@@ -527,7 +500,7 @@ module Attributor
527
500
 
528
501
  attr_reader :validating, :dumping
529
502
 
530
- def initialize(contents={})
503
+ def initialize(contents = {})
531
504
  @validating = false
532
505
  @dumping = false
533
506
 
@@ -550,80 +523,82 @@ module Attributor
550
523
  self.class.value_attribute
551
524
  end
552
525
 
553
-
554
526
  def ==(other)
555
527
  contents == other || (other.respond_to?(:contents) ? contents == other.contents : false)
556
528
  end
557
529
 
558
- def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
530
+ def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
559
531
  context = [context] if context.is_a? ::String
560
- errors = []
561
532
 
562
533
  if self.class.keys.any?
563
- extra_keys = @contents.keys - self.class.keys.keys
564
- if extra_keys.any? && !self.class.options[:allow_extra]
565
- return extra_keys.collect do |k|
566
- "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
567
- end
534
+ self.validate_keys(context)
535
+ else
536
+ self.validate_generic(context)
537
+ end
538
+ end
539
+
540
+ def validate_keys(context)
541
+ extra_keys = @contents.keys - self.class.keys.keys
542
+ if extra_keys.any? && !self.class.options[:allow_extra]
543
+ return extra_keys.collect do |k|
544
+ "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
568
545
  end
546
+ end
569
547
 
570
- keys_with_values = Array.new
548
+ errors = []
549
+ keys_with_values = []
571
550
 
572
- self.class.keys.each do |key, attribute|
573
- sub_context = self.class.generate_subcontext(context,key)
551
+ self.class.keys.each do |key, attribute|
552
+ sub_context = self.class.generate_subcontext(context, key)
574
553
 
575
- value = @contents[key]
576
- unless value.nil?
577
- keys_with_values << key
578
- end
554
+ value = @contents[key]
555
+ keys_with_values << key unless value.nil?
579
556
 
580
- if value.respond_to?(:validating) # really, it's a thing with sub-attributes
581
- next if value.validating
582
- end
583
-
584
- errors.push(*attribute.validate(value, sub_context))
585
- end
586
- self.class.requirements.each do |req|
587
- validation_errors = req.validate(keys_with_values, context)
588
- errors.push(*validation_errors) unless validation_errors.empty?
557
+ if value.respond_to?(:validating) # really, it's a thing with sub-attributes
558
+ next if value.validating
589
559
  end
590
- else
591
- @contents.each do |key, value|
592
- # FIXME: the sub contexts and error messages don't really make sense here
593
- unless key_type == Attributor::Object
594
- sub_context = context + ["key(#{key.inspect})"]
595
- errors.push(*key_attribute.validate(key, sub_context))
596
- end
597
560
 
598
- unless value_type == Attributor::Object
599
- sub_context = context + ["value(#{value.inspect})"]
600
- errors.push(*value_attribute.validate(value, sub_context))
601
- end
602
- end
561
+ errors.concat attribute.validate(value, sub_context)
562
+ end
563
+ self.class.requirements.each do |requirement|
564
+ validation_errors = requirement.validate(keys_with_values, context)
565
+ errors.concat(validation_errors) unless validation_errors.empty?
603
566
  end
604
567
  errors
605
568
  end
606
569
 
570
+ def validate_generic(context)
571
+ @contents.each_with_object([]) do |(key, value), errors|
572
+ # FIXME: the sub contexts and error messages don't really make sense here
573
+ unless key_type == Attributor::Object
574
+ sub_context = context + ["key(#{key.inspect})"]
575
+ errors.concat key_attribute.validate(key, sub_context)
576
+ end
577
+
578
+ unless value_type == Attributor::Object
579
+ sub_context = context + ["value(#{value.inspect})"]
580
+ errors.concat value_attribute.validate(value, sub_context)
581
+ end
582
+ end
583
+ end
607
584
 
608
585
  def dump(**opts)
609
586
  return CIRCULAR_REFERENCE_MARKER if @dumping
610
587
  @dumping = true
611
588
 
612
- contents.each_with_object({}) do |(k,v),hash|
613
- k = self.key_attribute.dump(k,opts)
589
+ contents.each_with_object({}) do |(k, v), hash|
590
+ k = key_attribute.dump(k, opts)
614
591
 
615
- if (attribute_for_value = self.class.keys[k])
616
- v = attribute_for_value.dump(v,opts)
617
- else
618
- v = self.value_attribute.dump(v,opts)
619
- end
592
+ v = if (attribute_for_value = self.class.keys[k])
593
+ attribute_for_value.dump(v, opts)
594
+ else
595
+ value_attribute.dump(v, opts)
596
+ end
620
597
 
621
598
  hash[k] = v
622
599
  end
623
600
  ensure
624
601
  @dumping = false
625
602
  end
626
-
627
603
  end
628
-
629
604
  end