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.
- checksums.yaml +4 -4
- data/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
@@ -9,9 +9,8 @@ module Attributor
|
|
9
9
|
end
|
10
10
|
|
11
11
|
module ClassMethods
|
12
|
-
|
13
|
-
|
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:
|
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
|
-
|
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
|
-
|
36
|
-
|
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
|
data/lib/attributor/types/csv.rb
CHANGED
@@ -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 =
|
19
|
+
reason = 'Attributor::CSV only supports dumping values of type ' \
|
22
20
|
"Array or String, not #{values.class.name}."
|
23
|
-
raise DumpError
|
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
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
return ::Date
|
9
|
-
end
|
9
|
+
def self.example(context = nil, options: {})
|
10
|
+
load(Randgen.date, context)
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
10
|
+
::DateTime
|
13
11
|
end
|
14
12
|
|
15
|
-
def self.example(context=nil, options: {})
|
16
|
-
|
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, **
|
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?(
|
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
|
29
|
-
raise Attributor::DeserializationError, context: context, from: value.class, encoding:
|
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
|
-
|
9
|
+
::Float
|
11
10
|
end
|
12
11
|
|
13
|
-
def self.example(
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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 =
|
67
|
-
v =
|
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
|
-
|
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
|
-
|
95
|
+
definition
|
97
96
|
end
|
98
97
|
@keys
|
99
98
|
end
|
100
99
|
|
101
100
|
def self.definition
|
102
101
|
opts = {
|
103
|
-
:
|
104
|
-
:
|
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 =
|
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.
|
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 -
|
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
|
-
|
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
|
162
|
-
|
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] && !(
|
166
|
-
raise Attributor::AttributorException
|
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
|
-
|
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
|
-
|
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 =
|
188
|
-
block =
|
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(
|
198
|
+
context ||= ["#{Hash}-#{rand(10_000_000)}"]
|
210
199
|
context = Array(context)
|
211
200
|
|
212
|
-
if
|
213
|
-
result =
|
201
|
+
if keys.any?
|
202
|
+
result = new
|
214
203
|
result.extend(ExampleMixin)
|
215
204
|
|
216
|
-
result.lazy_attributes =
|
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 =
|
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 =
|
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
|
250
|
-
raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{
|
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
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
263
|
+
value
|
276
264
|
elsif value.is_a?(::String)
|
277
|
-
|
265
|
+
decode_json(value, context)
|
278
266
|
elsif value.respond_to?(:to_hash)
|
279
|
-
|
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
|
-
|
285
|
-
return
|
273
|
+
def self.load_generic(value, context)
|
274
|
+
return new(value) if key_type == Object && value_type == Object
|
286
275
|
|
287
|
-
|
288
|
-
obj[
|
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:
|
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
|
-
|
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
|
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
|
-
|
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:
|
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
|
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
|
-
|
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 =
|
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
|
399
|
-
sub_context =
|
400
|
-
v = object.fetch(
|
401
|
-
hash.set(
|
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 ==
|
380
|
+
object.each do |k, val|
|
381
|
+
next if k == extra_keys
|
406
382
|
|
407
|
-
sub_context =
|
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
|
-
|
388
|
+
keys.each do |key_name, attribute|
|
413
389
|
next if hash.key?(key_name)
|
414
|
-
sub_context =
|
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.
|
427
|
-
raise ArgumentError, "#{
|
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
|
413
|
+
if keys.any?
|
441
414
|
# Spit keys if it's the root or if it's an anonymous structures
|
442
|
-
if
|
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] =
|
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] =
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
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
|
-
|
548
|
+
errors = []
|
549
|
+
keys_with_values = []
|
571
550
|
|
572
|
-
|
573
|
-
|
551
|
+
self.class.keys.each do |key, attribute|
|
552
|
+
sub_context = self.class.generate_subcontext(context, key)
|
574
553
|
|
575
|
-
|
576
|
-
|
577
|
-
keys_with_values << key
|
578
|
-
end
|
554
|
+
value = @contents[key]
|
555
|
+
keys_with_values << key unless value.nil?
|
579
556
|
|
580
|
-
|
581
|
-
|
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
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
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 =
|
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
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
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
|