attributor 5.0.2 → 5.1.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.
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