attributed_object 0.2.1
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 +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +222 -0
- data/Rakefile +1 -0
- data/attributed_object.gemspec +24 -0
- data/benchmark_attributed_object.rb +108 -0
- data/lib/attributed_object.rb +67 -0
- data/lib/attributed_object/base.rb +106 -0
- data/lib/attributed_object/coerce.rb +30 -0
- data/lib/attributed_object/strict.rb +28 -0
- data/lib/attributed_object/type.rb +11 -0
- data/lib/attributed_object/types/array_of.rb +25 -0
- data/lib/attributed_object/types/hash_of.rb +28 -0
- data/lib/attributed_object/version.rb +3 -0
- data/lib/attributed_object_helpers/hash_util.rb +21 -0
- data/lib/attributed_object_helpers/type_check.rb +46 -0
- data/lib/attributed_object_helpers/type_coerce.rb +52 -0
- data/spec/attributed_object/coerce_spec.rb +159 -0
- data/spec/attributed_object/strict_spec.rb +129 -0
- data/spec/attributed_object_spec.rb +228 -0
- metadata +110 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
module AttributedObject
|
2
|
+
module Base
|
3
|
+
module ClassExtension
|
4
|
+
def attributed_object(options={})
|
5
|
+
@attributed_object_options = attributed_object_options.merge(options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def attributed_object_options
|
9
|
+
return @attributed_object_options if !@attributed_object_options.nil?
|
10
|
+
|
11
|
+
parent_ops = self.superclass.respond_to?(:attributed_object_options) ? self.superclass.attributed_object_options : {}
|
12
|
+
|
13
|
+
@attributed_object_options = {
|
14
|
+
default_to: Unset,
|
15
|
+
ignore_extra_keys: false,
|
16
|
+
coerce_blanks_to_nil: false
|
17
|
+
}.merge(parent_ops)
|
18
|
+
end
|
19
|
+
|
20
|
+
def attribute_defs
|
21
|
+
return @attribute_defs if @attribute_defs
|
22
|
+
parent_defs = {}
|
23
|
+
parent_defs = self.superclass.attribute_defs if self.superclass.respond_to?(:attribute_defs)
|
24
|
+
@attribute_defs = parent_defs.clone
|
25
|
+
end
|
26
|
+
|
27
|
+
def attribute(attr_name, type_info = Unset, default: Unset, disallow: Unset)
|
28
|
+
if default == Unset
|
29
|
+
default_to = attributed_object_options.fetch(:default_to)
|
30
|
+
|
31
|
+
if default_to != Unset
|
32
|
+
default = default_to.is_a?(TypeDefaults) ? default_to.fetch(type_info) : default_to
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
_attributed_object_check_type_supported!(type_info)
|
37
|
+
|
38
|
+
attribute_defs[attr_name] = {
|
39
|
+
type_info: type_info,
|
40
|
+
default: default,
|
41
|
+
disallow: disallow,
|
42
|
+
}
|
43
|
+
|
44
|
+
attr_writer attr_name
|
45
|
+
attr_reader attr_name
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
def initialize(args={})
|
51
|
+
initialize_attributes(args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def attributes
|
55
|
+
Hash[self.class.attribute_defs.map { |name, _|
|
56
|
+
[name, self.send(name)]
|
57
|
+
}]
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize_attributes(args)
|
61
|
+
symbolized_args = AttributedObjectHelpers::HashUtil.symbolize_keys(args)
|
62
|
+
if !self.class.attributed_object_options.fetch(:ignore_extra_keys)
|
63
|
+
symbolized_args.keys.each do |key|
|
64
|
+
if !self.class.attribute_defs.keys.include?(key)
|
65
|
+
raise UnknownAttributeError.new(self.class, key, args)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
else
|
69
|
+
symbolized_args = AttributedObjectHelpers::HashUtil.slice(symbolized_args, self.class.attribute_defs.keys)
|
70
|
+
end
|
71
|
+
|
72
|
+
self.class.attribute_defs.each { |name, opts|
|
73
|
+
if !symbolized_args.has_key?(name)
|
74
|
+
default = opts[:default]
|
75
|
+
default = default.call if default.respond_to?(:call)
|
76
|
+
symbolized_args[name] = default unless default == Unset
|
77
|
+
end
|
78
|
+
|
79
|
+
if !symbolized_args.has_key?(name)
|
80
|
+
raise MissingAttributeError.new(self.class, name, args)
|
81
|
+
end
|
82
|
+
|
83
|
+
if opts[:disallow] != Unset && symbolized_args[name] == opts[:disallow]
|
84
|
+
raise DisallowedValueError.new(self.class, name, args)
|
85
|
+
end
|
86
|
+
|
87
|
+
if opts[:type_info] != Unset && symbolized_args[name] != nil
|
88
|
+
symbolized_args[name] = _attributed_object_on_init_attribute(opts[:type_info], symbolized_args[name], name: name, args: args)
|
89
|
+
end
|
90
|
+
self.send("#{name}=", symbolized_args[name])
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def ==(other)
|
95
|
+
self.class == other.class && self.attributes == other.attributes
|
96
|
+
end
|
97
|
+
|
98
|
+
def as_json(options=nil)
|
99
|
+
attrs = self.attributes
|
100
|
+
return attrs.as_json(options) if attrs.respond_to?(:as_json)
|
101
|
+
{}.merge(attrs)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module AttributedObject
|
2
|
+
module Coerce
|
3
|
+
def self.included(descendant)
|
4
|
+
super
|
5
|
+
descendant.send(:extend, ClassExtension)
|
6
|
+
descendant.send(:include, InstanceMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassExtension
|
10
|
+
include AttributedObject::Base::ClassExtension
|
11
|
+
|
12
|
+
def _attributed_object_check_type_supported!(type_info)
|
13
|
+
AttributedObjectHelpers::TypeCoerce.check_type_supported!(type_info)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
include AttributedObject::Base::InstanceMethods
|
19
|
+
|
20
|
+
def _attributed_object_on_init_attribute(type_info, value, name:, args:)
|
21
|
+
return AttributedObjectHelpers::TypeCoerce.coerce(
|
22
|
+
type_info,
|
23
|
+
value,
|
24
|
+
coerce_blanks_to_nil: self.class.attributed_object_options.fetch(:coerce_blanks_to_nil)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module AttributedObject
|
2
|
+
module Strict
|
3
|
+
def self.included(descendant)
|
4
|
+
super
|
5
|
+
descendant.send(:extend, ClassExtension)
|
6
|
+
descendant.send(:include, InstanceMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassExtension
|
10
|
+
include AttributedObject::Base::ClassExtension
|
11
|
+
|
12
|
+
def _attributed_object_check_type_supported!(type_info)
|
13
|
+
AttributedObjectHelpers::TypeCheck.check_type_supported!(type_info)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
include AttributedObject::Base::InstanceMethods
|
19
|
+
|
20
|
+
def _attributed_object_on_init_attribute(type_info, value, name:, args:)
|
21
|
+
type_matches = AttributedObjectHelpers::TypeCheck.check(type_info, value)
|
22
|
+
raise TypeError.new(self.class, name, args) if !type_matches
|
23
|
+
return value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module AttributedObject
|
2
|
+
module Types
|
3
|
+
class ArrayOf < AttributedObject::Type
|
4
|
+
def initialize(type_info)
|
5
|
+
@type_info = type_info
|
6
|
+
end
|
7
|
+
|
8
|
+
def strict_check(array)
|
9
|
+
return false if !array.is_a?(Array)
|
10
|
+
array.all?{ |e| AttributedObjectHelpers::TypeCheck.check(@type_info, e) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def coerce(array)
|
14
|
+
raise AttributedObject::UncoercibleValueError.new("Trying to coerce into Array but value is not an array") if !array.is_a?(Array)
|
15
|
+
array.map { |e| AttributedObjectHelpers::TypeCoerce.coerce(@type_info, e) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module AttributedObject::Base::ClassExtension
|
22
|
+
def ArrayOf(type_info)
|
23
|
+
AttributedObject::Types::ArrayOf.new(type_info)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module AttributedObject
|
2
|
+
module Types
|
3
|
+
class HashOf < AttributedObject::Type
|
4
|
+
def initialize(key_type_info, value_type_info)
|
5
|
+
@key_type_info = key_type_info
|
6
|
+
@value_type_info = value_type_info
|
7
|
+
end
|
8
|
+
|
9
|
+
def strict_check(hash)
|
10
|
+
return false if !hash.is_a?(Hash)
|
11
|
+
hash.all? do |k,v|
|
12
|
+
AttributedObjectHelpers::TypeCheck.check(@key_type_info, k) && AttributedObjectHelpers::TypeCheck.check(@value_type_info, v)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def coerce(hash)
|
17
|
+
raise AttributedObject::UncoercibleValueError.new("Trying to coerce into Hash but value is not an hash") if !hash.is_a?(Hash)
|
18
|
+
hash.map { |k,v| [AttributedObjectHelpers::TypeCoerce.coerce(@key_type_info, k), AttributedObjectHelpers::TypeCoerce.coerce(@value_type_info, v)] }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module AttributedObject::Base::ClassExtension
|
25
|
+
def HashOf(key_type_info, value_type_info)
|
26
|
+
AttributedObject::Types::HashOf.new(key_type_info, value_type_info)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module AttributedObjectHelpers
|
2
|
+
class HashUtil
|
3
|
+
def self.symbolize_keys(hash)
|
4
|
+
new_hash = {}
|
5
|
+
|
6
|
+
hash.each { |k, v|
|
7
|
+
if k.respond_to?(:to_sym)
|
8
|
+
new_hash[k.to_sym] = v
|
9
|
+
else
|
10
|
+
new_hash[k] = v
|
11
|
+
end
|
12
|
+
}
|
13
|
+
|
14
|
+
return new_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.slice(hash, keys)
|
18
|
+
Hash[ [keys, hash.values_at(*keys)].transpose]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module AttributedObjectHelpers
|
2
|
+
class TypeCheck
|
3
|
+
def self.check_type_supported!(type_info)
|
4
|
+
supported = type_info.is_a?(Class) || [
|
5
|
+
:string,
|
6
|
+
:boolean,
|
7
|
+
:integer,
|
8
|
+
:float,
|
9
|
+
:numeric,
|
10
|
+
:symbol,
|
11
|
+
:array,
|
12
|
+
:hash
|
13
|
+
].include?(type_info)
|
14
|
+
supported = type_info.is_a?(AttributedObject::Type) if !supported
|
15
|
+
raise AttributedObject::ConfigurationError.new("Unknown Type for type checking #{type_info}") unless supported
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.check(type_info, value)
|
19
|
+
return value.is_a?(type_info) if type_info.is_a?(Class)
|
20
|
+
|
21
|
+
case type_info
|
22
|
+
when :string
|
23
|
+
return value.is_a?(String)
|
24
|
+
when :boolean
|
25
|
+
return value == true || value == false
|
26
|
+
when :integer
|
27
|
+
return value.is_a?(Integer)
|
28
|
+
when :float
|
29
|
+
return value.is_a?(Float)
|
30
|
+
when :numeric
|
31
|
+
return value.is_a?(Numeric)
|
32
|
+
when :symbol
|
33
|
+
return value.is_a?(Symbol)
|
34
|
+
when :array
|
35
|
+
return value.is_a?(Array)
|
36
|
+
when :hash
|
37
|
+
return value.is_a?(Hash)
|
38
|
+
else
|
39
|
+
if type_info.is_a?(AttributedObject::Type)
|
40
|
+
return type_info.strict_check(value)
|
41
|
+
end
|
42
|
+
raise AttributedObject::ConfigurationError.new("Unknown Type for type checking #{type_info}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module AttributedObjectHelpers
|
2
|
+
class TypeCoerce
|
3
|
+
def self.check_type_supported!(type_info)
|
4
|
+
supported = type_info.is_a?(Class) || [
|
5
|
+
:string,
|
6
|
+
:boolean,
|
7
|
+
:integer,
|
8
|
+
:float,
|
9
|
+
:numeric,
|
10
|
+
:symbol
|
11
|
+
].include?(type_info)
|
12
|
+
supported = type_info.is_a?(AttributedObject::Type) if !supported
|
13
|
+
raise AttributedObject::ConfigurationError.new("Unknown Type for type coercion #{type_info}") unless supported
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.coerce(type_info, value, coerce_blanks_to_nil: false)
|
17
|
+
return nil if value.nil?
|
18
|
+
return nil if coerce_blanks_to_nil && !(type_info == :string && value == '') # blank string stays blank
|
19
|
+
|
20
|
+
case type_info
|
21
|
+
when :string
|
22
|
+
return value.to_s
|
23
|
+
when :boolean
|
24
|
+
return [true, 1, 'true', '1'].include?(value)
|
25
|
+
when :integer
|
26
|
+
return value.to_i
|
27
|
+
when :float
|
28
|
+
return value.to_f
|
29
|
+
when :numeric
|
30
|
+
return (float = value.to_f) && (float % 1.0 == 0) ? float.to_i : float
|
31
|
+
when :symbol
|
32
|
+
return value.to_sym
|
33
|
+
else
|
34
|
+
if type_info.is_a?(Class) && type_info.respond_to?(:attributed_object)
|
35
|
+
return value if value.is_a?(type_info)
|
36
|
+
if !value.is_a?(Hash)
|
37
|
+
raise AttributedObject::UncoercibleValueError.new("Trying to coerce into #{type_info}, but value is not a hash, its #{value.class}")
|
38
|
+
end
|
39
|
+
return type_info.new(value)
|
40
|
+
end
|
41
|
+
if type_info.is_a?(Class)
|
42
|
+
return value if value.is_a?(type_info)
|
43
|
+
raise AttributedObject::UncoercibleValueError.new("Trying to coerce into #{type_info}, but no coercion is registered for #{type_info}->#{value.class}")
|
44
|
+
end
|
45
|
+
if type_info.is_a?(AttributedObject::Type)
|
46
|
+
return type_info.coerce(value)
|
47
|
+
end
|
48
|
+
raise AttributedObject::ConfigurationError.new("Unknown Type for type coerce #{type_info}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'attributed_object'
|
2
|
+
|
3
|
+
describe AttributedObject::Coerce do
|
4
|
+
class CoercedFoo
|
5
|
+
include AttributedObject::Coerce
|
6
|
+
attribute :a_string, :string, default: 'its a string'
|
7
|
+
attribute :a_boolean, :boolean, default: false
|
8
|
+
attribute :a_integer, :integer, default: 77
|
9
|
+
attribute :a_float, :float, default: 98.12
|
10
|
+
attribute :a_numeric, :numeric, default: 12.12
|
11
|
+
attribute :a_symbol, :symbol, default: :some_default_symbol
|
12
|
+
attribute :any_class_without_coercer, Class, default: nil
|
13
|
+
attribute :untyped, default: nil
|
14
|
+
end
|
15
|
+
|
16
|
+
class BlankCoercedFoo < CoercedFoo
|
17
|
+
attributed_object coerce_blanks_to_nil: true
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'coerces strings' do
|
21
|
+
expect(CoercedFoo.new(a_string: '12').a_string).to eq('12')
|
22
|
+
expect(CoercedFoo.new(a_string: 12).a_string).to eq('12')
|
23
|
+
|
24
|
+
expect(CoercedFoo.new(a_string: '').a_string).to eq('')
|
25
|
+
expect(CoercedFoo.new(a_string: nil).a_string).to eq(nil)
|
26
|
+
expect(BlankCoercedFoo.new(a_string: '').a_string).to eq('')
|
27
|
+
expect(BlankCoercedFoo.new(a_string: nil).a_string).to eq(nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'coerces booleans' do
|
31
|
+
expect(CoercedFoo.new(a_boolean: true).a_boolean).to eq(true)
|
32
|
+
expect(CoercedFoo.new(a_boolean: 1).a_boolean).to eq(true)
|
33
|
+
expect(CoercedFoo.new(a_boolean: 'true').a_boolean).to eq(true)
|
34
|
+
expect(CoercedFoo.new(a_boolean: '1').a_boolean).to eq(true)
|
35
|
+
expect(CoercedFoo.new(a_boolean: false).a_boolean).to eq(false)
|
36
|
+
expect(CoercedFoo.new(a_boolean: 0).a_boolean).to eq(false)
|
37
|
+
expect(CoercedFoo.new(a_boolean: 'false').a_boolean).to eq(false)
|
38
|
+
expect(CoercedFoo.new(a_boolean: '0').a_boolean).to eq(false)
|
39
|
+
|
40
|
+
expect(CoercedFoo.new(a_boolean: '').a_boolean).to eq(false)
|
41
|
+
expect(CoercedFoo.new(a_boolean: nil).a_boolean).to eq(nil)
|
42
|
+
expect(BlankCoercedFoo.new(a_boolean: '').a_boolean).to eq(nil)
|
43
|
+
expect(BlankCoercedFoo.new(a_boolean: nil).a_boolean).to eq(nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'coerces integers' do
|
47
|
+
expect(CoercedFoo.new(a_integer: 1).a_integer).to eq(1)
|
48
|
+
expect(CoercedFoo.new(a_integer: 1.1).a_integer).to eq(1)
|
49
|
+
expect(CoercedFoo.new(a_integer: '1').a_integer).to eq(1)
|
50
|
+
expect(CoercedFoo.new(a_integer: '01').a_integer).to eq(1)
|
51
|
+
expect(CoercedFoo.new(a_integer: '1.1').a_integer).to eq(1)
|
52
|
+
expect(CoercedFoo.new(a_integer: nil).a_integer).to eq(nil)
|
53
|
+
expect(BlankCoercedFoo.new(a_integer: '').a_integer).to eq(nil)
|
54
|
+
expect(BlankCoercedFoo.new(a_integer: nil).a_integer).to eq(nil)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'coerces floats' do
|
58
|
+
expect(CoercedFoo.new(a_float: 1).a_float).to eq(1.0)
|
59
|
+
expect(CoercedFoo.new(a_float: 1.1).a_float).to eq(1.1)
|
60
|
+
expect(CoercedFoo.new(a_float: '1').a_float).to eq(1.0)
|
61
|
+
expect(CoercedFoo.new(a_float: '01').a_float).to eq(1.0)
|
62
|
+
expect(CoercedFoo.new(a_float: '1.1').a_float).to eq(1.1)
|
63
|
+
expect(CoercedFoo.new(a_float: nil).a_float).to eq(nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'coerces numerics' do
|
67
|
+
expect(CoercedFoo.new(a_numeric: 1).a_numeric).to eq(1)
|
68
|
+
expect(CoercedFoo.new(a_numeric: 1.1).a_numeric).to eq(1.1)
|
69
|
+
expect(CoercedFoo.new(a_numeric: '1').a_numeric).to eq(1)
|
70
|
+
expect(CoercedFoo.new(a_numeric: '01').a_numeric).to eq(1)
|
71
|
+
expect(CoercedFoo.new(a_numeric: '1.1').a_numeric).to eq(1.1)
|
72
|
+
expect(CoercedFoo.new(a_numeric: nil).a_numeric).to eq(nil)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'coerces symbols' do
|
76
|
+
expect(CoercedFoo.new(a_symbol: :some_symbol).a_symbol).to eq(:some_symbol)
|
77
|
+
expect(CoercedFoo.new(a_symbol: 'something').a_symbol).to eq(:something)
|
78
|
+
expect(CoercedFoo.new(a_symbol: '1').a_symbol).to eq(:'1')
|
79
|
+
expect(CoercedFoo.new(a_symbol: nil).a_symbol).to eq(nil)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'does nothing without type' do
|
83
|
+
expect(CoercedFoo.new(untyped: '1').untyped).to eq('1')
|
84
|
+
expect(CoercedFoo.new(untyped: 1).untyped).to eq(1)
|
85
|
+
expect(CoercedFoo.new(untyped: nil).untyped).to eq(nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'only checks type for any class' do
|
89
|
+
expect(CoercedFoo.new(any_class_without_coercer: CoercedFoo).any_class_without_coercer).to eq(CoercedFoo)
|
90
|
+
expect{ CoercedFoo.new(any_class_without_coercer: 12) }.to raise_error(AttributedObject::UncoercibleValueError)
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'coercing into AttributedObjects' do
|
94
|
+
class Toy
|
95
|
+
include AttributedObject::Coerce
|
96
|
+
|
97
|
+
attribute :kind, :symbol
|
98
|
+
end
|
99
|
+
|
100
|
+
class Child
|
101
|
+
include AttributedObject::Coerce
|
102
|
+
|
103
|
+
attribute :name, :string
|
104
|
+
attribute :age, :integer
|
105
|
+
attribute :toys, ArrayOf(Toy)
|
106
|
+
end
|
107
|
+
|
108
|
+
class Parent
|
109
|
+
include AttributedObject::Coerce
|
110
|
+
|
111
|
+
attribute :name, :string
|
112
|
+
attribute :child, Child
|
113
|
+
attribute :config, HashOf(:symbol, :boolean)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'coerces into AttributedObjects' do
|
117
|
+
parent = Parent.new({
|
118
|
+
name: 'Peter',
|
119
|
+
config: { one: '1', two: '0' },
|
120
|
+
child: {
|
121
|
+
name: 'Zelda',
|
122
|
+
age: 12,
|
123
|
+
toys: [
|
124
|
+
{
|
125
|
+
kind: 'teddybear'
|
126
|
+
},
|
127
|
+
{
|
128
|
+
kind: 'doll'
|
129
|
+
},
|
130
|
+
]
|
131
|
+
}
|
132
|
+
})
|
133
|
+
|
134
|
+
expect(parent).to eq(Parent.new(
|
135
|
+
name: 'Peter',
|
136
|
+
config: { one: true, two: false },
|
137
|
+
child: Child.new(
|
138
|
+
name: 'Zelda',
|
139
|
+
age: 12,
|
140
|
+
toys: [
|
141
|
+
Toy.new(
|
142
|
+
kind: :teddybear
|
143
|
+
),
|
144
|
+
Toy.new(
|
145
|
+
kind: :doll
|
146
|
+
)
|
147
|
+
]
|
148
|
+
)
|
149
|
+
))
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'throws error if it can not be coerced (not a hash)' do
|
153
|
+
expect { Parent.new({
|
154
|
+
name: 'Peter',
|
155
|
+
child: 'a child'
|
156
|
+
}) }.to raise_error(AttributedObject::UncoercibleValueError)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|