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.
- data/.rspec +2 -0
- data/Gemfile.lock +29 -11
- data/README.md +274 -0
- data/lib/activerecord/base/class_methods.rb +8 -1
- data/lib/activerecord/base/instance_methods.rb +15 -0
- data/lib/tableless_model.rb +7 -2
- data/lib/tableless_model/class_methods.rb +4 -5
- data/lib/tableless_model/instance_methods.rb +29 -7
- data/lib/tableless_model/version.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- data/spec/tableless_model_spec.rb +203 -193
- data/tableless_model.gemspec +3 -2
- metadata +34 -11
- data/README.rdoc +0 -170
- data/spec/test_helper.rb +0 -157
data/spec/spec_helper.rb
ADDED
@@ -1,246 +1,256 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
27
|
-
|
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
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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 "
|
68
|
-
|
69
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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 "
|
108
|
-
|
109
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
146
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
175
|
-
|
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 "
|
185
|
-
|
186
|
-
|
187
|
-
|
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 "
|
191
|
-
|
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 "
|
195
|
-
|
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 "
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
217
|
+
# restoring original type
|
218
|
+
ModelOptions.attributes[:typed_attribute_no_default_value][:type] = :integer
|
219
|
+
end
|
223
220
|
|
224
|
-
|
225
|
-
|
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
|
-
|
228
|
-
|
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
|
-
|
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
|
-
|
237
|
-
|
238
|
+
before(:each) do
|
239
|
+
Timecop.freeze(frozen_time) { TestModel.create({ :options => test_values }) }
|
240
|
+
end
|
238
241
|
|
239
|
-
|
240
|
-
|
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
|