tableless_model 0.0.7 → 0.0.8

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.
@@ -1,3 +1,3 @@
1
1
  module TablelessModel
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require "active_record"
5
+ require "timecop"
6
+ require "tableless_model"
7
+
@@ -1,246 +1,256 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
1
+ require "spec_helper"
2
2
 
3
- require "active_record"
4
- require 'minitest/autorun'
3
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
4
+ ActiveRecord::Base.connection.execute(" create table test_models (id integer, options varchar(50)) ")
5
5
 
6
- require File.expand_path(File.join(File.dirname(__FILE__), "../lib/tableless_model"))
7
-
8
-
9
- describe "A class inheriting from ActiveRecord::TablelessModel" do
10
- before do
11
- class TestClass1 < ActiveRecord::TablelessModel
12
- end
13
- end
14
-
15
- it "has the accessor 'attributes', which is originally an empty hash" do
16
- TestClass1.must_respond_to("attributes")
17
- TestClass1.attributes.must_be_kind_of Hash
18
- TestClass1.attributes.must_be_empty
19
- end
20
-
21
- it "responds to 'attribute', 'cast'" do
22
- ["attribute", "cast"].each {|method_name| TestClass1.must_respond_to method_name }
23
- end
6
+ class ModelOptions < ActiveRecord::TablelessModel
7
+ attribute :no_default_value_no_type_attribute
8
+ attribute :no_default_value_typed_attribute, :type => :integer
9
+ attribute :no_type_attribute, :default => 111
10
+ attribute :typed_attribute, :default => 5, :type => :integer
11
+ attribute :typed_attribute_no_default_value, :type => :integer
12
+ attribute :attribute_with_proc_default_value, :type => :time, :default => lambda { Time.now }
24
13
  end
25
14
 
26
- describe "The 'attribute' macro" do
27
- before do
28
- class TestClass < ActiveRecord::TablelessModel
29
- attribute :test_attribute
30
- end
31
- end
32
-
33
- it "adds a key-value pair to the attributes accessor" do
34
- TestClass.attributes.wont_be_empty
35
- TestClass.attributes.key?("test_attribute").must_equal true, "An attribute named 'test_attribute' should have been defined"
36
- end
15
+ class TestModel < ActiveRecord::Base
16
+ has_tableless :options => ModelOptions
37
17
  end
38
18
 
39
- describe "An instance of TablelessModel" do
40
- before do
41
- class TestClass2 < ActiveRecord::TablelessModel
42
- attribute :test_attribute
43
- attribute :test_attribute_with_default_value, :default => "default value"
44
- attribute :test_attribute_with_type_and_default_value, :default => "003xxx", :type => :integer
45
- end
46
-
47
- @instance = TestClass2.new
48
- end
49
-
50
- it "has a getter and a setter for each defined attribute" do
51
- [:test_attribute, :test_attribute_with_default_value, :test_attribute_with_type_and_default_value].each do |attribute_name|
52
- @instance.must_respond_to attribute_name, "Getter for #{attribute_name} should have been defined"
53
- @instance.must_respond_to "#{attribute_name}=", "Setter for #{attribute_name} should have been defined"
54
- end
55
- end
56
-
57
- it "assigns the default value to an attribute that has not yet been set, if a default value has been specified" do
58
- @instance.test_attribute_with_default_value.must_equal "default value"
59
- @instance.test_attribute_with_type_and_default_value.must_equal 3
60
- end
61
-
62
- it "should allow overriding the default values" do
63
- instance = TestClass2.new( :test_attribute_with_default_value => "changed value" )
64
- instance.test_attribute_with_default_value.must_equal "changed value"
19
+ describe TestModel do
20
+ let(:test_model) { subject }
21
+ let(:options) { test_model.options }
22
+ let(:frozen_time) { Time.local(2011, 7, 24, 18, 47, 0).in_time_zone("Europe/London") }
23
+ let(:test_values) do
24
+ {
25
+ :no_default_value_no_type_attribute => "no_default_value_no_type_attribute",
26
+ :no_default_value_typed_attribute => 4567,
27
+ :no_type_attribute => "no_type_attribute",
28
+ :typed_attribute => 8765,
29
+ :typed_attribute_no_default_value => 34,
30
+ :attribute_with_proc_default_value => frozen_time
31
+ }
65
32
  end
66
33
 
67
- it "assumes an attribute's data type is string if the type has not been specified" do
68
- @instance.test_attribute.must_be_kind_of String
69
- end
70
-
71
- it "assigns the expected not-nil value to an attribute if a default value hasn't been specified" do
72
- @instance.test_attribute.must_equal ""
34
+ it "is not in a changed state if no properties have changed" do
35
+ test_model.changed?.should == false
36
+ test_model.changes.should == {}
73
37
  end
74
38
 
