attributor 2.2.1 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +12 -4
- data/lib/attributor.rb +4 -2
- data/lib/attributor/attribute.rb +8 -9
- data/lib/attributor/exceptions.rb +6 -6
- data/lib/attributor/extensions/randexp.rb +5 -0
- data/lib/attributor/types/bigdecimal.rb +28 -0
- data/lib/attributor/types/boolean.rb +2 -0
- data/lib/attributor/types/collection.rb +6 -5
- data/lib/attributor/types/csv.rb +21 -0
- data/lib/attributor/types/date.rb +37 -0
- data/lib/attributor/types/date_time.rb +1 -1
- data/lib/attributor/types/hash.rb +64 -18
- data/lib/attributor/types/model.rb +14 -6
- data/lib/attributor/types/string.rb +1 -1
- data/lib/attributor/types/time.rb +39 -0
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_spec.rb +5 -12
- data/spec/support/hashes.rb +7 -0
- data/spec/support/models.rb +3 -1
- data/spec/types/bigdecimal_spec.rb +48 -0
- data/spec/types/boolean_spec.rb +4 -0
- data/spec/types/collection_spec.rb +12 -1
- data/spec/types/csv_spec.rb +31 -0
- data/spec/types/date_spec.rb +94 -0
- data/spec/types/date_time_spec.rb +21 -17
- data/spec/types/float_spec.rb +4 -0
- data/spec/types/hash_spec.rb +122 -11
- data/spec/types/ids_spec.rb +2 -2
- data/spec/types/integer_spec.rb +3 -0
- data/spec/types/model_spec.rb +16 -4
- data/spec/types/string_spec.rb +4 -0
- data/spec/types/time_spec.rb +91 -0
- metadata +12 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eaf11f67f19da1559c3abdbcebed02193ac9e5e3
|
4
|
+
data.tar.gz: 6ba6a1dbf45d4e2d43450036651f0d4c0fde9829
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 410e51a836135f981bee0cc40aabb254c526ade79aafa224a96881dac78b8c1986ff088b33f89e80da8a0899ebc086a9ff589768130adf78ca3cb84ae7a8b350
|
7
|
+
data.tar.gz: 22d78b19144db5a3c19b778a6c9c32e66ce819f86d3a71dc443462ab0f4a8911345cefe193995b43897c8fd30224de0a67a7e000fa79a5b18a66586e70adbb67
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
Attributor Changelog
|
2
2
|
============================
|
3
3
|
|
4
|
-
|
4
|
+
2.3.0
|
5
5
|
------
|
6
6
|
|
7
|
+
* Added `recurse` option to `Type.load` that is used by `Model` and `Hash` to force the loading of values (specifically, so that default values are assigned) even if the loaded value is `nil`.
|
8
|
+
* Fix `Attributor::CSV` to dump `String` values and generate `String` examples.
|
9
|
+
* Default values of `false` now work correctly.
|
10
|
+
* Added `BigDecimal`, `Date` and `Time` types
|
11
|
+
* `DateTime.load` now raises `CoercionError` (instead of returning `nil`) if given values that can not coerced properly.
|
12
|
+
* `Hash.dump` now first calls `Hash.load`, and correctly uses defined value types for dumping.
|
13
|
+
* Added `Hash#get`, for retrieving keys using the same logic the `case_insensitive_load` and `allow_extra` with defined `extra` key.
|
14
|
+
|
7
15
|
|
8
16
|
2.2.1
|
9
17
|
------
|
@@ -22,7 +30,7 @@ next
|
|
22
30
|
* `:case_insensitive_load` for string-keyed hashes. This allows loading hashes with keys that do not exactly match the case defined in the hash.
|
23
31
|
* Added `:allow_extras` option to allow handling of undefined keys when loading.
|
24
32
|
* Added `Hash#set` to encapsulate the above options and attribute loading.
|
25
|
-
* Added `extra` command in the `keys` DSL, which lets you define a key (whose value should be a Hash), to group any unspecified keys during load.
|
33
|
+
* Added `extra` command in the `keys` DSL, which lets you define a key (whose value should be a Hash), to group any unspecified keys during load.
|
26
34
|
|
27
35
|
2.1.0
|
28
36
|
------
|
@@ -31,7 +39,7 @@ next
|
|
31
39
|
* Add Collection subclasses for CSVs and Ids
|
32
40
|
* CSV type for Collection of values serialized as comma-separated strings.
|
33
41
|
* Ids type. A helper for creating CSVs with members matching a given a type's :identity option.
|
34
|
-
* Allow instances of Models to be initialized with initial data.
|
42
|
+
* Allow instances of Models to be initialized with initial data.
|
35
43
|
* Supported formats for the data are equivalent to the loading formats (i.e. ruby Hash, a JSON string or another instance of the same model type).
|
36
44
|
* Improved context reporting in errors
|
37
45
|
* Added contextual information while loading and dumping attributes.
|
@@ -39,7 +47,7 @@ next
|
|
39
47
|
* `validate` takes a `context` argument that (instead of a string) is now an array of parent segments.
|
40
48
|
* `dump` takes a `context:` option parameter of the same type
|
41
49
|
* Enhanced error messages to report the correct context scope.
|
42
|
-
* Make Attribute assignments in models to report a special context (not the attributor root)
|
50
|
+
* Make Attribute assignments in models to report a special context (not the attributor root)
|
43
51
|
* Instead of reporting "$." as the context , when doing model.field_name=value, they'll now report "assignment.of(field_name)" instead
|
44
52
|
* Truncate the length of values when reporting loading errors when they're long (i.e. >500 chars)
|
45
53
|
* `Model.attributes` may now be called more than once to set add or replace attributes. The exact behavior depends upon the types of the attributes being added or replaced. See [model_spec.rb](spec/types/model_spec.rb) for examples.
|
data/lib/attributor.rb
CHANGED
@@ -67,16 +67,19 @@ module Attributor
|
|
67
67
|
|
68
68
|
require_relative 'attributor/types/container'
|
69
69
|
require_relative 'attributor/types/object'
|
70
|
+
require_relative 'attributor/types/bigdecimal'
|
70
71
|
require_relative 'attributor/types/integer'
|
71
72
|
require_relative 'attributor/types/string'
|
72
73
|
require_relative 'attributor/types/model'
|
73
74
|
require_relative 'attributor/types/struct'
|
74
75
|
require_relative 'attributor/types/boolean'
|
76
|
+
require_relative 'attributor/types/date'
|
75
77
|
require_relative 'attributor/types/date_time'
|
78
|
+
require_relative 'attributor/types/time'
|
76
79
|
require_relative 'attributor/types/float'
|
77
80
|
require_relative 'attributor/types/collection'
|
78
81
|
require_relative 'attributor/types/hash'
|
79
|
-
|
82
|
+
|
80
83
|
|
81
84
|
require_relative 'attributor/types/csv'
|
82
85
|
require_relative 'attributor/types/ids'
|
@@ -85,5 +88,4 @@ module Attributor
|
|
85
88
|
require_relative 'attributor/types/tempfile'
|
86
89
|
require_relative 'attributor/types/file_upload'
|
87
90
|
|
88
|
-
|
89
91
|
end
|
data/lib/attributor/attribute.rb
CHANGED
@@ -38,11 +38,10 @@ module Attributor
|
|
38
38
|
|
39
39
|
|
40
40
|
def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
41
|
-
value = type.load(value,context,**options)
|
41
|
+
value = type.load(value,context,**options)
|
42
42
|
|
43
|
-
# just in case type.load(value) returned nil, even though value is not nil.
|
44
43
|
if value.nil?
|
45
|
-
value = self.options[:default] if self.options
|
44
|
+
value = self.options[:default] if self.options.key?(:default)
|
46
45
|
end
|
47
46
|
|
48
47
|
value
|
@@ -72,7 +71,7 @@ module Attributor
|
|
72
71
|
TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if ]
|
73
72
|
INTERNAL_OPTIONS = [:dsl_compiler,:dsl_compiler_options] # Options we don't want to expose when describing attributes
|
74
73
|
def describe(shallow=true)
|
75
|
-
description = { }
|
74
|
+
description = { }
|
76
75
|
# Clone the common options
|
77
76
|
TOP_LEVEL_OPTIONS.each do |option_name|
|
78
77
|
description[option_name] = self.options[option_name] if self.options.has_key? option_name
|
@@ -100,13 +99,13 @@ module Attributor
|
|
100
99
|
def example(context=nil, parent: nil, values:{})
|
101
100
|
raise ArgumentError, "attribute example cannot take a context of type String" if (context.is_a? ::String )
|
102
101
|
if context
|
103
|
-
ctx = Attributor.humanize_context(context)
|
102
|
+
ctx = Attributor.humanize_context(context)
|
104
103
|
seed, _ = Digest::SHA1.digest(ctx).unpack("QQ")
|
105
104
|
Random.srand(seed)
|
106
105
|
end
|
107
106
|
|
108
107
|
if self.options.has_key? :example
|
109
|
-
val = self.options[:example]
|
108
|
+
val = self.options[:example]
|
110
109
|
case val
|
111
110
|
when ::String
|
112
111
|
# FIXME: spec this properly to use self.type.native_type
|
@@ -180,7 +179,7 @@ module Attributor
|
|
180
179
|
|
181
180
|
def validate_missing_value(context)
|
182
181
|
raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
|
183
|
-
|
182
|
+
|
184
183
|
# Missing attribute was required if :required option was set
|
185
184
|
return ["Attribute #{Attributor.humanize_context(context)} is required"] if self.options[:required]
|
186
185
|
|
@@ -201,7 +200,7 @@ module Attributor
|
|
201
200
|
# should never get here if the option validation worked...
|
202
201
|
raise AttributorException.new("unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}")
|
203
202
|
end
|
204
|
-
|
203
|
+
|
205
204
|
# chop off the last part
|
206
205
|
requirement_context = context[0..-2]
|
207
206
|
requirement_context_string = requirement_context.join(Attributor::SEPARATOR)
|
@@ -257,7 +256,7 @@ module Attributor
|
|
257
256
|
raise AttributorException.new("Required_if must be a String, a Hash definition or a Proc") unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc)
|
258
257
|
raise AttributorException.new("Required_if cannot be specified together with :required") if self.options[:required]
|
259
258
|
when :example
|
260
|
-
unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition)
|
259
|
+
unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition)
|
261
260
|
raise AttributorException.new("Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)")
|
262
261
|
end
|
263
262
|
else
|
@@ -4,9 +4,9 @@ module Attributor
|
|
4
4
|
|
5
5
|
class LoadError < AttributorException
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
class IncompatibleTypeError < LoadError
|
9
|
-
|
9
|
+
|
10
10
|
def initialize(type:, value_type: , context: )
|
11
11
|
super "Type #{type} cannot load values of type #{value_type} while loading #{Attributor.humanize_context(context)}."
|
12
12
|
end
|
@@ -19,18 +19,18 @@ module Attributor
|
|
19
19
|
super msg
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
class DeserializationError < LoadError
|
24
24
|
def initialize( context: , from:, encoding: , value: nil)
|
25
25
|
msg = "Error deserializing a #{from} using #{encoding} while loading #{Attributor.humanize_context(context)}."
|
26
26
|
msg += " Received value #{Attributor.errorize_value(value)}" if value
|
27
27
|
super msg
|
28
|
-
end
|
28
|
+
end
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
class DumpError < AttributorException
|
32
32
|
def initialize( context: , name: , type: , original_exception: )
|
33
|
-
msg = "Error while dumping attribute #{name} of type #{type} for context #{Attributor.humanize_context(context)}
|
33
|
+
msg = "Error while dumping attribute #{name} of type #{type} for context #{Attributor.humanize_context(context)}."
|
34
34
|
msg << " Reason: #{original_exception}"
|
35
35
|
super msg
|
36
36
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Attributor
|
4
|
+
|
5
|
+
class BigDecimal
|
6
|
+
include Type
|
7
|
+
|
8
|
+
def self.native_type
|
9
|
+
return ::BigDecimal
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.example(context=nil, **options)
|
13
|
+
return ::BigDecimal.new("#{/\d{3}/.gen}.#{/\d{3}/.gen}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
17
|
+
return nil if value.nil?
|
18
|
+
return value if value.is_a?(self.native_type)
|
19
|
+
if value.kind_of?(::Float)
|
20
|
+
return BigDecimal(value, 10)
|
21
|
+
end
|
22
|
+
return BigDecimal(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
@@ -16,6 +16,8 @@ module Attributor
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
19
|
+
return nil if value.nil?
|
20
|
+
|
19
21
|
raise CoercionError, context: context, from: value.class, to: self, value: value if value.is_a?(::Float)
|
20
22
|
return false if [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].include?(value)
|
21
23
|
return true if [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].include?(value)
|
@@ -41,10 +41,12 @@ module Attributor
|
|
41
41
|
# @return An Array of native type objects conforming to the specified member_type
|
42
42
|
def self.example(context=nil, options: {})
|
43
43
|
result = []
|
44
|
-
size = rand(3) + 1
|
44
|
+
size = options[:size] || (rand(3) + 1)
|
45
|
+
size = [*size].sample if size.is_a?(Range)
|
46
|
+
|
45
47
|
context ||= ["Collection-#{result.object_id}"]
|
46
48
|
context = Array(context)
|
47
|
-
|
49
|
+
|
48
50
|
size.times do |i|
|
49
51
|
subcontext = context + ["at(#{i})"]
|
50
52
|
result << self.member_attribute.example(subcontext)
|
@@ -64,7 +66,7 @@ module Attributor
|
|
64
66
|
elsif value.is_a?(::String)
|
65
67
|
loaded_value = decode_string(value,context)
|
66
68
|
else
|
67
|
-
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
69
|
+
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
68
70
|
end
|
69
71
|
|
70
72
|
return loaded_value.collect { |member| self.member_attribute.load(member,context) }
|
@@ -82,7 +84,6 @@ module Attributor
|
|
82
84
|
end
|
83
85
|
|
84
86
|
def self.describe(shallow=false)
|
85
|
-
#puts "Collection: #{self.type}"
|
86
87
|
hash = super(shallow)
|
87
88
|
hash[:options] = {} unless hash[:options]
|
88
89
|
hash[:member_attribute] = self.member_attribute.describe
|
@@ -95,7 +96,7 @@ module Attributor
|
|
95
96
|
if options.has_key?(:reference) && !member_options.has_key?(:reference)
|
96
97
|
member_options[:reference] = options[:reference]
|
97
98
|
end
|
98
|
-
|
99
|
+
|
99
100
|
# create the member_attribute, passing in our member_type and whatever constructor_block is.
|
100
101
|
# that in turn will call construct on the type if applicable.
|
101
102
|
@member_attribute = Attributor::Attribute.new self.member_type, member_options, &constructor_block
|
data/lib/attributor/types/csv.rb
CHANGED
@@ -6,5 +6,26 @@ module Attributor
|
|
6
6
|
value.split(',')
|
7
7
|
end
|
8
8
|
|
9
|
+
def self.dump(values, **opts)
|
10
|
+
case values
|
11
|
+
when ::String
|
12
|
+
values
|
13
|
+
when ::Array
|
14
|
+
values.collect { |value| member_attribute.dump(value,opts).to_s }.join(',')
|
15
|
+
else
|
16
|
+
context = opts[:context]
|
17
|
+
name = opts[:context].last.to_s
|
18
|
+
type = values.class.name
|
19
|
+
reason = "Attributor::CSV only supports dumping values of type " +
|
20
|
+
"Array or String, not #{values.class.name}."
|
21
|
+
raise DumpError.new(context: context, name: name, type: type, original_exception: reason)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.example(context=nil, options: {})
|
26
|
+
collection = super(context, options: options.merge(size: (2..4)))
|
27
|
+
return collection.join(',')
|
28
|
+
end
|
29
|
+
|
9
30
|
end
|
10
31
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Attributor
|
4
|
+
|
5
|
+
class Date
|
6
|
+
include Type
|
7
|
+
|
8
|
+
def self.native_type
|
9
|
+
return ::Date
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.example(context=nil, options: {})
|
13
|
+
return self.load(/[:date:]/.gen, context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
17
|
+
return value if value.is_a?(self.native_type)
|
18
|
+
return nil if value.nil?
|
19
|
+
|
20
|
+
return value.to_date if value.respond_to?(:to_date)
|
21
|
+
|
22
|
+
case value
|
23
|
+
when ::String
|
24
|
+
begin
|
25
|
+
return ::Date.parse(value)
|
26
|
+
rescue ArgumentError => e
|
27
|
+
raise Attributor::DeserializationError, context: context, from: value.class, encoding: "Date" , value: value
|
28
|
+
end
|
29
|
+
else
|
30
|
+
raise CoercionError, context: context, from: value.class, to: self, value: value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
@@ -19,7 +19,7 @@ module Attributor
|
|
19
19
|
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
20
20
|
# We assume that if the value is already in the right type, we've decoded it already
|
21
21
|
return value if value.is_a?(self.native_type)
|
22
|
-
return value.to_datetime if value.
|
22
|
+
return value.to_datetime if value.respond_to?(:to_datetime)
|
23
23
|
return nil unless value.is_a?(::String)
|
24
24
|
# TODO: we should be able to convert not only from String but Time...etc
|
25
25
|
# Else, we'll decode it from String.
|
@@ -119,7 +119,7 @@ module Attributor
|
|
119
119
|
|
120
120
|
unless @concrete
|
121
121
|
return self.of(key:self.key_type, value: self.value_type)
|
122
|
-
|
122
|
+
.construct(constructor_block,**options)
|
123
123
|
end
|
124
124
|
|
125
125
|
if options[:case_insensitive_load] && !(self.key_type <= String)
|
@@ -158,15 +158,10 @@ module Attributor
|
|
158
158
|
|
159
159
|
|
160
160
|
def self.dump(value, **opts)
|
161
|
-
|
162
|
-
|
163
|
-
value.each_with_object({}) do |(k,v),hash|
|
164
|
-
k = key_type.dump(k,opts) if @key_type
|
165
|
-
v = value_type.dump(v,opts) if @value_type
|
166
|
-
hash[k] = v
|
167
|
-
end
|
161
|
+
self.load(value).dump(**opts)
|
168
162
|
end
|
169
163
|
|
164
|
+
|
170
165
|
def self.check_option!(name, definition)
|
171
166
|
case name
|
172
167
|
when :reference
|
@@ -186,9 +181,13 @@ module Attributor
|
|
186
181
|
end
|
187
182
|
|
188
183
|
|
189
|
-
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
184
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
|
190
185
|
if value.nil?
|
191
|
-
|
186
|
+
if recurse
|
187
|
+
loaded_value = {}
|
188
|
+
else
|
189
|
+
return nil
|
190
|
+
end
|
192
191
|
elsif value.is_a?(self)
|
193
192
|
return value
|
194
193
|
elsif value.kind_of?(Attributor::Hash)
|
@@ -201,7 +200,7 @@ module Attributor
|
|
201
200
|
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
202
201
|
end
|
203
202
|
|
204
|
-
return self.from_hash(loaded_value,context) if self.keys.any?
|
203
|
+
return self.from_hash(loaded_value,context, recurse: recurse) if self.keys.any?
|
205
204
|
return self.new(loaded_value) if (key_type == Object && value_type == Object)
|
206
205
|
|
207
206
|
loaded_value.each_with_object(self.new) do| (k, v), obj |
|
@@ -215,9 +214,41 @@ module Attributor
|
|
215
214
|
end
|
216
215
|
|
217
216
|
def generate_subcontext(context, key_name)
|
218
|
-
self.class.
|
217
|
+
self.class.generate_subcontext(context,key_name)
|
218
|
+
end
|
219
|
+
|
220
|
+
def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
|
221
|
+
key = self.class.key_attribute.load(key, context)
|
222
|
+
value = @contents[key]
|
223
|
+
|
224
|
+
if (attribute = self.class.keys[key])
|
225
|
+
return self[key] = attribute.load(value, context)
|
226
|
+
end
|
227
|
+
|
228
|
+
if self.class.options[:case_insensitive_load]
|
229
|
+
key = self.class.insensitive_map[key.downcase]
|
230
|
+
return self.get(key, context: context)
|
231
|
+
end
|
232
|
+
|
233
|
+
if self.class.options[:allow_extra]
|
234
|
+
if self.class.extra_keys.nil?
|
235
|
+
return @contents[key] = self.class.value_attribute.load(value, context)
|
236
|
+
else
|
237
|
+
extra_keys_key = self.class.extra_keys
|
238
|
+
|
239
|
+
unless @contents.key? extra_keys_key
|
240
|
+
extra_keys_value = self.class.keys[extra_keys_key].load({})
|
241
|
+
@contents[extra_keys_key] = extra_keys_value
|
242
|
+
end
|
243
|
+
|
244
|
+
return @contents[extra_keys_key].get(key, context: context)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
raise AttributorException, "Unknown key received: #{key.inspect} for #{Attributor.humanize_context(context)}"
|
219
249
|
end
|
220
250
|
|
251
|
+
|
221
252
|
def set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
|
222
253
|
key = self.class.key_attribute.load(key, context)
|
223
254
|
|
@@ -234,14 +265,21 @@ module Attributor
|
|
234
265
|
if self.class.extra_keys.nil?
|
235
266
|
return self[key] = self.class.value_attribute.load(value, context)
|
236
267
|
else
|
237
|
-
|
268
|
+
extra_keys_key = self.class.extra_keys
|
269
|
+
|
270
|
+
unless @contents.key? extra_keys_key
|
271
|
+
extra_keys_value = self.class.keys[extra_keys_key].load({})
|
272
|
+
@contents[extra_keys_key] = extra_keys_value
|
273
|
+
end
|
274
|
+
|
275
|
+
return self[extra_keys_key].set(key, value, context: context)
|
238
276
|
end
|
239
277
|
end
|
240
278
|
|
241
279
|
raise AttributorException, "Unknown key received: #{key.inspect} while loading #{Attributor.humanize_context(context)}"
|
242
280
|
end
|
243
281
|
|
244
|
-
def self.from_hash(object,context)
|
282
|
+
def self.from_hash(object,context, recurse: false)
|
245
283
|
hash = self.new
|
246
284
|
|
247
285
|
# if the hash definition includes named extra keys, initialize
|
@@ -265,7 +303,7 @@ module Attributor
|
|
265
303
|
self.keys.each do |key_name, attribute|
|
266
304
|
next if hash.key?(key_name)
|
267
305
|
sub_context = self.generate_subcontext(context,key_name)
|
268
|
-
hash[key_name] = attribute.load(nil, sub_context)
|
306
|
+
hash[key_name] = attribute.load(nil, sub_context, recurse: recurse)
|
269
307
|
end
|
270
308
|
|
271
309
|
hash
|
@@ -374,9 +412,17 @@ module Attributor
|
|
374
412
|
end
|
375
413
|
end
|
376
414
|
|
377
|
-
def dump(
|
378
|
-
@contents.each_with_object(
|
379
|
-
|
415
|
+
def dump(**opts)
|
416
|
+
@contents.each_with_object({}) do |(k,v),hash|
|
417
|
+
k = self.key_attribute.dump(k,opts)
|
418
|
+
|
419
|
+
if (attribute_for_value = self.class.keys[k])
|
420
|
+
v = attribute_for_value.dump(v,opts)
|
421
|
+
else
|
422
|
+
v = self.value_attribute.dump(v,opts)
|
423
|
+
end
|
424
|
+
|
425
|
+
hash[k] = v
|
380
426
|
end
|
381
427
|
end
|
382
428
|
|