disposable 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -5
  3. data/CHANGES.md +4 -0
  4. data/Gemfile +1 -1
  5. data/README.md +154 -1
  6. data/database.sqlite3 +0 -0
  7. data/disposable.gemspec +7 -7
  8. data/gemfiles/Gemfile.rails-3.0.lock +10 -8
  9. data/gemfiles/Gemfile.rails-3.2.lock +9 -7
  10. data/gemfiles/Gemfile.rails-4.0.lock +9 -7
  11. data/gemfiles/Gemfile.rails-4.1.lock +10 -8
  12. data/lib/disposable.rb +6 -7
  13. data/lib/disposable/callback.rb +174 -0
  14. data/lib/disposable/composition.rb +21 -58
  15. data/lib/disposable/expose.rb +49 -0
  16. data/lib/disposable/twin.rb +85 -38
  17. data/lib/disposable/twin/builder.rb +12 -30
  18. data/lib/disposable/twin/changed.rb +50 -0
  19. data/lib/disposable/twin/collection.rb +95 -0
  20. data/lib/disposable/twin/composition.rb +43 -15
  21. data/lib/disposable/twin/option.rb +1 -1
  22. data/lib/disposable/twin/persisted.rb +20 -0
  23. data/lib/disposable/twin/property_processor.rb +29 -0
  24. data/lib/disposable/twin/representer.rb +42 -14
  25. data/lib/disposable/twin/save.rb +19 -34
  26. data/lib/disposable/twin/schema.rb +31 -0
  27. data/lib/disposable/twin/setup.rb +38 -0
  28. data/lib/disposable/twin/sync.rb +114 -0
  29. data/lib/disposable/version.rb +1 -1
  30. data/test/api_semantics_test.rb +263 -0
  31. data/test/callback_group_test.rb +222 -0
  32. data/test/callbacks_test.rb +450 -0
  33. data/test/example.rb +40 -0
  34. data/test/expose_test.rb +92 -0
  35. data/test/persisted_test.rb +101 -0
  36. data/test/test_helper.rb +64 -0
  37. data/test/twin/benchmarking.rb +33 -0
  38. data/test/twin/builder_test.rb +32 -0
  39. data/test/twin/changed_test.rb +108 -0
  40. data/test/twin/collection_test.rb +223 -0
  41. data/test/twin/composition_test.rb +56 -25
  42. data/test/twin/expose_test.rb +73 -0
  43. data/test/twin/feature_test.rb +61 -0
  44. data/test/twin/from_test.rb +37 -0
  45. data/test/twin/inherit_test.rb +57 -0
  46. data/test/twin/option_test.rb +27 -0
  47. data/test/twin/readable_test.rb +57 -0
  48. data/test/twin/save_test.rb +192 -0
  49. data/test/twin/schema_test.rb +69 -0
  50. data/test/twin/setup_test.rb +139 -0
  51. data/test/twin/skip_unchanged_test.rb +64 -0
  52. data/test/twin/struct_test.rb +168 -0
  53. data/test/twin/sync_option_test.rb +228 -0
  54. data/test/twin/sync_test.rb +128 -0
  55. data/test/twin/twin_test.rb +49 -128
  56. data/test/twin/writeable_test.rb +56 -0
  57. metadata +106 -20
  58. data/STUFF +0 -4
  59. data/lib/disposable/twin/finders.rb +0 -29
  60. data/lib/disposable/twin/new.rb +0 -30
  61. data/lib/disposable/twin/save_.rb +0 -21
  62. data/test/composition_test.rb +0 -102