75
- it "does not allow access to undefined attributes" do
76
- @instance.wont_respond_to "unknown_attribute"
77
- @instance.wont_respond_to "unknown_attribute="
39
+ context "when setting all (or more than one of) the tableless model's attributes at once" do
40
+ before(:each) do
41
+ test_model.options = test_values
42
+ end
78
43
 
79
- proc { @instance["unknown_attribute"] }.must_raise(NoMethodError)
80
- proc { @instance["unknown_attribute="] }.must_raise(NoMethodError)
81
- end
82
-
83
- it "shows the expected output on inspect" do
84
- @instance.inspect.must_equal "<#TestClass2 test_attribute=\"\" test_attribute_with_default_value=\"default value\" test_attribute_with_type_and_default_value=3>"
85
- end
86
-
87
-
88
- it "should not allow merging" do
89
- proc { @instance.merge(:new_symbol_key => "new_symbol_key") }.must_raise NoMethodError
90
- end
91
-
92
- end
93
-
94
-
95
- describe "An instance of TablelessModel" do
96
- before do
97
- class TestClass3 < ActiveRecord::TablelessModel
98
- attribute :typed_test_attribute, :type => :integer
44
+ after(:each) do
45
+ TestModel.delete_all
99
46
  end
100
- end
101
-
102
- describe "instance" do
103
- before do
104
- @instance = TestClass3.new
47
+
48
+ it "the attributes have the expected values" do
49
+ test_model.options.no_default_value_no_type_attribute.should == "no_default_value_no_type_attribute"
50
+ test_model.options.no_default_value_typed_attribute.should == 4567
51
+ test_model.options.no_type_attribute.should == "no_type_attribute"
52
+ test_model.options.typed_attribute.should == 8765
53
+ test_model.options.typed_attribute_no_default_value.should == 34
54
+ test_model.options.attribute_with_proc_default_value.should == frozen_time
105
55
  end
106
56
 
107
- it "has rw the accessor __owner_object" do
108
- @instance.must_respond_to "__owner_object"
109
- @instance.must_respond_to "__owner_object="
57
+ it "forces the owner model to a changed state with partial_updates on" do
58
+ test_model.changed?.should == true
59
+ test_model.changes.keys.should include "options"
60
+ test_model.changes[:options][0].should be_nil
61
+ test_model.changes[:options][1].should == test_values
110
62
  end
63
+
64
+ context "and saving the parent model" do
65
+ before(:each) do
66
+ test_model.save!
67
+ end
68
+
69
+ it "correctly initialises the attributes with their expected values when reading from database" do
70
+ # Not sure if these are really needed, but I am leaving them just in case Identity Map
71
+ # prevents the record from being actually loaded from the database for this test.
72
+ options = nil
73
+ test_model = nil
74
+
75
+ instance = TestModel.first
111
76
 
112
- it "has rw the accessor __serialized_attribute" do
113
- @instance.must_respond_to "__serialized_attribute"
114
- @instance.must_respond_to "__serialized_attribute="
77
+ instance.options.no_default_value_no_type_attribute.should == "no_default_value_no_type_attribute"
78
+ instance.options.no_default_value_typed_attribute.should == 4567
79
+ instance.options.no_type_attribute.should == "no_type_attribute"
80
+ instance.options.typed_attribute.should == 8765
81
+ instance.options.typed_attribute_no_default_value.should == 34
82
+ instance.options.attribute_with_proc_default_value.should == frozen_time
83
+ end
115
84
  end
116
85
  end
117
-
118
- it "tries to enforce type casting if a type has been specified for an attribute" do
119
86
 
120
- test_values = [ "test", 1234, true, "1234.12", "2011-01-02 15:23" ]
87
+ context "when receives a message with the name of an attribute in the tableless model" do
88
+ before(:each) do
89
+ test_model.options = test_values
90
+ end
121
91
 
122
- [ :string, :integer, :float, :decimal, :time, :date, :datetime, :boolean ].each do |type|
123
-
124
- # temporarily changing type
125
- TestClass3.attributes["typed_test_attribute"][:type] = type
126
-
127
- instance = TestClass3.new
128
-
129
- type_name = case type
130
- when :datetime then :date_time
131
- when :decimal then :big_decimal
132
- else type
133
- end
134
-
135
-
136
- # excluding some test values that would always fail depending on the type
137
- exclude_test_values = case type
138
- when :decimal then [ "test", true ]
139
- when :time then [ "test", 1234, true ]
140
- when :date then [ "test", 1234, true, "1234.12" ]
141
- when :datetime then [ "test", 1234, true ]
142
- else []
92
+ context "and it doesn't have a method of its own by that name" do
93
+ it "delegates the method call to the tableless model" do
94
+ test_values.each do |k, v|
95
+ test_model.send(k).should == v
96
+ end
143
97
  end
