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.
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
+