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.
@@ -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,11 @@
1
+ module AttributedObject
2
+ class Type
3
+ def strict_check(value)
4
+ raise 'implement me'
5
+ end
6
+
7
+ def coerce(value)
8
+ raise 'implement me'
9
+ end
10
+ end
11
+ end
@@ -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,3 @@
1
+ module AttributedObject
2
+ VERSION = "0.2.1"
3
+ 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