disposable 0.0.9 → 0.1.0
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.
- 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
|