98
+
99
+ it "delegates setters too" do
100
+ instance = TestModel.new
144
101
 
145
- (test_values - exclude_test_values).each do |value|
146
- instance.typed_test_attribute = value
147
-
148
- if type == :boolean
149
- [true, false].include?(instance.typed_test_attribute).must_equal true, "Expected #{instance.typed_test_attribute.inspect} to be boolean, not #{type.class}"
150
- else
151
- instance.typed_test_attribute.must_be_kind_of type_name.to_s.classify.constantize
102
+ test_values.each do |k, v|
103
+ instance.send("#{k}=", v)
104
+ instance.options.send(k).should == v
152
105
  end
153
106
  end
154
107
 
108
+ it "can delegate calls to attribute_name?" do
109
+ test_values.each do |k, v|
110
+ test_model.send("#{k}?").should == !!v
111
+ end
112
+ end
155
113
  end
156
114
  end
157
- end
158
-
159
- describe "An ActiveRecord::Base model" do
160
- before do
161
- class ModelOptions < ActiveRecord::TablelessModel
162
- attribute :aaa, :default => 111
163
- attribute :bbb, :default => "bbb"
115
+
116
+ describe "#options" do
117
+ it "is an instance of the tableless model" do
118
+ options.should be_instance_of ModelOptions
164
119
  end
165
-
166
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
167
- ActiveRecord::Base.connection.execute(" create table test_models (options varchar(50)) ")
168
-
169
- class TestModel < ActiveRecord::Base
170
- has_tableless :options => ModelOptions
120
+
121
+ it "sets the attributes to the expected default values" do
122
+ Timecop.freeze(frozen_time) do
123
+ test_model.options.no_default_value_no_type_attribute.should == ""
124
+ test_model.options.no_default_value_typed_attribute.should == 0
125
+ test_model.options.no_type_attribute.should == "111"
126
+ test_model.options.typed_attribute.should == 5
127
+ test_model.options.typed_attribute_no_default_value.should == 0
128
+ test_model.options.attribute_with_proc_default_value.should == frozen_time
129
+ end
130
+ end
131
+
132
+ it "honours the type of the attribute, when specified" do
133
+ options.typed_attribute.should == 5
171
134
  end
172
- end
173
135
 
174
- it "responds to default_value_for, has_tableless" do
175
- TestModel.must_respond_to(:has_tableless)
176
- end
177
-
178
-
179
- describe "instance" do
180
- before do
181
- @instance = TestModel.new
136
+ it "assumes the default type of an attribute is :string, if no type has been specified" do
137
+ options.no_type_attribute.should == "111"
182
138
  end
183
139
 
184
- it "must respond to changed?" do
185
- @instance.must_respond_to "changed?"
186
- @instance.changed?.must_equal false
187
- @instance.changes.must_equal({})
140
+ it "assigns a non-nil string value to an attribute with no type nor default value" do
141
+ options.no_default_value_no_type_attribute.should == ""
142
+ end
143
+
144
+ it "assigns a non-nil value of the expected type to an attribute with type but no default value" do
145
+ options.no_default_value_typed_attribute.should == 0
188
146
  end
189
147
 
190
- it "sets the accessor __owner_object to self in the tableless model instance" do
191
- @instance.options.__owner_object.must_equal @instance
148
+ it "allows setting the value of a known attribute" do
149
+ expect {
150
+ options.typed_attribute = 1234
151
+ test_model.options.typed_attribute.should == 1234
152
+ }.to_not raise_error
192
153
  end
193
154
 
194
- it "sets the accessor __serialized_attribute to the name of its column that stored the tableless model instance, serialized" do
195
- @instance.options.__serialized_attribute.must_equal :options
155
+ it "doesn't recognise undefined attributes" do
156
+ expect {
157
+ options.unknown_attribute
158
+ }.to raise_error NoMethodError
196
159
  end
197
160
 
161
+ it "doesn't allow setting undefined attributes" do
162
+ expect {
163
+ options.unknown_attribute = "whatever"
164
+ }.to raise_error NoMethodError
165
+ end
198
166
 
199
- it "has a getter and a setter for :options" do
200
- %w(options options=).each{|m| @instance.must_respond_to m }
201
- @instance.options.must_be_kind_of ModelOptions
202
- @instance.options.wont_be_nil
203
- @instance.options.aaa.must_equal "111"
204
- @instance.options.bbb.must_equal "bbb"
205
- proc { @instance.options = "test" }.must_raise NoMethodError, "should not accept other values than a hash or ModelOptions instance"
167
+ it "does not allow merging, since the tableless mode is supposed to be used like a normal model, not a hash" do
168
+ expect {
169
+ options.merge(:some_new_attribute => "whatever")
170
+ }.should raise_error NoMethodError
206
171
  end
