disposable 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -5
- data/CHANGES.md +4 -0
- data/Gemfile +1 -1
- data/README.md +154 -1
- data/database.sqlite3 +0 -0
- data/disposable.gemspec +7 -7
- data/gemfiles/Gemfile.rails-3.0.lock +10 -8
- data/gemfiles/Gemfile.rails-3.2.lock +9 -7
- data/gemfiles/Gemfile.rails-4.0.lock +9 -7
- data/gemfiles/Gemfile.rails-4.1.lock +10 -8
- data/lib/disposable.rb +6 -7
- data/lib/disposable/callback.rb +174 -0
- data/lib/disposable/composition.rb +21 -58
- data/lib/disposable/expose.rb +49 -0
- data/lib/disposable/twin.rb +85 -38
- data/lib/disposable/twin/builder.rb +12 -30
- data/lib/disposable/twin/changed.rb +50 -0
- data/lib/disposable/twin/collection.rb +95 -0
- data/lib/disposable/twin/composition.rb +43 -15
- data/lib/disposable/twin/option.rb +1 -1
- data/lib/disposable/twin/persisted.rb +20 -0
- data/lib/disposable/twin/property_processor.rb +29 -0
- data/lib/disposable/twin/representer.rb +42 -14
- data/lib/disposable/twin/save.rb +19 -34
- data/lib/disposable/twin/schema.rb +31 -0
- data/lib/disposable/twin/setup.rb +38 -0
- data/lib/disposable/twin/sync.rb +114 -0
- data/lib/disposable/version.rb +1 -1
- data/test/api_semantics_test.rb +263 -0
- data/test/callback_group_test.rb +222 -0
- data/test/callbacks_test.rb +450 -0
- data/test/example.rb +40 -0
- data/test/expose_test.rb +92 -0
- data/test/persisted_test.rb +101 -0
- data/test/test_helper.rb +64 -0
- data/test/twin/benchmarking.rb +33 -0
- data/test/twin/builder_test.rb +32 -0
- data/test/twin/changed_test.rb +108 -0
- data/test/twin/collection_test.rb +223 -0
- data/test/twin/composition_test.rb +56 -25
- data/test/twin/expose_test.rb +73 -0
- data/test/twin/feature_test.rb +61 -0
- data/test/twin/from_test.rb +37 -0
- data/test/twin/inherit_test.rb +57 -0
- data/test/twin/option_test.rb +27 -0
- data/test/twin/readable_test.rb +57 -0
- data/test/twin/save_test.rb +192 -0
- data/test/twin/schema_test.rb +69 -0
- data/test/twin/setup_test.rb +139 -0
- data/test/twin/skip_unchanged_test.rb +64 -0
- data/test/twin/struct_test.rb +168 -0
- data/test/twin/sync_option_test.rb +228 -0
- data/test/twin/sync_test.rb +128 -0
- data/test/twin/twin_test.rb +49 -128
- data/test/twin/writeable_test.rb +56 -0
- metadata +106 -20
- data/STUFF +0 -4
- data/lib/disposable/twin/finders.rb +0 -29
- data/lib/disposable/twin/new.rb +0 -30
- data/lib/disposable/twin/save_.rb +0 -21
- data/test/composition_test.rb +0 -102
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# reason: unique API for collection (adding, removing, deleting, etc.)
|
4
|
+
# delay DB write until saving Twin
|
5
|
+
|
6
|
+
# TODO: eg "after delete hook (dynamic_delete)", after_add
|
7
|
+
|
8
|
+
class TwinCollectionTest < MiniTest::Spec
|
9
|
+
module Model
|
10
|
+
Song = Struct.new(:id, :title, :album)
|
11
|
+
Album = Struct.new(:id, :name, :songs, :artist)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
module Twin
|
16
|
+
class Album < Disposable::Twin
|
17
|
+
property :id # DISCUSS: needed for #save.
|
18
|
+
property :name
|
19
|
+
collection :songs, :twin => lambda { |*| Song }
|
20
|
+
end
|
21
|
+
|
22
|
+
class Song < Disposable::Twin
|
23
|
+
property :id # DISCUSS: needed for #save.
|
24
|
+
property :title
|
25
|
+
property :album, :twin => Album
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
let (:song) { Model::Song.new(1, "Broken", nil) }
|
30
|
+
let (:album) { Model::Album.new(1, "The Rest Is Silence", [song]) }
|
31
|
+
|
32
|
+
describe "reader for collection" do
|
33
|
+
it do
|
34
|
+
twin = Twin::Album.new(album)
|
35
|
+
|
36
|
+
twin.songs.size.must_equal 1
|
37
|
+
twin.songs[0].title.must_equal "Broken"
|
38
|
+
twin.songs.must_be_instance_of Disposable::Twin::Collection
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
require "disposable/twin/sync"
|
45
|
+
require "disposable/twin/save"
|
46
|
+
|
47
|
+
class TwinCollectionActiveRecordTest < MiniTest::Spec
|
48
|
+
module Twin
|
49
|
+
class Album < Disposable::Twin
|
50
|
+
property :id # DISCUSS: needed for #save.
|
51
|
+
property :name
|
52
|
+
collection :songs, :twin => lambda { |*| Song }
|
53
|
+
property :artist, twin: lambda { |*| Artist }
|
54
|
+
|
55
|
+
include Sync
|
56
|
+
include Save
|
57
|
+
include Setup
|
58
|
+
include Collection::Semantics
|
59
|
+
end
|
60
|
+
|
61
|
+
class Song < Disposable::Twin
|
62
|
+
property :id # DISCUSS: needed for #save.
|
63
|
+
property :title
|
64
|
+
|
65
|
+
# property :persisted?, readonly: true # TODO: implement that!!!! for #sync
|
66
|
+
|
67
|
+
include Sync
|
68
|
+
include Save
|
69
|
+
end
|
70
|
+
|
71
|
+
class Artist < Disposable::Twin
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
let (:album) { Album.create(name: "The Rest Is Silence") }
|
76
|
+
let (:song1) { Song.new(title: "Snorty Pacifical Rascal") } # unsaved.
|
77
|
+
let (:song2) { Song.create(title: "At Any Cost") } # saved.
|
78
|
+
let (:twin) { Twin::Album.new(album) }
|
79
|
+
|
80
|
+
it do
|
81
|
+
# TODO: test all writers.
|
82
|
+
twin.songs << song1 # assuming that we add AR model here.
|
83
|
+
twin.songs << song2
|
84
|
+
|
85
|
+
twin.songs.size.must_equal 2
|
86
|
+
|
87
|
+
twin.songs[0].must_be_instance_of Twin::Song # twin wraps << added in twin.
|
88
|
+
twin.songs[1].must_be_instance_of Twin::Song
|
89
|
+
|
90
|
+
# twin.songs[0].persisted?.must_equal false
|
91
|
+
twin.songs[0].send(:model).persisted?.must_equal false
|
92
|
+
twin.songs[1].send(:model).persisted?.must_equal true
|
93
|
+
|
94
|
+
album.songs.size.must_equal 0 # nothing synced, yet.
|
95
|
+
|
96
|
+
# sync: delete removed items, add new?
|
97
|
+
|
98
|
+
# save
|
99
|
+
twin.save
|
100
|
+
|
101
|
+
album.persisted?.must_equal true
|
102
|
+
album.name.must_equal "The Rest Is Silence"
|
103
|
+
|
104
|
+
album.songs.size.must_equal 2 # synced!
|
105
|
+
|
106
|
+
album.songs[0].persisted?.must_equal true
|
107
|
+
album.songs[1].persisted?.must_equal true
|
108
|
+
album.songs[0].title.must_equal "Snorty Pacifical Rascal"
|
109
|
+
album.songs[1].title.must_equal "At Any Cost"
|
110
|
+
end
|
111
|
+
|
112
|
+
# test with adding to existing collection [song1] << song2
|
113
|
+
|
114
|
+
# TODO: #delete non-existent twin.
|
115
|
+
describe "#delete" do
|
116
|
+
let (:album) { Album.create(name: "The Rest Is Silence", songs: [song1]) }
|
117
|
+
|
118
|
+
it do
|
119
|
+
twin.songs.delete(twin.songs.first)
|
120
|
+
|
121
|
+
twin.songs.size.must_equal 0
|
122
|
+
album.songs.size.must_equal 1 # not synced, yet.
|
123
|
+
|
124
|
+
twin.save
|
125
|
+
|
126
|
+
twin.songs.size.must_equal 0
|
127
|
+
album.songs.size.must_equal 0
|
128
|
+
song1.persisted?.must_equal true
|
129
|
+
end
|
130
|
+
|
131
|
+
# non-existant delete.
|
132
|
+
it do
|
133
|
+
twin.songs.delete("non-existant") # won't delete anything.
|
134
|
+
twin.songs.size.must_equal 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#destroy" do
|
139
|
+
let (:album) { Album.create(name: "The Rest Is Silence", songs: [song1]) }
|
140
|
+
|
141
|
+
it do
|
142
|
+
twin.songs.destroy(twin.songs.first)
|
143
|
+
|
144
|
+
twin.songs.size.must_equal 0
|
145
|
+
album.songs.size.must_equal 1 # not synced, yet.
|
146
|
+
|
147
|
+
twin.save
|
148
|
+
|
149
|
+
twin.songs.size.must_equal 0
|
150
|
+
album.songs.size.must_equal 0
|
151
|
+
song1.persisted?.must_equal false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
describe "#added" do
|
157
|
+
let (:album) { Album.create(name: "The Rest Is Silence", songs: [song1]) }
|
158
|
+
|
159
|
+
it do
|
160
|
+
twin = Twin::Album.new(album)
|
161
|
+
|
162
|
+
twin.songs.added.must_equal []
|
163
|
+
twin.songs << song2
|
164
|
+
twin.songs.added.must_equal [twin.songs[1]]
|
165
|
+
twin.songs.insert(2, song3 = Song.new)
|
166
|
+
twin.songs.added.must_equal [twin.songs[1], twin.songs[2]]
|
167
|
+
|
168
|
+
# TODO: what to do if we override an item (insert)?
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "#deleted" do
|
173
|
+
let (:album) { Album.create(name: "The Rest Is Silence", songs: [song1, song2, song3 = Song.new]) }
|
174
|
+
|
175
|
+
it do
|
176
|
+
twin = Twin::Album.new(album)
|
177
|
+
|
178
|
+
twin.songs.deleted.must_equal []
|
179
|
+
|
180
|
+
twin.songs.delete(deleted1 = twin.songs[-1])
|
181
|
+
twin.songs.delete(deleted2 = twin.songs[-1])
|
182
|
+
|
183
|
+
twin.songs.must_equal [twin.songs[0]]
|
184
|
+
|
185
|
+
twin.songs.deleted.must_equal [deleted1, deleted2]
|
186
|
+
end
|
187
|
+
|
188
|
+
# non-existant delete.
|
189
|
+
it do
|
190
|
+
twin.songs.delete("non-existant") # won't delete anything.
|
191
|
+
twin.songs.deleted.must_equal []
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
class CollectionUnitTest < MiniTest::Spec
|
198
|
+
module Twin
|
199
|
+
class Album < Disposable::Twin
|
200
|
+
end
|
201
|
+
|
202
|
+
class Song < Disposable::Twin
|
203
|
+
property :album, twin: Twin::Album
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
module Model
|
208
|
+
Album = Struct.new(:id, :name, :songs, :artist)
|
209
|
+
end
|
210
|
+
|
211
|
+
let(:collection) { Disposable::Twin::Collection.new(Disposable::Twin::Twinner.new(Twin::Song.representer_class.representable_attrs.get(:album)), []) }
|
212
|
+
|
213
|
+
# #insert(index, model)
|
214
|
+
it do
|
215
|
+
collection.insert(0, Model::Album.new).must_be_instance_of Twin::Album
|
216
|
+
end
|
217
|
+
|
218
|
+
# #<<
|
219
|
+
it do
|
220
|
+
collection << Model::Album.new
|
221
|
+
collection[0].must_be_instance_of Twin::Album
|
222
|
+
end
|
223
|
+
end
|
@@ -1,20 +1,18 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
# Disposable::Twin::Composition.
|
5
4
|
class TwinCompositionTest < MiniTest::Spec
|
6
|
-
class Request < Disposable::Twin
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
property :name, :on => :requester
|
11
|
-
property :id, :on => :requester
|
5
|
+
class Request < Disposable::Twin
|
6
|
+
include Sync
|
7
|
+
include Save
|
8
|
+
include Composition
|
12
9
|
|
13
|
-
|
10
|
+
property :song_title, on: :song, from: :title
|
11
|
+
property :song_id, on: :song, from: :id
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
#
|
13
|
+
property :name, on: :requester
|
14
|
+
property :id, on: :requester
|
15
|
+
property :captcha, readable: false, writeable: false, on: :requester # TODO: allow both, virtual with and without :on.
|
18
16
|
end
|
19
17
|
|
20
18
|
module Model
|
@@ -22,24 +20,57 @@ class TwinCompositionTest < MiniTest::Spec
|
|
22
20
|
Requester = Struct.new(:id, :name)
|
23
21
|
end
|
24
22
|
|
25
|
-
let (:requester) { Model::Requester.new(1, "Greg Howe") }
|
26
|
-
let (:song) { Model::Song.new(2, "Extraction") }
|
23
|
+
let (:requester) { Model::Requester.new(1, "Greg Howe").extend(Disposable::Saveable) }
|
24
|
+
let (:song) { Model::Song.new(2, "Extraction").extend(Disposable::Saveable) }
|
25
|
+
|
26
|
+
let (:request) { Request.new(song: song, requester: requester) }
|
27
|
+
|
28
|
+
it do
|
29
|
+
request.song_title.must_equal "Extraction"
|
30
|
+
request.song_id.must_equal 2
|
31
|
+
request.name.must_equal "Greg Howe"
|
32
|
+
request.id.must_equal 1
|
33
|
+
|
34
|
+
request.song_title = "Tease"
|
35
|
+
request.name = "Wooten"
|
36
|
+
|
37
|
+
|
38
|
+
request.song_title.must_equal "Tease"
|
39
|
+
request.name.must_equal "Wooten"
|
40
|
+
|
41
|
+
# does not write to model.
|
42
|
+
song.title.must_equal "Extraction"
|
43
|
+
requester.name.must_equal "Greg Howe"
|
44
|
+
|
45
|
+
|
46
|
+
request.save
|
47
|
+
|
48
|
+
# make sure models got synced and saved.
|
49
|
+
song.id.must_equal 2
|
50
|
+
song.title.must_equal "Tease"
|
51
|
+
requester.id.must_equal 1
|
52
|
+
requester.name.must_equal "Wooten"
|
53
|
+
|
54
|
+
song.saved?.must_equal true
|
55
|
+
requester.saved?.must_equal true
|
56
|
+
end
|
27
57
|
|
28
|
-
|
58
|
+
# save with block.
|
59
|
+
it do
|
60
|
+
request.song_title = "Tease"
|
61
|
+
request.name = "Wooten"
|
62
|
+
request.captcha = "Awesome!"
|
29
63
|
|
30
|
-
|
31
|
-
|
64
|
+
# does not write to model.
|
65
|
+
song.title.must_equal "Extraction"
|
66
|
+
requester.name.must_equal "Greg Howe"
|
32
67
|
|
33
68
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
request.name = "Wooten"
|
69
|
+
nested_hash = nil
|
70
|
+
request.save do |hash|
|
71
|
+
nested_hash = hash
|
38
72
|
end
|
39
73
|
|
40
|
-
|
41
|
-
# no writing to model.
|
42
|
-
it { song.title.must_equal "Extraction" }
|
43
|
-
it { request.name.must_equal "Wooten" }
|
74
|
+
nested_hash.must_equal(:song=>{"title"=>"Tease", "id"=>2}, :requester=>{"name"=>"Wooten", "id"=>1, "captcha"=>"Awesome!"})
|
44
75
|
end
|
45
76
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# Disposable::Twin::Expose.
|
4
|
+
class TwinExposeTest < MiniTest::Spec
|
5
|
+
class Request < Disposable::Twin
|
6
|
+
feature Sync
|
7
|
+
feature Save
|
8
|
+
feature Expose
|
9
|
+
|
10
|
+
property :song_title, from: :title
|
11
|
+
property :id
|
12
|
+
# virtual.
|
13
|
+
property :captcha, readable: false, writeable: false
|
14
|
+
# nested.
|
15
|
+
property :album do
|
16
|
+
property :name, from: :getName
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Model
|
21
|
+
Song = Struct.new(:id, :title, :album)
|
22
|
+
Album = Struct.new(:getName)
|
23
|
+
end
|
24
|
+
|
25
|
+
let (:album) { Model::Album.new("Appeal To Reason").extend(Disposable::Saveable) }
|
26
|
+
let (:song) { Model::Song.new(2, "Extraction", album).extend(Disposable::Saveable) }
|
27
|
+
|
28
|
+
let (:request) { Request.new(song) }
|
29
|
+
|
30
|
+
it do
|
31
|
+
request.song_title.must_equal "Extraction"
|
32
|
+
request.id.must_equal 2
|
33
|
+
|
34
|
+
request.song_title = "Tease"
|
35
|
+
request.id = 1
|
36
|
+
|
37
|
+
|
38
|
+
request.song_title.must_equal "Tease"
|
39
|
+
request.id.must_equal 1
|
40
|
+
|
41
|
+
# does not write to model.
|
42
|
+
song.title.must_equal "Extraction"
|
43
|
+
song.id.must_equal 2
|
44
|
+
|
45
|
+
request.save
|
46
|
+
|
47
|
+
# make sure models got synced and saved.
|
48
|
+
song.id.must_equal 1
|
49
|
+
song.title.must_equal "Tease"
|
50
|
+
song.album.must_equal album # nested objects don't get twinned or anything.
|
51
|
+
|
52
|
+
song.saved?.must_equal true
|
53
|
+
end
|
54
|
+
|
55
|
+
# save with block.
|
56
|
+
it do
|
57
|
+
request.song_title = "Tease"
|
58
|
+
request.id = 1
|
59
|
+
request.captcha = "Awesome!"
|
60
|
+
|
61
|
+
nested_hash = nil
|
62
|
+
request.save do |hash|
|
63
|
+
nested_hash = hash
|
64
|
+
end
|
65
|
+
|
66
|
+
nested_hash.must_equal({"title"=>"Tease", "id"=>1, "captcha" => "Awesome!", "album"=>{"getName"=>"Appeal To Reason"}})
|
67
|
+
|
68
|
+
# does not write to model.
|
69
|
+
song.title.must_equal "Extraction"
|
70
|
+
song.id.must_equal 2
|
71
|
+
album.getName.must_equal "Appeal To Reason"
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FeatureTest < MiniTest::Spec
|
4
|
+
Song = Struct.new(:title, :album, :composer)
|
5
|
+
Album = Struct.new(:name, :songs, :artist)
|
6
|
+
Artist = Struct.new(:name)
|
7
|
+
|
8
|
+
module Date
|
9
|
+
def date
|
10
|
+
"May 16"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Instrument
|
15
|
+
def instrument
|
16
|
+
"Violins"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class AlbumForm < Disposable::Twin
|
21
|
+
include Setup
|
22
|
+
feature Date
|
23
|
+
property :name
|
24
|
+
|
25
|
+
collection :songs do
|
26
|
+
include Setup
|
27
|
+
property :title
|
28
|
+
|
29
|
+
property :composer do
|
30
|
+
include Setup
|
31
|
+
feature Instrument
|
32
|
+
property :name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
property :artist do
|
37
|
+
include Setup
|
38
|
+
property :name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let (:song) { Song.new("Broken") }
|
43
|
+
let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
|
44
|
+
let (:composer) { Artist.new("Greg Graffin") }
|
45
|
+
let (:artist) { Artist.new("Bad Religion") }
|
46
|
+
let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
|
47
|
+
|
48
|
+
let (:form) { AlbumForm.new(album) }
|
49
|
+
|
50
|
+
it do
|
51
|
+
form.date.must_equal "May 16"
|
52
|
+
form.artist.date.must_equal "May 16"
|
53
|
+
form.songs[0].date.must_equal "May 16"
|
54
|
+
form.songs[1].date.must_equal "May 16"
|
55
|
+
form.songs[1].composer.date.must_equal "May 16"
|
56
|
+
form.songs[1].wont_be_kind_of(Instrument)
|
57
|
+
form.songs[1].composer.must_be_kind_of(Instrument)
|
58
|
+
form.songs[1].composer.instrument.must_equal "Violins"
|
59
|
+
form.artist.date.must_equal "May 16"
|
60
|
+
end
|
61
|
+
end
|