attributor 2.2.1 → 2.3.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/.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
|
|