simple_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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