simple_mapper 0.0.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,10 @@
1
+ module SimpleMapper
2
+ class Exception < ::Exception
3
+ end
4
+ class TypeConversionException < Exception
5
+ end
6
+ class InvalidTypeException < Exception
7
+ end
8
+ class InvalidPatternException < Exception
9
+ end
10
+ end
@@ -0,0 +1,181 @@
1
+ require 'test_helper'
2
+
3
+ # Change tracking is done at the attribute object level
4
+ # via SimpleMapper::Attribute methods +changed?+ and +changed!+,
5
+ # but the basic attribute object depends on the +simple_mapper_changes+
6
+ # hash attribute provided by SimpleMapper::Attributes to including classes.
7
+ #
8
+ # The unit tests mock out this interdependency, so we need this simple
9
+ # integration test to verify the basics.
10
+ class AttributeChangeTrackingTest < Test::Unit::TestCase
11
+ context 'Objects built using SimpleMapper::Attributes' do
12
+ setup do
13
+ @class = Class.new do
14
+ include SimpleMapper::Attributes
15
+ maps :a
16
+ maps :b
17
+ end
18
+ @values = {:a => 'A', :b => 'B'}
19
+ @instance = @class.new(@values.clone)
20
+ end
21
+
22
+ should 'indicate no change on attribute objects for not-newly-assigned attributes' do
23
+ assert_equal false, @class.simple_mapper.attributes[:a].changed?(@instance)
24
+ assert_equal false, @class.simple_mapper.attributes[:b].changed?(@instance)
25
+ end
26
+
27
+ should 'indicate no change on instance via :attribute_changed? for not-newly-assigned attributes' do
28
+ assert_equal false, @instance.attribute_changed?(:a)
29
+ assert_equal false, @instance.attribute_changed?(:b)
30
+ end
31
+
32
+ should 'indicate change on attribute objects for newly-assigned attributes' do
33
+ @instance.a = 'something else'
34
+ assert_equal true, @class.simple_mapper.attributes[:a].changed?(@instance)
35
+ assert_equal false, @class.simple_mapper.attributes[:b].changed?(@instance)
36
+ end
37
+
38
+ should 'indicate change on instance via :attribute_changed? for newly-assigned attributes' do
39
+ @instance.a = 'something else'
40
+ assert_equal true, @instance.attribute_changed?(:a)
41
+ assert_equal false, @instance.attribute_changed?(:b)
42
+ end
43
+
44
+ should 'indicate change on nested attribute if nested attribute has a newly-assigned member' do
45
+ @class.maps :nested do
46
+ maps :inner_a
47
+ maps :inner_b
48
+ end
49
+ assert_equal false, @instance.attribute_changed?(:nested)
50
+ @instance.nested.inner_a = 'foo'
51
+ assert_equal true, @instance.attribute_changed?(:nested)
52
+ end
53
+
54
+ context 'when converting :to_simple with :changed' do
55
+ should 'result in an empty hash if no attributes have been changed' do
56
+ assert_equal({}, @instance.to_simple(:changed => true))
57
+ end
58
+
59
+ should 'result in hash with one member if only one member was changed' do
60
+ @instance.a = new_a = 'new a'
61
+ assert_equal({:a => new_a}, @instance.to_simple(:changed => true))
62
+ end
63
+
64
+ should 'result in hash with multiple members if multiple members were changed' do
65
+ @instance.a = new_a = 'new_a'
66
+ @instance.b = new_b = 'new_b'
67
+ assert_equal({:a => new_a, :b => new_b}, @instance.to_simple(:changed => true))
68
+ end
69
+
70
+ should 'result in nested hash if the class has nested attributes' do
71
+ @class.maps :nested do
72
+ maps :inner_a
73
+ maps :inner_b
74
+ end
75
+ @instance.a = new_a = 'new_a'
76
+ @instance.nested.inner_a = new_inner_a = 'new_inner_a'
77
+ assert_equal({:a => new_a, :nested => {:inner_a => new_inner_a}}, @instance.to_simple(:changed => true))
78
+ end
79
+
80
+ context 'and a hash collection attribute' do
81
+ setup do
82
+ @collection_class = Class.new(SimpleMapper::Attribute) do
83
+ include SimpleMapper::Attribute::Collection
84
+
85
+ def prefix
86
+ '__'
87
+ end
88
+
89
+ def member_key?(key)
90
+ key.to_s[0..1] == prefix
91
+ end
92
+
93
+ def from_simple_key(key)
94
+ key.to_s.sub(prefix, '').to_sym
95
+ end
96
+
97
+ def to_simple_key(key)
98
+ (prefix + key.to_s).to_sym
99
+ end
100
+ end
101
+
102
+ @class.maps :collection, :attribute_class => @collection_class
103
+ @instance = @class.new(@values.clone)
104
+ end
105
+
106
+ should 'have no output for the collection if no members were changed' do
107
+ @instance.a = new_a = 'new_a'
108
+ assert_equal({:a => new_a}, @instance.to_simple(:changed => true))
109
+ end
110
+
111
+ should 'have output for items added to the collection' do
112
+ @instance.collection[:first] = 'first'
113
+ @instance.collection[:second] = 'second'
114
+ @instance.collection[:third] = 'third'
115
+ assert_equal(
116
+ {
117
+ :__first => 'first',
118
+ :__second => 'second',
119
+ :__third => 'third',
120
+ },
121
+ @instance.to_simple(:changed => true)
122
+ )
123
+ end
124
+
125
+ should 'have output for items altered in the collection' do
126
+ @instance = @class.new(:__first => 'first', :__second => 'second', :__third => 'third')
127
+ @instance.collection[:second] = 'new second'
128
+ @instance.collection.delete(:third)
129
+ @instance.collection[:fourth] = 'fourth'
130
+ assert_equal(
131
+ {:__second => 'new second',
132
+ :__third => nil,
133
+ :__fourth => 'fourth'},
134
+ @instance.to_simple(:changed => true)
135
+ )
136
+ end
137
+
138
+ context 'with mapped members' do
139
+ setup do
140
+ @mapped_collection_class = Class.new(@collection_class) do
141
+ def prefix
142
+ '--'
143
+ end
144
+ end
145
+ @class.maps :mapped_collection, :attribute_class => @mapped_collection_class do
146
+ maps :name
147
+ maps :id, :type => :simple_uuid, :default => :from_type
148
+ end
149
+ end
150
+
151
+ should 'have output for items added to collection' do
152
+ @instance.mapped_collection[:me] = @instance.mapped_collection.build(:name => 'me')
153
+ @instance.mapped_collection[:you] = @instance.mapped_collection.build(:name => 'you')
154
+ expected = {
155
+ :'--me' => {:name => 'me', :id => @instance.mapped_collection[:me].id},
156
+ :'--you' => {:name => 'you', :id => @instance.mapped_collection[:you].id},
157
+ }
158
+ assert_equal expected, @instance.to_simple(:changed => true)
159
+ end
160
+
161
+ should 'have output for items removed from collection' do
162
+ @instance = @class.new(:'--me' => {:name => 'me'}, :'--you' => {:name => 'you'})
163
+ @instance.mapped_collection.delete(:you)
164
+ assert_equal({:'--you' => nil}, @instance.to_simple(:changed => true))
165
+ end
166
+
167
+ should 'have output for all attributes in replaced member' do
168
+ @instance = @class.new(:'--me' => {:name => 'me'})
169
+ @instance.mapped_collection[:me] = @instance.mapped_collection.build
170
+ assert_equal({:'--me' => {:name => nil, :id => @instance.mapped_collection[:me].id}}, @instance.to_simple(:changed => true))
171
+ end
172
+ end
173
+ end
174
+
175
+ context 'and an array collection attribute' do
176
+ setup do
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,169 @@
1
+ require 'test_helper'
2
+
3
+ class AttributeMetaInteractionTest < Test::Unit::TestCase
4
+ context 'Object attribute / meta attribute method' do
5
+ setup do
6
+ @class = Class.new do
7
+ include SimpleMapper::Attributes
8
+ maps :foo
9
+ end
10
+ end
11
+
12
+ context 'read_source_attribute' do
13
+ setup do
14
+ @instance = @class.new(:foo => 'Foo!')
15
+ end
16
+
17
+ should 'return the attribute specified from the source hash' do
18
+ assert_equal 'Foo!', @instance.read_source_attribute(:foo)
19
+ end
20
+
21
+ should 'return the attribute from source even if updated locally' do
22
+ @instance.write_attribute(:foo, 'Blah!')
23
+ assert_equal 'Blah!', @instance.read_attribute(:foo)
24
+ assert_equal 'Foo!', @instance.read_source_attribute(:foo)
25
+ end
26
+
27
+ should 'return the source attribute via string key if symbol key does not exist' do
28
+ @class.maps :some_attr
29
+ @instance = @class.new('foo' => 'Foo!', :some_attr => 'Some Attr')
30
+ assert_equal 'Foo!', @instance.read_attribute(:foo)
31
+ assert_equal 'Some Attr', @instance.read_attribute(:some_attr)
32
+ end
33
+ end
34
+
35
+ context 'read_attribute' do
36
+ setup do
37
+ @instance = @class.new(:foo => 'Foo!', :some_attr => 'Some Attr')
38
+ end
39
+
40
+ should 'return the source attribute by default' do
41
+ assert_equal 'Foo!', @instance.read_attribute(:foo)
42
+ end
43
+
44
+ should 'return the updated attribute if one exists' do
45
+ @instance.write_attribute(:foo, 'Blah!')
46
+ assert_equal 'Blah!', @instance.read_attribute(:foo)
47
+ end
48
+
49
+ should 'return the attribute default if source attribute is nil' do
50
+ @instance = @class.new
51
+ @instance.attribute_object_for(:foo).expects(:default_value).with(@instance).returns('foo default')
52
+ result = @instance.read_attribute(:foo)
53
+ assert_equal 'foo default', result
54
+ end
55
+
56
+ context 'with a type converter' do
57
+ setup do
58
+ @expected_out = '_foo_'
59
+ @expected_in = 'Foo!'
60
+ @type_a = mock('type_a')
61
+ @type_a.expects(:decode).with(@expected_in).returns(@expected_out)
62
+ @class.simple_mapper.attributes[:foo].type = @type_a
63
+ end
64
+
65
+ should 'transform the source attribute' do
66
+ assert_equal @expected_out, @instance.read_attribute(:foo)
67
+ end
68
+
69
+ should 'transform the default value if no source value is defined' do
70
+ @instance = @class.new
71
+ @instance.attribute_object_for(:foo).default = :get_default
72
+ @instance.stubs(:get_default).returns(@expected_in)
73
+ assert_equal @expected_out, @instance.read_attribute(:foo)
74
+ end
75
+ end
76
+
77
+ context 'with a type name' do
78
+ setup do
79
+ foo_type = mock('foo_type')
80
+ foo_class = foo_type.class
81
+ @expected_in = 'Foo!'
82
+ @expected_out = 'Foo on Ewe!'
83
+ foo_type.expects(:decode).once.with(@expected_in).returns(@expected_out)
84
+ SimpleMapper::Attributes.stubs(:types).with.returns({:foo_type => {
85
+ :name => :foo_type,
86
+ :expected_type => foo_class,
87
+ :converter => foo_type,
88
+ }})
89
+ @class.simple_mapper.attributes[:foo].type = :foo_type
90
+ end
91
+
92
+ should 'transform the source attribute if a type name was specified' do
93
+ assert_equal @expected_out, @instance.read_attribute(:foo)
94
+ end
95
+
96
+ should 'transform the default value if the source is nil' do
97
+ @instance = @class.new
98
+ @instance.attribute_object_for(:foo).default = :get_default
99
+ @instance.stubs(:get_default).returns(@expected_in)
100
+ assert_equal @expected_out, @instance.read_attribute(:foo)
101
+ end
102
+ end
103
+
104
+ should 'not transform the source attr if it is already of the expected type' do
105
+ foo_type = mock('foo too type')
106
+ foo_type.expects(:decode).never
107
+ SimpleMapper::Attributes.expects(:type_for).with(:foo_type).returns({
108
+ :name => :foo_type,
109
+ :expected_type => String,
110
+ :converter => foo_type,
111
+ })
112
+ @class.simple_mapper.attributes[:foo].type = :foo_type
113
+ assert_equal 'Foo!', @instance.read_attribute(:foo)
114
+ end
115
+
116
+ should 'not transform a written attribute when type was specified' do
117
+ type_a = mock('type_a')
118
+ type_a.expects(:decode).never
119
+ @class.simple_mapper.attributes[:foo].type = type_a
120
+ @instance.write_attribute(:foo, 'blahblah')
121
+ assert_equal 'blahblah', @instance.read_attribute(:foo)
122
+ end
123
+ end
124
+
125
+ context 'get_attribute_default' do
126
+ setup do
127
+ @instance = @class.new(:foo => 'foo')
128
+ end
129
+
130
+ should 'return nil if the attribute has no default specified' do
131
+ @class.maps :without_default
132
+ assert_equal nil, @instance.get_attribute_default(:without_default)
133
+ end
134
+
135
+ should 'invoke specified default symbol on instance if attr has default specified' do
136
+ @class.maps :with_default, :default => :some_default
137
+ @instance.expects(:some_default).once.with.returns('the default value')
138
+ assert_equal 'the default value', @instance.get_attribute_default(:with_default)
139
+ end
140
+
141
+ context 'with :default of :from_type' do
142
+ setup do
143
+ @expected_val = 'some default value'
144
+ @name = :with_default
145
+ @type = stub('type', :name => :type_name, :decode => :foo, :encode => :foo2)
146
+ @type.expects(:default).once.with.returns(@expected_val)
147
+ end
148
+
149
+ should 'invoke :default on type converter if default is :from_type and :type is object' do
150
+ @class.maps :with_default, :type => @type, :default => :from_type
151
+ assert_equal @expected_val, @instance.get_attribute_default(:with_default)
152
+ end
153
+
154
+ should 'invoke :default on registered type if default is :from_type and :type is registered' do
155
+ begin
156
+ SimpleMapper::Attributes.stubs(:types).with.returns({@name => {
157
+ :name => @name,
158
+ :expected_class => @type.class,
159
+ :converter => @type}})
160
+ @class.maps :with_default, :type => @name, :default => :from_type
161
+ assert_equal @expected_val, @instance.get_attribute_default(:with_default)
162
+ ensure
163
+ SimpleMapper::Attributes.types.delete @type.name
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,77 @@
1
+ require 'test_helper'
2
+
3
+ # basic end-to-end tests for the SimpleMapper::Attribute::Pattern attribute class.
4
+ class AttributePatternIntegrationTest < Test::Unit::TestCase
5
+ context 'A SimpleMapper::Attribute::Pattern attribute' do
6
+ setup do
7
+ @target_class = SimpleMapper::Attribute::Pattern
8
+ @class = Class.new do
9
+ include SimpleMapper::Attributes
10
+ end
11
+ end
12
+
13
+ context 'with simple values' do
14
+ setup do
15
+ @class.maps :simple, :attribute_class => @target_class, :pattern => /^simple_/
16
+ @class.maps :float, :attribute_class => @target_class, :pattern => /^float_/, :type => :float
17
+ @simple = {:simple_a => 'a', :simple_b => 'b', :simple_c => 'c'}
18
+ @float = {:float_a => 1.0, :float_b => 100.1, :float_c => 99.99999}
19
+ @object = @class.new( @simple.merge( @float.inject({}) {|h, kv| h[kv[0]] = kv[1].to_s; h} ) ) end
20
+
21
+ should 'map untyped pairs to the patterned collection attribute' do
22
+ assert_equal @simple, @object.simple
23
+ end
24
+
25
+ should 'map typed pairs to the patterned collection attribute' do
26
+ assert_equal @float, @object.float
27
+ end
28
+
29
+ should 'map untyped instance values back to simple structure using to_simple' do
30
+ result = @object.to_simple || {}
31
+ assert_equal @simple, result.reject {|k, v| ! /^simple_/.match(k.to_s)}
32
+ end
33
+
34
+ should 'map typed instance values back to simple structure using to_simple' do
35
+ result = @object.to_simple || {}
36
+ assert_equal(@float.inject({}) {|h, pair| h[pair[0]] = pair[1].to_s; h},
37
+ result.reject {|k, v| ! /^float_/.match(k.to_s)})
38
+ end
39
+ end
40
+
41
+ context 'with a nested mapper' do
42
+ setup do
43
+ @class.maps :not_nested
44
+ @class.maps :nested, :attribute_class => @target_class, :pattern => /^nested_/ do
45
+ maps :foo
46
+ maps :bar
47
+ end
48
+
49
+ @input = {:nested_a => {:foo => 'foo_a', :bar => 'bar_a'},
50
+ :nested_b => {:foo => 'foo_b', :bar => 'bar_b'},
51
+ :nested_c => {:foo => 'foo_c', :bar => 'bar_c'},
52
+ :not_nested => 'blah'}
53
+ @object = @class.new(@input)
54
+ end
55
+
56
+ should 'convert nested source values to type defined by block per item in patterned collection attribute' do
57
+ result = @object.nested.inject({}) do |hash, keyval|
58
+ hash[keyval[0]] = {:foo => keyval[1].foo, :bar => keyval[1].bar}
59
+ hash
60
+ end
61
+ expected = @input.clone
62
+ expected.delete :not_nested
63
+ assert_equal expected, result
64
+ end
65
+
66
+ should 'still map simple attributes too' do
67
+ assert_equal @input[:not_nested], @object.not_nested.to_s
68
+ end
69
+
70
+ should 'map nested objects back to nested simple structures with to_simple' do
71
+ expected = @input.reject {|k, v| ! /^nested_/.match(k.to_s)}
72
+ result = @object.to_simple || {}
73
+ assert_equal(expected, result.reject {|k, v| ! /^nested/.match(k.to_s)})
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,128 @@
1
+ require 'test_helper'
2
+
3
+ # The :to_simple method of the SimpleMapper::Attributes
4
+ # module ultimately depends on the :to_simple
5
+ # implementation of the attribute objects for a given
6
+ # mapper class.
7
+ #
8
+ # Consequently, the integration test is necessary,
9
+ # as other :to_simple tests at the unit level rely on
10
+ # mocks for this interdependency.
11
+ class ToSimpleTest < Test::Unit::TestCase
12
+ context "The mapper's :to_simple method" do
13
+ setup do
14
+ @class = Class.new do
15
+ include SimpleMapper::Attributes
16
+ maps :no_type
17
+ end
18
+ end
19
+
20
+ should 'return a hash with key/value per known attribute' do
21
+ @class.maps :other
22
+ @instance = @class.new( :no_type => 'no type', :other => 'Other!' )
23
+ assert_equal( {:no_type => 'no type', :other => 'Other!'}, @instance.to_simple )
24
+ end
25
+
26
+ should 'return hash whose key/value pairs follow attribute state' do
27
+ @class.maps :other
28
+ @instance = @class.new( :no_type => 'original', :other => 'original' )
29
+ @instance.other = 'updated'
30
+ assert_equal({:no_type => 'original', :other => 'updated'}, @instance.to_simple)
31
+ end
32
+
33
+ should 'map attributes to specified keys in returned hash' do
34
+ @class.maps :other, :key => :alt_key
35
+ @instance = @class.new
36
+ @instance.no_type = 'no type'
37
+ @instance.other = 'alt key'
38
+ assert_equal({:no_type => 'no type', :alt_key => 'alt key'}, @instance.to_simple)
39
+ end
40
+
41
+ should 'return a hash with strings for keys when :string_keys option is true' do
42
+ @class.maps :other, :key => :alt_key
43
+ @instance = @class.new
44
+ @instance.no_type = 'no type'
45
+ @instance.other = 'alt key'
46
+ assert_equal({'no_type' => 'no type', 'alt_key' => 'alt key'},
47
+ @instance.to_simple(:string_keys => true))
48
+ end
49
+
50
+ should 'encode typed attribute values for converter types' do
51
+ @type = Object.new
52
+ @type.instance_eval do
53
+ def encode(value)
54
+ "encoded: #{value}"
55
+ end
56
+ def decode(value)
57
+ value
58
+ end
59
+ end
60
+ @class.maps :typed, :type => @type
61
+ @instance = @class.new( :typed => 'typed!' )
62
+ assert_equal 'encoded: typed!', @instance.to_simple[:typed]
63
+ end
64
+
65
+ should 'encode typed attribute values for registered types' do
66
+ @type = mock('type')
67
+ @type.expects(:encode).once.with('typed!').returns('encoded!')
68
+ @type.stubs(:decode).returns('typed!')
69
+ SimpleMapper::Attributes.expects(:type_for).with(:type).at_least_once.returns({
70
+ :name => :type,
71
+ :expected_type => @type.class,
72
+ :converter => @type,
73
+ })
74
+ @class.maps :typed, :type => :type
75
+ @instance = @class.new( :typed => 'typed!' )
76
+ assert_equal 'encoded!', @instance.to_simple[:typed]
77
+ end
78
+
79
+ context 'with :defined argument' do
80
+ should 'return a hash with key/value per defined attribute' do
81
+ @class.maps :undefined
82
+ assert_equal({:no_type => 'nt'}, @class.new({:no_type => 'nt'}).to_simple(:defined => true))
83
+ end
84
+
85
+ should 'return an empty hash if no attributes were set' do
86
+ assert_equal({}, @class.new.to_simple(:defined => true))
87
+ end
88
+ end
89
+
90
+ context 'with :changes argument' do
91
+ setup do
92
+ @class.maps :other
93
+ @instance = @class.new({:typed => 'typed', :other => 'other'})
94
+ end
95
+
96
+ should 'return an empty hash if no attributes were changed' do
97
+ assert_equal({}, @instance.to_simple(:changed => true))
98
+ end
99
+
100
+ should 'return a hash of only the key/value pairs that were changed' do
101
+ @instance.other = 'udder'
102
+ @instance.class.simple_mapper.attributes[:other].stubs(:changed?).with(@instance).returns(true)
103
+ assert_equal({:other => 'udder'}, @instance.to_simple(:changed => true))
104
+ end
105
+ end
106
+
107
+ context 'with an attribute value that uses a mapper' do
108
+ setup do
109
+ @class.maps :other do; end
110
+ @instance = @class.new({:no_type => 'no type',
111
+ :other => {}})
112
+ @mock = @instance.other
113
+ end
114
+
115
+ should 'invoke to_simple on attribute value rather than encode' do
116
+ @mock.expects(:to_simple).returns('to simple')
117
+ assert_equal({:no_type => 'no type', :other => 'to simple'},
118
+ @instance.to_simple)
119
+ end
120
+
121
+ should 'pass outer to_simple arguments along to inner to_simple' do
122
+ options = {:foo => 'floppy', :foot => 'fungi'}
123
+ @mock.expects(:to_simple).with(options).returns(nil)
124
+ @instance.to_simple(options)
125
+ end
126
+ end
127
+ end
128
+ end