207
-
208
- describe "setter" do
209
- before do
210
- @return_value = @instance.send("options=", { :aaa => "CCC", :bbb => "DDD" })
172
+
173
+ it "shows the expected object-like output on inspect" do
174
+ Timecop.freeze(frozen_time) do
175
+ instance = TestModel.new
176
+ instance.options.inspect.should == "<#ModelOptions attribute_with_proc_default_value=2011-07-24 18:47:00 BST no_default_value_no_type_attribute=\"\" no_default_value_typed_attribute=0 no_type_attribute=\"111\" typed_attribute=5 typed_attribute_no_default_value=0>"
211
177
  end
212
-
213
- it "correctly sets the serialized column" do
214
- @return_value.must_be_kind_of ModelOptions
215
- %w(aaa bbb).each{|m| @return_value.must_respond_to m}
216
- @instance.options.aaa.must_equal "CCC"
217
- @instance.options.bbb.must_equal "DDD"
178
+ end
179
+
180
+ it "tries to enforce type casting if a type has been specified for an attribute" do
181
+ test_values = [ "test", 1234, true, "1234.12", "2011-01-02 15:23" ]
182
+
183
+ [ :string, :integer, :float, :decimal, :time, :date, :datetime, :boolean ].each do |type|
184
+
185
+ # temporarily changing type
186
+ ModelOptions.attributes[:typed_attribute_no_default_value][:type] = type
187
+
188
+ instance = ModelOptions.new
189
+
190
+ type_name = case type
191
+ when :datetime then :date_time
192
+ when :decimal then :big_decimal
193
+ else type
194
+ end
195
+
196
+
197
+ # excluding some test values that would always fail depending on the type
198
+ exclude_test_values = case type
199
+ when :decimal then [ "test", true ]
200
+ when :time then [ "test", 1234, true ]
201
+ when :date then [ "test", 1234, true, "1234.12" ]
202
+ when :datetime then [ "test", 1234, true ]
203
+ else []
204
+ end
205
+
206
+ (test_values - exclude_test_values).each do |value|
207
+ instance.typed_attribute_no_default_value = value
208
+
209
+ if type == :boolean
210
+ [true, false].include?(instance.typed_attribute_no_default_value).should == true
211
+ else
212
+ instance.typed_attribute_no_default_value.should be_kind_of type_name.to_s.classify.constantize
213
+ end
214
+ end
218
215
  end
219
216
 
220
- it "forces the owner object to a changed state with partial_updates on" do
221
- @instance.options.aaa = "changed aaa"
222
- @instance.options.bbb = "changed bbb"
217
+ # restoring original type
218
+ ModelOptions.attributes[:typed_attribute_no_default_value][:type] = :integer
219
+ end
223
220
 
224
- @instance.options.aaa.must_equal "changed aaa"
225
- @instance.changes.keys.include?("options").must_equal true
221
+ it "remembers who is the owner model, so that it can be forced to a changed state when any attributes change" do
222
+ options.__owner_object.should == test_model
223
+ options.__serialized_attribute.should == :options
224
+ end
226
225
 
227
- @instance.changes[:options][0].must_equal nil
228
- @instance.changes[:options][1].must_equal({"aaa"=>"changed aaa", "bbb"=>"changed bbb"})
226
+ context "when a proc is passed to define the default value of an attribute" do
227
+ context "and a new instance of the parent model is being created" do
228
+ it "allows passing a Proc/lambda to define a default value at runtime" do
229
+ Timecop.freeze(frozen_time) do
230
+ TestModel.new.options.attribute_with_proc_default_value.should == frozen_time
231
+ end
232
+ end
229
233
  end
230
-
231
- it "should save the serialized column correctly" do
232
- @instance.save!
233
234
 
234
- instance = TestModel.first
235
+ context "and an existing instance of the parent model is read from database" do
236
+ let(:some_other_time) { Time.local(2011, 6, 30, 15, 22, 0).in_time_zone("Europe/London") }
235
237
 
236
- instance.options.aaa.must_equal "CCC"
237
- instance.options.bbb.must_equal "DDD"
238
+ before(:each) do
239
+ Timecop.freeze(frozen_time) { TestModel.create({ :options => test_values }) }
240
+ end
238
241
 
239
- # Ensuring the serialize macro is used
240
- instance.options.must_be_kind_of ModelOptions
242
+ after(:each) do
243
+ TestModel.delete_all
244
+ end
245
+
246
+ it "does not execute the proc, but sets the attribute to the expected value from database" do
247
+ Timecop.freeze(some_other_time) do
248
+ TestModel.first.options.attribute_with_proc_default_value.should == frozen_time
249
+ end
250
+ end
241
251
  end
242
252
  end
253
+
243
254
  end
244
- end
245
255
 
246
-
256
+ end