dm-is-temporal 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +20 -0
- data/Gemfile.lock +43 -0
- data/README.md +3 -2
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/dm-is-temporal/is/temporal.rb +230 -11
- data/spec/assoc_spec.rb +263 -0
- data/spec/hooks_spec.rb +20 -16
- data/spec/temporal_spec.rb +9 -6
- metadata +55 -14
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
# there is a bug in 1.1.0 that is preventing up from using it. Its already fixed though, and should be available
|
4
|
+
# in 1.1.1. We'll upgrade then
|
5
|
+
|
6
|
+
#gem 'dm-core', '~> 1.1.1'
|
7
|
+
gem 'dm-core', '~> 1.0.2'
|
8
|
+
|
9
|
+
group :development do
|
10
|
+
gem 'jeweler', '1.5.2'
|
11
|
+
gem 'rake', '0.8.7'
|
12
|
+
gem 'rspec', '1.3.0'
|
13
|
+
end
|
14
|
+
|
15
|
+
group :datamapper do
|
16
|
+
# gem 'dm-migrations', '~> 1.1.1'
|
17
|
+
# gem 'dm-sqlite-adapter', '~> 1.1.1'
|
18
|
+
gem 'dm-migrations', '~> 1.0.2'
|
19
|
+
gem 'dm-sqlite-adapter', '~> 1.0.2'
|
20
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.2.5)
|
5
|
+
data_objects (0.10.5)
|
6
|
+
addressable (~> 2.1)
|
7
|
+
dm-core (1.0.2)
|
8
|
+
addressable (~> 2.2)
|
9
|
+
extlib (~> 0.9.15)
|
10
|
+
dm-do-adapter (1.0.2)
|
11
|
+
data_objects (~> 0.10.2)
|
12
|
+
dm-core (~> 1.0.2)
|
13
|
+
dm-migrations (1.0.2)
|
14
|
+
dm-core (~> 1.0.2)
|
15
|
+
dm-sqlite-adapter (1.0.2)
|
16
|
+
dm-do-adapter (~> 1.0.2)
|
17
|
+
do_sqlite3 (~> 0.10.2)
|
18
|
+
do_jdbc (0.10.5-java)
|
19
|
+
data_objects (= 0.10.5)
|
20
|
+
do_sqlite3 (0.10.5-java)
|
21
|
+
data_objects (= 0.10.5)
|
22
|
+
do_jdbc (= 0.10.5)
|
23
|
+
jdbc-sqlite3 (>= 3.5.8)
|
24
|
+
extlib (0.9.15)
|
25
|
+
git (1.2.5)
|
26
|
+
jdbc-sqlite3 (3.6.14.2.056-java)
|
27
|
+
jeweler (1.5.2)
|
28
|
+
bundler (~> 1.0.0)
|
29
|
+
git (>= 1.2.5)
|
30
|
+
rake
|
31
|
+
rake (0.8.7)
|
32
|
+
rspec (1.3.0)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
java
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
dm-core (~> 1.0.2)
|
39
|
+
dm-migrations (~> 1.0.2)
|
40
|
+
dm-sqlite-adapter (~> 1.0.2)
|
41
|
+
jeweler (= 1.5.2)
|
42
|
+
rake (= 0.8.7)
|
43
|
+
rspec (= 1.3.0)
|
data/README.md
CHANGED
@@ -105,7 +105,8 @@ How it works
|
|
105
105
|
-------------
|
106
106
|
Temporal patterns differ from versioning patterns (such as [dm-is-versioned](https://github.com/datamapper/dm-is-versioned))
|
107
107
|
in that every version of the temporal properties is a peer (even the most recent). Accessing temporal properties without the `at(time)` method
|
108
|
-
is just a convinience for `at(DateTime.now)`.
|
108
|
+
is just a convinience for `at(DateTime.now)`. Furthermore, a reference to a temporal object doesn't have to change when
|
109
|
+
the state of the temporal object changes. This is due to the continuity table (my_models) illustrated below.
|
109
110
|
|
110
111
|
In addition, you have the ability inject versions at previous time-states (modifying history).
|
111
112
|
|
@@ -136,7 +137,7 @@ TODO
|
|
136
137
|
------
|
137
138
|
|
138
139
|
+ MyClass.update (update all records for a model) doesn't work
|
139
|
-
+ Temporal Associations
|
140
|
+
+ Bi-directional Temporal Associations
|
140
141
|
+ Temporal Property pattern (i.e. multiple independent temporal properties per class)
|
141
142
|
+ Bi-temporality
|
142
143
|
+ Add a config flag that enables an error to be raised when attempting to rewrite existing versions
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
@@ -6,6 +6,7 @@ module DataMapper
|
|
6
6
|
|
7
7
|
def initialize(base)
|
8
8
|
@__properties__ = []
|
9
|
+
@__has__ = []
|
9
10
|
@__base__ = base
|
10
11
|
@__before__ = []
|
11
12
|
@__after__ = []
|
@@ -23,24 +24,46 @@ module DataMapper
|
|
23
24
|
@__after__ << [args, block]
|
24
25
|
end
|
25
26
|
|
26
|
-
def has(*args)
|
27
|
-
|
27
|
+
def has(multiplicity, *args)
|
28
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
29
|
+
name = args.shift
|
30
|
+
raise "Associations :through option is not yet supported!" if opts[:through]
|
31
|
+
@__has__ << [multiplicity, name, args, opts]
|
28
32
|
end
|
29
33
|
|
30
34
|
def belongs_to(*args)
|
31
35
|
raise 'Temporal associations are not supported yet!'
|
32
36
|
end
|
33
37
|
|
38
|
+
def n
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
34
42
|
def __properties__
|
35
43
|
@__properties__
|
36
44
|
end
|
45
|
+
|
46
|
+
def __has__
|
47
|
+
@__has__
|
48
|
+
end
|
37
49
|
end
|
38
50
|
|
51
|
+
@@__temporal_classes__ = []
|
52
|
+
|
53
|
+
def __temporal_classes__
|
54
|
+
@@__temporal_classes__
|
55
|
+
end
|
56
|
+
protected :__temporal_classes__
|
57
|
+
|
39
58
|
def is_temporal(*args, &block)
|
40
59
|
|
41
60
|
extend(Migration) if respond_to?(:auto_migrate!)
|
42
61
|
|
43
|
-
version_model = DataMapper::Model.new
|
62
|
+
version_model = DataMapper::Model.new do
|
63
|
+
def has(*args)
|
64
|
+
#ignore all
|
65
|
+
end
|
66
|
+
end
|
44
67
|
|
45
68
|
if block_given?
|
46
69
|
version_model.instance_eval <<-RUBY
|
@@ -55,11 +78,12 @@ module DataMapper
|
|
55
78
|
version_model.instance_eval(&block)
|
56
79
|
|
57
80
|
const_set(:TemporalVersion, version_model)
|
81
|
+
@@__temporal_classes__ << self::TemporalVersion
|
58
82
|
|
59
83
|
h = PropertyHelper.new(self)
|
60
84
|
h.instance_eval(&block)
|
61
85
|
|
62
|
-
has n, :temporal_versions
|
86
|
+
has n, :temporal_versions #, :accessor => :protected
|
63
87
|
|
64
88
|
create_methods
|
65
89
|
|
@@ -67,8 +91,48 @@ module DataMapper
|
|
67
91
|
create_temporal_reader(a[0])
|
68
92
|
create_temporal_writer(a[0])
|
69
93
|
end
|
94
|
+
|
95
|
+
|
96
|
+
h.__has__.each do |a|
|
97
|
+
has_args = a[2]
|
98
|
+
plural_name = a[1]
|
99
|
+
|
100
|
+
if has_args.any? and has_args[0].is_a?(String)
|
101
|
+
clazz = has_args[0]
|
102
|
+
table = DataMapper::Inflector.tableize(clazz)
|
103
|
+
singular_name = DataMapper::Inflector.singularize(table)
|
104
|
+
else
|
105
|
+
singular_name = DataMapper::Inflector.singularize(plural_name.to_s)
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
list_model = DataMapper::Model.new
|
110
|
+
|
111
|
+
list_model.instance_eval <<-RUBY
|
112
|
+
def self.default_repository_name
|
113
|
+
:#{default_repository_name}
|
114
|
+
end
|
115
|
+
RUBY
|
116
|
+
|
117
|
+
list_model.property(:id, DataMapper::Property::Serial)
|
118
|
+
list_model.property(:updated_at, DataMapper::Property::DateTime)
|
119
|
+
list_model.property(:deleted_at, DataMapper::Property::DateTime)
|
120
|
+
list_model.before(:save) { self.updated_at ||= DateTime.now }
|
121
|
+
list_model.belongs_to singular_name #, :accessor => :protected
|
122
|
+
|
123
|
+
temporal_list_name = "temporal_#{plural_name}".to_sym
|
124
|
+
|
125
|
+
list_model_name = Inflector.classify("temporal_#{singular_name}")
|
126
|
+
|
127
|
+
const_set(list_model_name.to_sym, list_model)
|
128
|
+
@@__temporal_classes__ << eval("self::#{list_model_name}")
|
129
|
+
|
130
|
+
has n, temporal_list_name, list_model_name
|
131
|
+
|
132
|
+
create_temporal_list_reader(plural_name, singular_name, temporal_list_name, list_model_name)
|
133
|
+
# create_temporal_list_writer(a[1], temporal_list)
|
134
|
+
end
|
70
135
|
else
|
71
|
-
# const_set(:TemporalVersion + args[0], version_model)
|
72
136
|
raise "Temporal Property pattern not supported yet"
|
73
137
|
end
|
74
138
|
|
@@ -146,6 +210,15 @@ module DataMapper
|
|
146
210
|
@__at__ = nil
|
147
211
|
t
|
148
212
|
end
|
213
|
+
|
214
|
+
def __versions_for_context__(temporal_list_name, context=DateTime.now)
|
215
|
+
@__at__ ||= context
|
216
|
+
t = self.__send__(temporal_list_name).select do |n|
|
217
|
+
(t.nil? or n.updated_at > t.updated_at) and n.updated_at <= @__at__ and (n.deleted_at.nil? or (n.deleted_at > @__at__))
|
218
|
+
end
|
219
|
+
@__at__ = nil
|
220
|
+
t
|
221
|
+
end
|
149
222
|
RUBY
|
150
223
|
end
|
151
224
|
|
@@ -172,7 +245,24 @@ module DataMapper
|
|
172
245
|
end
|
173
246
|
t.#{name} = x
|
174
247
|
self.save
|
175
|
-
|
248
|
+
x
|
249
|
+
end
|
250
|
+
RUBY
|
251
|
+
end
|
252
|
+
|
253
|
+
def create_temporal_list_reader(plural_name, singular_name, temporal_list_name, temporal_list_model)
|
254
|
+
class_eval <<-RUBY
|
255
|
+
def #{plural_name}(context=DateTime.now)
|
256
|
+
at = @__at__
|
257
|
+
at ||= context
|
258
|
+
versions = __versions_for_context__(#{temporal_list_name.inspect}, context)
|
259
|
+
TemporalListProxy.new(
|
260
|
+
self,
|
261
|
+
versions.map {|v| v.#{singular_name}},
|
262
|
+
#{temporal_list_model},
|
263
|
+
#{temporal_list_name.to_sym.inspect},
|
264
|
+
#{singular_name.to_sym.inspect},
|
265
|
+
at)
|
176
266
|
end
|
177
267
|
RUBY
|
178
268
|
end
|
@@ -191,19 +281,148 @@ module DataMapper
|
|
191
281
|
end
|
192
282
|
end
|
193
283
|
|
284
|
+
class TemporalListProxy
|
285
|
+
# make this a blank slate
|
286
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
|
287
|
+
|
288
|
+
def initialize(base_object, list, temporal_list_model, temporal_list_name, name, context)
|
289
|
+
@base_object = base_object
|
290
|
+
@list = list
|
291
|
+
@temporal_list_name = temporal_list_name
|
292
|
+
@temporal_list_model = temporal_list_model
|
293
|
+
@name = name
|
294
|
+
@context = context
|
295
|
+
end
|
296
|
+
|
297
|
+
def <<(x)
|
298
|
+
new_model = @temporal_list_model.create(:updated_at => @context, @name => x)
|
299
|
+
@base_object.send(@temporal_list_name) << new_model
|
300
|
+
end
|
301
|
+
|
302
|
+
def clear
|
303
|
+
@base_object.send(@temporal_list_name).each do |temporal|
|
304
|
+
temporal.deleted_at = @context
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def []=(x,y)
|
309
|
+
raise "Unsupported method"
|
310
|
+
end
|
311
|
+
|
312
|
+
def map!
|
313
|
+
raise "Unsupported method"
|
314
|
+
end
|
315
|
+
|
316
|
+
def collect!
|
317
|
+
raise "Unsupported method"
|
318
|
+
end
|
319
|
+
|
320
|
+
def compact!
|
321
|
+
raise "Unsupported method"
|
322
|
+
end
|
323
|
+
|
324
|
+
def delete(obj)
|
325
|
+
@base_object.send(@temporal_list_name).each do |temporal|
|
326
|
+
if temporal.send(@name) == obj
|
327
|
+
temporal.deleted_at = @context
|
328
|
+
return true
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def delete_at(i)
|
334
|
+
# probably won't ever support this - not really doing order
|
335
|
+
raise "Unsupported method"
|
336
|
+
end
|
337
|
+
|
338
|
+
def delete_if
|
339
|
+
@base_object.send(@temporal_list_name).each do |temporal|
|
340
|
+
if yield(temporal.send(@name))
|
341
|
+
temporal.deleted_at = @context
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def drop(i)
|
347
|
+
raise "Unsupported method"
|
348
|
+
end
|
349
|
+
|
350
|
+
def drop_while(i)
|
351
|
+
raise "Unsupported method"
|
352
|
+
end
|
353
|
+
|
354
|
+
def fill(*args)
|
355
|
+
raise "Unsupported method"
|
356
|
+
end
|
357
|
+
|
358
|
+
def flatten!(level=nil)
|
359
|
+
raise "Unsupported method"
|
360
|
+
end
|
361
|
+
|
362
|
+
def replace(other_array=nil)
|
363
|
+
raise "Unsupported method"
|
364
|
+
end
|
365
|
+
|
366
|
+
def insert(index, *args)
|
367
|
+
raise "Unsupported method"
|
368
|
+
end
|
369
|
+
|
370
|
+
def pop(n=nil)
|
371
|
+
temporal = @base_object.send(@temporal_list_name).last
|
372
|
+
temporal.deleted_at = @context
|
373
|
+
temporal.send(@name)
|
374
|
+
end
|
375
|
+
|
376
|
+
def push(*obj)
|
377
|
+
raise "Unsupported method"
|
378
|
+
end
|
379
|
+
|
380
|
+
def rehject!
|
381
|
+
raise "Unsupported method"
|
382
|
+
end
|
383
|
+
|
384
|
+
def reverse!
|
385
|
+
raise "Unsupported method"
|
386
|
+
end
|
387
|
+
|
388
|
+
def shuffle!
|
389
|
+
raise "Unsupported method"
|
390
|
+
end
|
391
|
+
|
392
|
+
def slice(*args)
|
393
|
+
raise "Unsupported method"
|
394
|
+
end
|
395
|
+
|
396
|
+
def sort!
|
397
|
+
raise "Unsupported method"
|
398
|
+
end
|
399
|
+
|
400
|
+
def uniq!
|
401
|
+
raise "Unsupported method"
|
402
|
+
end
|
403
|
+
|
404
|
+
def method_missing(sym, *args, &block)
|
405
|
+
@list.__send__(sym, *args, &block)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
194
409
|
module Migration
|
195
410
|
|
196
411
|
def auto_migrate!(repository_name = self.repository_name)
|
197
|
-
super
|
198
|
-
self::
|
412
|
+
super(repository_name)
|
413
|
+
self::__temporal_classes__.each do |t|
|
414
|
+
t.auto_migrate!
|
415
|
+
end
|
199
416
|
end
|
200
417
|
|
201
418
|
def auto_upgrade!(repository_name = self.repository_name)
|
202
|
-
super
|
203
|
-
self::
|
419
|
+
super(repository_name)
|
420
|
+
self::__temporal_classes__.each do |t|
|
421
|
+
t.auto_upgrade!
|
422
|
+
end
|
204
423
|
end
|
205
424
|
|
206
|
-
end
|
425
|
+
end
|
207
426
|
end
|
208
427
|
end
|
209
428
|
end
|
data/spec/assoc_spec.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Assoc
|
4
|
+
class Foobar
|
5
|
+
include DataMapper::Resource
|
6
|
+
|
7
|
+
property :id, Serial
|
8
|
+
property :baz, String
|
9
|
+
|
10
|
+
# todo bi-direction temporal relationships
|
11
|
+
# belongs_to :my_model
|
12
|
+
end
|
13
|
+
|
14
|
+
class MyModel
|
15
|
+
include DataMapper::Resource
|
16
|
+
|
17
|
+
property :id, Serial
|
18
|
+
property :name, String
|
19
|
+
|
20
|
+
is_temporal do
|
21
|
+
has n, :foobars
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class MyModelTwo
|
26
|
+
include DataMapper::Resource
|
27
|
+
|
28
|
+
property :id, Serial
|
29
|
+
property :name, String
|
30
|
+
|
31
|
+
is_temporal do
|
32
|
+
has n, :foobazes, 'Foobar'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe DataMapper::Is::Temporal do
|
38
|
+
|
39
|
+
before(:all) do
|
40
|
+
DataMapper.setup(:default, "sqlite3::memory:")
|
41
|
+
DataMapper.setup(:test, "sqlite3::memory:")
|
42
|
+
end
|
43
|
+
|
44
|
+
before(:each) do
|
45
|
+
DataMapper.auto_migrate!
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#has" do
|
49
|
+
|
50
|
+
context "with defaults" do
|
51
|
+
subject do
|
52
|
+
Assoc::MyModel.create
|
53
|
+
end
|
54
|
+
|
55
|
+
it "adds a Foobar at the current time" do
|
56
|
+
f = Assoc::Foobar.create(:baz => "hello")
|
57
|
+
subject.foobars << f
|
58
|
+
subject.save
|
59
|
+
subject.foobars.size.should == 1
|
60
|
+
subject.foobars.should include(f)
|
61
|
+
|
62
|
+
subject.instance_eval { self.temporal_foobars.size.should == 1}
|
63
|
+
end
|
64
|
+
|
65
|
+
it "adds a couple Foobars at different times" do
|
66
|
+
bot = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
67
|
+
old = DateTime.parse('-2011-01-01T00:00:00+00:00')
|
68
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
69
|
+
|
70
|
+
subject.at(old).foobars << f1 = Assoc::Foobar.create(:baz => "hello")
|
71
|
+
subject.save
|
72
|
+
subject.at(old).foobars.size.should == 1
|
73
|
+
|
74
|
+
subject.instance_eval { self.temporal_foobars.size.should == 1}
|
75
|
+
|
76
|
+
subject.at(now).foobars << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
77
|
+
subject.save
|
78
|
+
|
79
|
+
subject.at(bot).foobars.size.should == 0
|
80
|
+
subject.at(old).foobars.size.should == 1
|
81
|
+
subject.at(now).foobars.size.should == 2
|
82
|
+
|
83
|
+
subject.instance_eval { self.temporal_foobars.size.should == 2}
|
84
|
+
|
85
|
+
subject.foobars.should include(f1, f2)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "pops" do
|
89
|
+
old = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
90
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
91
|
+
|
92
|
+
subject.at(old).foobars << f1 = Assoc::Foobar.create(:baz => "hello")
|
93
|
+
subject.at(old).foobars << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
94
|
+
subject.save
|
95
|
+
|
96
|
+
subject.at(old).foobars.should include(f1, f2)
|
97
|
+
subject.at(now).foobars.should include(f1, f2)
|
98
|
+
|
99
|
+
subject.at(old).foobars.pop
|
100
|
+
|
101
|
+
subject.at(old).foobars.size.should == 1
|
102
|
+
subject.at(now).foobars.size.should == 1
|
103
|
+
subject.foobars.size.should == 1
|
104
|
+
end
|
105
|
+
|
106
|
+
it "pops and has proper state before and after" do
|
107
|
+
bot = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
108
|
+
old = DateTime.parse('-2011-01-01T00:00:00+00:00')
|
109
|
+
mid = DateTime.parse('0000-01-01T00:00:00+00:00')
|
110
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
111
|
+
|
112
|
+
subject.at(old).foobars << f1 = Assoc::Foobar.create(:baz => "hello")
|
113
|
+
subject.at(old).foobars << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
114
|
+
subject.save
|
115
|
+
|
116
|
+
subject.at(bot).foobars.size.should == 0
|
117
|
+
subject.at(old).foobars.should include(f1, f2)
|
118
|
+
subject.at(mid).foobars.should include(f1, f2)
|
119
|
+
subject.at(now).foobars.should include(f1, f2)
|
120
|
+
|
121
|
+
subject.at(mid).foobars.pop
|
122
|
+
|
123
|
+
subject.at(bot).foobars.size.should == 0
|
124
|
+
subject.at(old).foobars.size.should == 2
|
125
|
+
subject.at(mid).foobars.size.should == 1
|
126
|
+
subject.at(now).foobars.size.should == 1
|
127
|
+
subject.foobars.size.should == 1
|
128
|
+
end
|
129
|
+
|
130
|
+
it "deletes" do
|
131
|
+
old = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
132
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
133
|
+
|
134
|
+
subject.at(old).foobars << f1 = Assoc::Foobar.create(:baz => "hello")
|
135
|
+
subject.at(old).foobars << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
136
|
+
subject.save
|
137
|
+
|
138
|
+
subject.at(old).foobars.should include(f1, f2)
|
139
|
+
|
140
|
+
subject.at(old).foobars.delete(f1)
|
141
|
+
|
142
|
+
subject.at(old).foobars.size.should == 1
|
143
|
+
subject.at(now).foobars.size.should == 1
|
144
|
+
subject.foobars.size.should == 1
|
145
|
+
subject.at(old).foobars.should include(f2)
|
146
|
+
subject.at(now).foobars.should include(f2)
|
147
|
+
subject.foobars.should include(f2)
|
148
|
+
|
149
|
+
subject.at(old).foobars.delete(f2)
|
150
|
+
|
151
|
+
subject.at(old).foobars.size.should == 0
|
152
|
+
subject.at(now).foobars.size.should == 0
|
153
|
+
subject.foobars.size.should == 0
|
154
|
+
end
|
155
|
+
|
156
|
+
it "delete_if" do
|
157
|
+
old = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
158
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
159
|
+
|
160
|
+
subject.at(old).foobars << f1 = Assoc::Foobar.create(:baz => "hello")
|
161
|
+
subject.at(old).foobars << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
162
|
+
subject.save
|
163
|
+
|
164
|
+
subject.at(old).foobars.should include(f1, f2)
|
165
|
+
|
166
|
+
subject.at(old).foobars.delete_if do |f|
|
167
|
+
f.baz == 'hello'
|
168
|
+
end
|
169
|
+
subject.save
|
170
|
+
|
171
|
+
subject.at(old).foobars.size.should == 1
|
172
|
+
subject.at(now).foobars.size.should == 1
|
173
|
+
subject.foobars.size.should == 1
|
174
|
+
subject.at(old).foobars.should include(f2)
|
175
|
+
subject.at(now).foobars.should include(f2)
|
176
|
+
subject.foobars.should include(f2)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "clears" do
|
180
|
+
bot = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
181
|
+
old = DateTime.parse('-2011-01-01T00:00:00+00:00')
|
182
|
+
mid = DateTime.parse('0000-01-01T00:00:00+00:00')
|
183
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
184
|
+
|
185
|
+
subject.at(old).foobars << f1 = Assoc::Foobar.create(:baz => "hello")
|
186
|
+
subject.at(old).foobars << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
187
|
+
subject.save
|
188
|
+
|
189
|
+
subject.at(old).foobars.should include(f1, f2)
|
190
|
+
|
191
|
+
subject.at(mid).foobars.clear
|
192
|
+
subject.save
|
193
|
+
|
194
|
+
subject.at(bot).foobars.size.should == 0
|
195
|
+
subject.at(old).foobars.size.should == 2
|
196
|
+
subject.at(mid).foobars.size.should == 0
|
197
|
+
subject.at(now).foobars.size.should == 0
|
198
|
+
subject.foobars.size.should == 0
|
199
|
+
end
|
200
|
+
|
201
|
+
it "adds a couple Foobars at the same time" do
|
202
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
203
|
+
|
204
|
+
subject.at(now) do |m|
|
205
|
+
m.foobars << Assoc::Foobar.create(:baz => "hello")
|
206
|
+
m.foobars << Assoc::Foobar.create(:baz => "goodbye")
|
207
|
+
end
|
208
|
+
subject.save
|
209
|
+
|
210
|
+
subject.at(now).foobars.size.should == 2
|
211
|
+
subject.instance_eval { self.temporal_foobars.size.should == 2}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context "with explicit class" do
|
216
|
+
subject do
|
217
|
+
Assoc::MyModelTwo.create
|
218
|
+
end
|
219
|
+
|
220
|
+
it "adds a Foobar at the current time" do
|
221
|
+
f = Assoc::Foobar.create(:baz => "hello")
|
222
|
+
subject.foobazes << f
|
223
|
+
subject.save
|
224
|
+
subject.foobazes.size.should == 1
|
225
|
+
subject.foobazes.should include(f)
|
226
|
+
|
227
|
+
subject.instance_eval { self.temporal_foobazes.size.should == 1}
|
228
|
+
end
|
229
|
+
|
230
|
+
it "adds a couple Foobars at different times" do
|
231
|
+
old = DateTime.parse('-4712-01-01T00:00:00+00:00')
|
232
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
233
|
+
|
234
|
+
subject.at(old).foobazes << f1 = Assoc::Foobar.create(:baz => "hello")
|
235
|
+
subject.save
|
236
|
+
subject.at(old).foobazes.size.should == 1
|
237
|
+
|
238
|
+
subject.instance_eval { self.temporal_foobazes.size.should == 1}
|
239
|
+
|
240
|
+
subject.at(now).foobazes << f2 = Assoc::Foobar.create(:baz => "goodbye")
|
241
|
+
subject.save
|
242
|
+
subject.at(now).foobazes.size.should == 2
|
243
|
+
|
244
|
+
subject.instance_eval { self.temporal_foobazes.size.should == 2}
|
245
|
+
|
246
|
+
subject.foobazes.should include(f1, f2)
|
247
|
+
end
|
248
|
+
|
249
|
+
it "adds a couple Foobars at the same time" do
|
250
|
+
now = DateTime.parse('2011-03-01T00:00:00+00:00')
|
251
|
+
|
252
|
+
subject.at(now) do |m|
|
253
|
+
m.foobazes << Assoc::Foobar.create(:baz => "hello")
|
254
|
+
m.foobazes << Assoc::Foobar.create(:baz => "goodbye")
|
255
|
+
end
|
256
|
+
subject.save
|
257
|
+
|
258
|
+
subject.at(now).foobazes.size.should == 2
|
259
|
+
subject.instance_eval { self.temporal_foobazes.size.should == 2}
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
data/spec/hooks_spec.rb
CHANGED
@@ -1,22 +1,23 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
module Hooks
|
4
|
+
class MyModel
|
5
|
+
include DataMapper::Resource
|
3
6
|
|
4
|
-
|
5
|
-
|
7
|
+
property :id, Serial
|
8
|
+
property :name, String
|
6
9
|
|
7
|
-
|
8
|
-
|
10
|
+
is_temporal do
|
11
|
+
property :foo, Integer
|
12
|
+
property :bar, String
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
before(:save) do |m|
|
15
|
+
m.foo = 42
|
16
|
+
end
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
after(:create) do |m|
|
19
|
-
m.bar = 'hello'
|
18
|
+
after(:create) do |m|
|
19
|
+
m.bar = 'hello'
|
20
|
+
end
|
20
21
|
end
|
21
22
|
end
|
22
23
|
end
|
@@ -26,13 +27,16 @@ describe DataMapper::Is::Temporal do
|
|
26
27
|
before(:all) do
|
27
28
|
DataMapper.setup(:default, "sqlite3::memory:")
|
28
29
|
DataMapper.setup(:test, "sqlite3::memory:")
|
30
|
+
end
|
31
|
+
|
32
|
+
before (:each) do
|
29
33
|
DataMapper.auto_migrate!
|
30
34
|
end
|
31
35
|
|
32
36
|
describe "#before" do
|
33
37
|
|
34
38
|
subject do
|
35
|
-
MyModel.create
|
39
|
+
Hooks::MyModel.create
|
36
40
|
end
|
37
41
|
|
38
42
|
it "before hook sets foo to 42" do
|
@@ -56,14 +60,14 @@ describe DataMapper::Is::Temporal do
|
|
56
60
|
subject.save
|
57
61
|
subject.at(oldish).bar.should == 'hello'
|
58
62
|
|
59
|
-
subject.instance_eval {
|
63
|
+
subject.instance_eval { self.temporal_versions.size.should == 1}
|
60
64
|
|
61
65
|
subject.at(nowish).bar = 'quack'
|
62
66
|
subject.save
|
63
67
|
subject.at(nowish).bar.should == 'quack'
|
64
68
|
subject.at(oldish).bar.should == 'hello'
|
65
69
|
|
66
|
-
subject.instance_eval {
|
70
|
+
subject.instance_eval { self.temporal_versions.size.should == 2}
|
67
71
|
end
|
68
72
|
end
|
69
73
|
end
|
data/spec/temporal_spec.rb
CHANGED
@@ -33,6 +33,9 @@ describe DataMapper::Is::Temporal do
|
|
33
33
|
before(:all) do
|
34
34
|
DataMapper.setup(:default, "sqlite3::memory:")
|
35
35
|
DataMapper.setup(:test, "sqlite3::memory:")
|
36
|
+
end
|
37
|
+
|
38
|
+
before(:each) do
|
36
39
|
DataMapper.auto_migrate!(:default)
|
37
40
|
DataMapper.auto_migrate!(:test)
|
38
41
|
end
|
@@ -45,7 +48,7 @@ describe DataMapper::Is::Temporal do
|
|
45
48
|
|
46
49
|
it "version has the right parent" do
|
47
50
|
subject.foo = 42
|
48
|
-
subject.instance_eval {
|
51
|
+
subject.instance_eval { self.temporal_versions[0].my_model_id.should == self.id}
|
49
52
|
end
|
50
53
|
|
51
54
|
it "update all still works for non-temporal properties" do
|
@@ -121,7 +124,7 @@ describe DataMapper::Is::Temporal do
|
|
121
124
|
subject.at(nowish).foo.should == 1024
|
122
125
|
subject.at(future).foo.should == 3
|
123
126
|
|
124
|
-
subject.instance_eval {
|
127
|
+
subject.instance_eval { self.temporal_versions.size.should == 3}
|
125
128
|
end
|
126
129
|
|
127
130
|
it "and rewriting works" do
|
@@ -315,7 +318,7 @@ describe DataMapper::Is::Temporal do
|
|
315
318
|
subject.at(nowish).bar.should == "dog"
|
316
319
|
subject.at(future).bar.should == "rat"
|
317
320
|
|
318
|
-
subject.instance_eval {
|
321
|
+
subject.instance_eval { self.temporal_versions.size.should == 3}
|
319
322
|
end
|
320
323
|
|
321
324
|
it "works with different times and non-temporal properties" do
|
@@ -377,7 +380,7 @@ describe DataMapper::Is::Temporal do
|
|
377
380
|
subject.at(nowish).name.should == "time"
|
378
381
|
subject.at(future).name.should == "time"
|
379
382
|
|
380
|
-
subject.instance_eval {
|
383
|
+
subject.instance_eval { self.temporal_versions.size.should == 3}
|
381
384
|
end
|
382
385
|
|
383
386
|
it "works with no time (i.e. now)" do
|
@@ -389,7 +392,7 @@ describe DataMapper::Is::Temporal do
|
|
389
392
|
subject.foo.should == 42
|
390
393
|
subject.bar.should == "cat"
|
391
394
|
|
392
|
-
subject.instance_eval {
|
395
|
+
subject.instance_eval { self.temporal_versions.size.should == 1}
|
393
396
|
end
|
394
397
|
|
395
398
|
it "works with no time (i.e. now)" do
|
@@ -403,7 +406,7 @@ describe DataMapper::Is::Temporal do
|
|
403
406
|
subject.bar.should == "cat"
|
404
407
|
subject.name.should == "foobar"
|
405
408
|
|
406
|
-
subject.instance_eval {
|
409
|
+
subject.instance_eval { self.temporal_versions.size.should == 1}
|
407
410
|
end
|
408
411
|
end
|
409
412
|
end
|
metadata
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-is-temporal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 3
|
8
|
-
- 0
|
9
|
-
version: 0.3.0
|
4
|
+
prerelease:
|
5
|
+
version: 0.4.0
|
10
6
|
platform: ruby
|
11
7
|
authors:
|
12
8
|
- Joe Kutner
|
@@ -14,10 +10,53 @@ autorequire:
|
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
12
|
|
17
|
-
date: 2011-
|
13
|
+
date: 2011-05-11 00:00:00 -05:00
|
18
14
|
default_executable:
|
19
|
-
dependencies:
|
20
|
-
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: dm-core
|
18
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.0.2
|
24
|
+
requirement: *id001
|
25
|
+
prerelease: false
|
26
|
+
type: :runtime
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jeweler
|
29
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - "="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.5.2
|
35
|
+
requirement: *id002
|
36
|
+
prerelease: false
|
37
|
+
type: :development
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: rake
|
40
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - "="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.8.7
|
46
|
+
requirement: *id003
|
47
|
+
prerelease: false
|
48
|
+
type: :development
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rspec
|
51
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - "="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 1.3.0
|
57
|
+
requirement: *id004
|
58
|
+
prerelease: false
|
59
|
+
type: :development
|
21
60
|
description: DataMapper plugin implementing temporal patterns
|
22
61
|
email: jpkutner [a] gmail [d] com
|
23
62
|
executables: []
|
@@ -28,12 +67,15 @@ extra_rdoc_files:
|
|
28
67
|
- LICENSE.txt
|
29
68
|
- README.md
|
30
69
|
files:
|
70
|
+
- Gemfile
|
71
|
+
- Gemfile.lock
|
31
72
|
- LICENSE.txt
|
32
73
|
- README.md
|
33
74
|
- Rakefile
|
34
75
|
- VERSION
|
35
76
|
- lib/dm-is-temporal.rb
|
36
77
|
- lib/dm-is-temporal/is/temporal.rb
|
78
|
+
- spec/assoc_spec.rb
|
37
79
|
- spec/hooks_spec.rb
|
38
80
|
- spec/spec.opts
|
39
81
|
- spec/spec_helper.rb
|
@@ -48,27 +90,26 @@ rdoc_options: []
|
|
48
90
|
require_paths:
|
49
91
|
- lib
|
50
92
|
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
51
94
|
requirements:
|
52
95
|
- - ">="
|
53
96
|
- !ruby/object:Gem::Version
|
54
|
-
segments:
|
55
|
-
- 0
|
56
97
|
version: "0"
|
57
98
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
58
100
|
requirements:
|
59
101
|
- - ">="
|
60
102
|
- !ruby/object:Gem::Version
|
61
|
-
segments:
|
62
|
-
- 0
|
63
103
|
version: "0"
|
64
104
|
requirements: []
|
65
105
|
|
66
106
|
rubyforge_project:
|
67
|
-
rubygems_version: 1.
|
107
|
+
rubygems_version: 1.5.1
|
68
108
|
signing_key:
|
69
109
|
specification_version: 3
|
70
110
|
summary: DataMapper plugin implementing temporal patterns
|
71
111
|
test_files:
|
112
|
+
- spec/assoc_spec.rb
|
72
113
|
- spec/hooks_spec.rb
|
73
114
|
- spec/spec_helper.rb
|
74
115
|
- spec/temporal_spec.rb
|