@@ -0,0 +1,64 @@
1
+ require "test_helper"
2
+
3
+ class SkipUnchangedTest < MiniTest::Spec
4
+ module Model
5
+ Song = Struct.new(:title, :composer)
6
+ Album = Struct.new(:name, :songs, :artist)
7
+ Artist = Struct.new(:name)
8
+ end
9
+
10
+
11
+ module Twin
12
+ class Album < Disposable::Twin
13
+ feature Setup
14
+ feature Sync
15
+ feature Sync::SkipUnchanged
16
+
17
+ property :name
18
+
19
+ collection :songs do
20
+ property :title
21
+
22
+ property :composer do
23
+ property :name
24
+ end
25
+ end
26
+
27
+ property :artist do
28
+ property :name
29
+ end
30
+ end
31
+ end
32
+
33
+
34
+ let (:song) { Model::Song.new() }
35
+ let (:composer) { Model::Artist.new(nil) }
36
+ let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
37
+ let (:artist) { Model::Artist.new("Bad Religion") }
38
+
39
+
40
+ let (:album) { Model::Album.new("30 Years Live", [song, song_with_composer], artist) }
41
+
42
+ it do
43
+ twin = Twin::Album.new(album)
44
+
45
+ twin.songs[1].composer.name = "Greg Graffin"
46
+ twin.songs[0].title = "Resist Stance"
47
+
48
+ # raises exceptions when setters are called.
49
+ album.instance_eval { def name=; end }
50
+ artist.instance_eval { def name=; end }
51
+ song_with_composer.instance_eval { def title=; end }
52
+
53
+ twin.sync
54
+
55
+ # unchanged, and no exception raised.
56
+ album.name.must_equal "30 Years Live"
57
+ song_with_composer.title.must_equal "American Jesus"
58
+ artist.name.must_equal "Bad Religion"
59
+
60
+ # this actually got synced.
61
+ song_with_composer.composer.name.must_equal "Greg Graffin" # was nil.
62
+ song.title.must_equal "Resist Stance" # was nil.
63
+ end
64
+ end
@@ -0,0 +1,168 @@
1
+ # require 'test_helper'
2
+ # require "representable/debug"
3
+
4
+ # require 'disposable/twin/struct'
5
+
6
+
7
+
8
+ # module Representable
9
+ # # The generic representer. Brings #to_hash and #from_hash to your object.
10
+ # # If you plan to write your own representer for a new media type, try to use this module (e.g., check how JSON reuses Hash's internal
11
+ # # architecture).
12
+ # module Object
13
+ # def self.included(base)
14
+ # base.class_eval do
15
+ # include Representable
16
+ # extend ClassMethods
17
+ # register_feature Representable::Object
18
+ # end
19
+ # end
20
+
21
+
22
+ # module ClassMethods
23
+ # def collection_representer_class
24
+ # Collection
25
+ # end
26
+ # end
27
+
28
+ # def from_object(data, options={}, binding_builder=Binding)
29
+ # update_properties_from(data, options, binding_builder)
30
+ # end
31
+
32
+ # # FIXME: remove me! only here to avoid AllowSymbols from Twin:Representer
33
+ # def update_properties_from(doc, options, format)
34
+ # representable_mapper(format, options).deserialize(doc, options)
35
+ # end
36
+ # end
37
+ # end
38
+
39
+
40
+
41
+
42
+
43
+ # class TwinStructTest < MiniTest::Spec
44
+ # class Song < Disposable::Twin
45
+ # include Struct
46
+ # property :number, default: 1 # FIXME: this should be :default_if_nil so it becomes clear with a model.
47
+ # option :cool?
48
+ # end
49
+
50
+ # # empty hash
51
+ # it { Song.new({}).number.must_equal 1 }
52
+ # # model hash
53
+ # it { Song.new(number: 2).number.must_equal 2 }
54
+
55
+ # # with hash and options as one hash.
56
+ # it { Song.new(number: 3, cool?: true).cool?.must_equal true }
57
+ # it { Song.new(number: 3, cool?: true).number.must_equal 3 }
58
+
59
+ # # with model hash and options hash separated.
60
+ # it { Song.new({number: 3}, {cool?: true}).cool?.must_equal true }
61
+ # it { Song.new({number: 3}, {cool?: true}).number.must_equal 3 }
62
+
63
+
64
+ # describe "writing" do
65
+ # let (:song) { Song.new(model, {cool?: true}) }
66
+ # let (:model) { {number: 3} }
67
+
68
+ # # writer
69
+ # it do
70
+ # song.number = 9
71
+ # song.number.must_equal 9
72
+ # model[:number].must_equal 3
73
+ # end
74
+
75
+ # # writer with sync
76
+ # it do
77
+ # song.number = 9
78
+ # song.sync
79
+
80
+ # song.number.must_equal 9
81
+ # model["number"].must_equal 9
82
+
83
+ # song.send(:model).object_id.must_equal model.object_id
84
+ # end
85
+ # end
86
+
87
+ # end
88
+
89
+
90
+ # # Non-lazy initialization. This will copy all properties from the wrapped object to the twin when
91
+ # # instantiating the twin.
92
+
93
+
94
+ # class TwinWithNestedStructTest < MiniTest::Spec
95
+ # class Song < Disposable::Twin
96
+ # include Setup
97
+ # property :title
98
+
99
+ # property :options, twin: true do # don't call #to_hash, this is triggered in the twin's constructor.
100
+ # include Struct
101
+ # property :recorded
102
+ # property :released
103
+
104
+ # property :preferences, twin: true do
105
+ # include Struct
106
+ # property :show_image
107
+ # property :play_teaser
108
+ # end
109
+ # end
110
+ # end
111
+
112
+ # # FIXME: test with missing hash properties, e.g. without released and with released:false.
113
+ # let (:model) { OpenStruct.new(title: "Seed of Fear and Anger", options: {recorded: true, released: 1,
114
+ # preferences: {show_image: true, play_teaser: 2}}) }
115
+
116
+ # # public "hash" reader
117
+ # it { Song.new(model).options.recorded.must_equal true }
118
+
119
+ # # public "hash" writer
120
+ # it ("xxx") {
121
+ # song = Song.new(model)
122
+
123
+ # puts song.inspect
124
+
125
+ # # puts song.options.inspect
126
+ # # puts song.options.preferences.to_hash
127
+ # # raise
128
+
129
+ # song.options.recorded = "yo"
130
+ # song.options.recorded.must_equal "yo"
131
+
132
+ # song.options.preferences.show_image.must_equal true
133
+ # song.options.preferences.play_teaser.must_equal 2
134
+
135
+ # song.options.preferences.show_image= 9
136
+
137
+
138
+ # # song.extend(Disposable::Twin::Struct::Sync)
139
+ # song.sync # this is only called on the top model, e.g. in Reform#save.
140
+
141
+ # model.title.must_equal "Seed of Fear and Anger"
142
+ # model.options["recorded"].must_equal "yo"
143
+ # model.options["preferences"].must_equal({"show_image" => 9, "play_teaser"=>2})
144
+ # }
145
+ # end
146
+
147
+
148
+
149
+ # class SyncRepresenter < Representable::Decorator
150
+ # include Representable::Object
151
+
152
+ # property :title
153
+ # property :album, instance: lambda { |fragment, *| fragment } do
154
+ # property :name
155
+ # end
156
+ # end
157
+
158
+ # album = Struct.new(:name).new("Ass It Is")
159
+
160
+ # SyncRepresenter.new(obj = Struct.new(:title, :album).new).from_object(Struct.new(:title, :album).new("Eternal Scream", album))
161
+
162
+ # puts obj.title.inspect
163
+ # puts obj.inspect
164
+ # # reform
165
+ # # sync: twin.title = "Good Bye"
166
+ # # album.sync (copy attributes in nested form)
167
+ # # twin.name = "Matters"
168
+ # # save: twin.save (this will do twin.sync... does that call save on all nested twins, too, or do we still have to do that in reform?)
@@ -0,0 +1,228 @@
1
+ # require 'test_helper'
2
+ # require "disposable/twin/changed"
3
+
4
+ # class SyncOptionTest < MiniTest::Spec
5
+ # module Model
6
+ # Song = Struct.new(:title, :composer)
7
+ # Album = Struct.new(:id, :name, :songs, :artist)
8
+ # Artist = Struct.new(:name, :hidden_taste)
9
+ # end
10
+
11
+
12
+ # module Twin
13
+ # class Album < Disposable::Twin
14
+ # feature Setup
15
+ # feature Sync
16
+ # # feature Changed
17
+
18
+ # property :id, sync: lambda { |value, options| nil }
19
+ # property :name, sync: lambda { |value, options| value == "Run For Cover" ? model.name= "processed+#{value}" : model.name= "#{value}+unprocessed" } # assign if album name is "Run For Cover".
20
+
21
+ # collection :songs ,
22
+ # sync: lambda { |value, options| } do # FIXME: this is called before the songs items where synced?
23
+ # property :title, sync: lambda { |value, options| value == "Empty Rooms" ? model.title= "++#{value}" : nil }
24
+ # end
25
+
26
+ # property :artist, sync: lambda { |twin, options| twin.model.hidden_taste = "Ska" } do
27
+ # property :name
28
+ # end
29
+ # end
30
+ # end
31
+
32
+
33
+
34
+ # # sync does NOT call setter.
35
+ # describe ":sync allows you conditionals and is run in twin context" do
36
+ # let (:album) { Model::Album.new(1, "Corridors Of Power", [song, song_with_composer], artist) }
37
+ # let (:song) { Model::Song.new() }
38
+ # let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
39
+ # let (:composer) { Model::Artist.new(nil) }
40
+ # let (:artist) { Model::Artist.new("Bad Religion") }
41
+
42
+ # let (:twin) { Twin::Album.new(album) }
43
+
44
+ # it do
45
+ # album.instance_exec { def id=(*); raise "don't call me!"; end }
46
+ # song.instance_exec { def title=(*); raise "don't call me!"; end }
47
+
48
+ # # triggers :sync.
49
+ # twin.name = "Run For Cover"
50
+
51
+ # twin.songs[1].title = "Empty Rooms" # song_with_composer. this will trigger the #title= writer.
52
+ # twin.artist.name = "Gary Moore"
53
+
54
+ # twin.sync # name is still "Corridors Of Power"
55
+
56
+ # # Album#id :sync was called, nothing happened.
57
+ # album.id.must_equal 1
58
+ # # Album#name :sync was called, first case.
59
+ # album.name.must_equal "processed+Run For Cover"
60
+ # # Song#title :sync called but returns nil.
61
+ # song.title.must_equal nil
62
+ # # Song#title :sync called and processed, first case.
63
+ # song_with_composer.title.must_equal "++Empty Rooms"
64
+
65
+ # # Artist#name :sync was called.
66
+ # artist.name.must_equal "Gary Moore"
67
+ # # Album#artist :sync was called.
68
+ # artist.hidden_taste.must_equal "Ska"
69
+ # end
70
+
71
+ # # trigger second case of Album#name to make sure conditionals work.
72
+ # it do
73
+ # twin.name = "This is not: Run For Cover" # triggers :sync, second case.
74
+ # twin.sync
75
+ # album.name.must_equal "This is not: Run For Cover+unprocessed"
76
+ # end
77
+ # end
78
+ # end
79
+
80
+
81
+ # class SyncWithDynamicOptionsTest < MiniTest::Spec
82
+ # module Model
83
+ # Song = Struct.new(:title, :composer)
84
+ # Album = Struct.new(:id, :name, :songs, :artist)
85
+ # Artist = Struct.new(:name, :hidden_taste)
86
+ # end
87
+
88
+
89
+ # module Twin
90
+ # class Album < Disposable::Twin
91
+ # feature Setup
92
+ # feature Sync
93
+ # # feature Changed
94
+
95
+ # property :id, sync: true
96
+ # property :name, sync: true
97
+
98
+ # collection :songs ,
99
+ # sync: lambda { |value, options| } do # FIXME: this is called before the songs items where synced?
100
+ # property :title, sync: lambda { |value, options| value == "Empty Rooms" ? model.title= "++#{value}" : nil }
101
+ # end
102
+
103
+ # property :artist, sync: true do
104
+ # property :name
105
+ # end
106
+ # end
107
+ # end
108
+
109
+
110
+ # # sync does NOT call setter.
111
+ # describe ":sync allows you conditionals and is run in twin context" do
112
+ # let (:album) { Model::Album.new(1, "Corridors Of Power", [song, song_with_composer], artist) }
113
+ # let (:song) { Model::Song.new() }
114
+ # let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
115
+ # let (:composer) { Model::Artist.new(nil) }
116
+ # let (:artist) { Model::Artist.new("Bad Religion") }
117
+
118
+ # let (:twin) { Twin::Album.new(album) }
119
+
120
+ # it do
121
+ # album.instance_exec { def id=(*); raise "don't call me!"; end }
122
+ # song.instance_exec { def title=(*); raise "don't call me!"; end }
123
+
124
+ # # triggers :sync.
125
+ # twin.name = "Run For Cover"
126
+
127
+ # twin.songs[1].title = "Empty Rooms" # song_with_composer. this will trigger the #title= writer.
128
+ # twin.artist.name = "Gary Moore"
129
+
130
+ # twin.sync(
131
+ # id: lambda { |value, options| nil },
132
+ # name: lambda { |value, options| options.user_options[:twin].model.name= "processed+#{value}" },
133
+
134
+ # artist: lambda { |twin, options| twin.model.hidden_taste = "Ska" },
135
+ # )
136
+
137
+ # # Album#id :sync was called, nothing happened.
138
+ # album.id.must_equal 1
139
+ # # Album#name :sync was called, first case.
140
+ # album.name.must_equal "processed+Run For Cover"
141
+ # # Song#title :sync called but returns nil.
142
+ # song.title.must_equal nil
143
+ # # Song#title :sync called and processed, first case.
144
+ # #### song_with_composer.title.must_equal "++Empty Rooms"
145
+
146
+ # # Artist#name :sync was called.
147
+ # artist.name.must_equal "Gary Moore"
148
+ # # Album#artist :sync was called.
149
+ # artist.hidden_taste.must_equal "Ska"
150
+ # end
151
+ # end
152
+ # end
153
+
154
+
155
+ # class SyncWithOptionsAndSkipUnchangedTest < MiniTest::Spec
156
+ # module Model
157
+ # Song = Struct.new(:title, :composer)
158
+ # Album = Struct.new(:id, :name, :songs, :artist)
159
+ # Artist = Struct.new(:name)
160
+ # end
161
+
162
+
163
+ # module Twin
164
+ # class Album < Disposable::Twin
165
+ # feature Setup
166
+ # feature Sync
167
+ # feature Sync::SkipUnchanged
168
+
169
+ # property :id
170
+ # property :name
171
+
172
+ # collection :songs do # FIXME: this is called before the songs items where synced?
173
+ # property :title
174
+ # end
175
+
176
+ # # only execute this when changed.
177
+ # property :artist, sync: lambda { |artist_twin, options| model.artist.name = "#{artist_twin.name}+" } do
178
+ # property :name
179
+ # end
180
+ # end
181
+ # end
182
+
183
+ # let (:album) { Model::Album.new(1, "Corridors Of Power", [], artist) }
184
+ # # let (:song) { Model::Song.new() }
185
+ # # let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
186
+ # # let (:composer) { Model::Artist.new(nil) }
187
+ # let (:artist) { Model::Artist.new("Bad Religion") }
188
+
189
+ # it do
190
+ # twin = Twin::Album.new(album)
191
+
192
+ # twin.artist.name.must_equal "Bad Religion"
193
+ # twin.sync
194
+ # twin.artist.name.must_equal "Bad Religion"
195
+
196
+ # twin.artist.name= "Greg Howe"
197
+ # twin.sync
198
+ # artist.name.must_equal "Greg Howe+"
199
+ # end
200
+ # end
201
+
202
+ # # :virtual wins over :sync
203
+ # # class SyncWithVirtualTest < MiniTest::Spec
204
+ # # Song = Struct.new(:title, :image, :band)
205
+ # # Band = Struct.new(:name)
206
+
207
+ # # let (:form) { HitForm.new(song) }
208
+ # # let (:song) { Song.new("Injection", Object, Band.new("Rise Against")) }
209
+
210
+ # # class HitForm < Disposable::Twin
211
+ # # include Sync::SkipUnchanged
212
+ # # register_feature Sync::SkipUnchanged
213
+
214
+ # # property :image, sync: lambda { |value, *| model.image = "processed via :sync: #{value}" }
215
+ # # property :band do
216
+ # # property :name, sync: lambda { |value, *| model.name = "band, processed: #{value}" }, virtual: true
217
+ # # end
218
+ # # end
219
+
220
+ # # it "abc" do
221
+ # # form.validate("image" => "Funny photo of Steve Harris", "band" => {"name" => "Iron Maiden"}).must_equal true
222
+
223
+ # # form.sync
224
+ # # song.image.must_equal "processed via :sync: Funny photo of Steve Harris"
225
+ # # song.band.name.must_equal "Rise Against"
226
+ # # end
227
+ # # end
228
+