disposable 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +4 -0
- data/Gemfile +1 -1
- data/README.md +321 -60
- data/database.sqlite3 +0 -0
- data/lib/disposable/twin/property_processor.rb +6 -4
- data/lib/disposable/twin/setup.rb +13 -1
- data/lib/disposable/twin/struct.rb +33 -4
- data/lib/disposable/twin/sync.rb +16 -3
- data/lib/disposable/version.rb +1 -1
- data/test/skip_getter_test.rb +37 -0
- data/test/skip_setter_test.rb +30 -0
- data/test/twin/struct_test.rb +72 -141
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 333ff0a256285692e2f30e1dd908b671a8f93a0a
|
4
|
+
data.tar.gz: ff98cc474bd5b508066d459489c711643d6164e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ca99470a8d3eed471d107648d17fb8d9d3525e4191cb2d635404c5c4e9dcacf10c6fefec61ea4835a6a38a7083667a33f131a0fc442e6418c7d549e9f8c1dc1
|
7
|
+
data.tar.gz: c56cb7f98015746abf7ce43d05d6479e0bfa2b7c25df41a2c41f64fd3519c9613e08d755d335cb2128bc7a0b29f5b4e14dd5530bd6cf83c9b521217ad7475c21
|
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,19 +4,26 @@ _Decorators on top of your ORM layer._
|
|
4
4
|
|
5
5
|
## Introduction
|
6
6
|
|
7
|
-
Disposable
|
7
|
+
Disposable is the missing API of ActiveRecord*. The mission:
|
8
8
|
|
9
|
-
|
9
|
+
* Maintain a manipulatable object graph that is a copy/map of a persistent structure.
|
10
|
+
* Prevent any write to the persistence layer until you say `sync`.
|
11
|
+
* Help designing your domain layer without being restricted to database layouts ([renaming](#renaming), [compositions](#composition), [hash fields](#struct)).
|
12
|
+
* Provide additional behavior like [change tracking](#change-tracking), [imperative callbacks](#imperative-callbacks) and [collection semantics](#collection-semantics).
|
10
13
|
|
11
|
-
Twins are an integral part of the [Trailblazer](https://github.com/apotonick/trailblazer) architectural style which provides clean layering of concerns.
|
12
14
|
|
13
|
-
|
15
|
+
Disposable gives you "_Twins_": non-persistent domain objects. That is reflected in the name of the gem. They can read from and write values to a persistent object and abstract the persistence layer until data is synced to the model.
|
14
16
|
|
15
|
-
##
|
17
|
+
## API
|
16
18
|
|
17
|
-
The
|
19
|
+
The public twin API is unbelievably simple.
|
20
|
+
|
21
|
+
1. `Twin::new` creates and populates the twin.
|
22
|
+
1. `Twin#"reader"` returns the value or nested twin of the property.
|
23
|
+
1. `Twin#"writer"=(v)` writes the value to the twin, not the model.
|
24
|
+
1. `Twin#sync` writes all values to the model.
|
25
|
+
1. `Twin#save` writes all values to the model and calls `save` on configured models.
|
18
26
|
|
19
|
-
Twins may contain validations, nevertheless, in Trailblazer, validations (or "Contracts") sit one layer above. They still can be part of your domain, though.
|
20
27
|
|
21
28
|
## Twin
|
22
29
|
|
@@ -24,78 +31,317 @@ Twins are only # FIXME % slower than AR alone.
|
|
24
31
|
|
25
32
|
Twins implement light-weight decorators objects with a unified interface. They map objects, hashes, and compositions of objects, along with optional hashes to inject additional options.
|
26
33
|
|
27
|
-
|
34
|
+
Every twin is based on a defined schema.
|
28
35
|
|
29
36
|
```ruby
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
## Definition
|
37
|
+
class AlbumTwin < Disposable::Twin
|
38
|
+
property :title
|
39
|
+
property :playable?, virtual: true # context-sensitive, e.g. current_user dependent.
|
34
40
|
|
35
|
-
|
41
|
+
collection :songs do
|
42
|
+
property :name
|
43
|
+
property :index
|
44
|
+
end
|
36
45
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
property :length
|
41
|
-
option :good?
|
46
|
+
property :artist do
|
47
|
+
property :full_name
|
48
|
+
end
|
42
49
|
end
|
43
50
|
```
|
44
51
|
|
45
|
-
##
|
52
|
+
## Constructor
|
46
53
|
|
47
|
-
|
54
|
+
Twins get populated from the decorated models.
|
48
55
|
|
49
56
|
```ruby
|
50
|
-
|
57
|
+
Song = Struct.new(:name, :index)
|
58
|
+
Artist = Struct.new(:full_name)
|
59
|
+
Album = Struct.new(:title, :songs, :artist)
|
51
60
|
```
|
52
61
|
|
53
|
-
|
62
|
+
You need to pass model and the facultative options to the twin constructor.
|
54
63
|
|
55
|
-
|
64
|
+
```ruby
|
65
|
+
album = Album.new("Nice Try")
|
66
|
+
twin = AlbumTwin.new(album, playable?: current_user.can?(:play))
|
67
|
+
```
|
68
|
+
|
69
|
+
## Readers
|
56
70
|
|
57
71
|
This will create a composition object of the actual model and the hash.
|
58
72
|
|
59
73
|
```ruby
|
60
|
-
twin.title
|
61
|
-
twin.
|
74
|
+
twin.title #=> "Nice Try"
|
75
|
+
twin.playable? #=> true
|
62
76
|
```
|
63
77
|
|
64
78
|
You can also override `property` values in the constructor:
|
65
79
|
|
66
80
|
```ruby
|
67
|
-
twin =
|
81
|
+
twin = AlbumTwin.new(album, title: "Plasticash")
|
68
82
|
twin.title #=> "Plasticash"
|
69
83
|
```
|
70
84
|
|
71
|
-
|
85
|
+
## Writers
|
86
|
+
|
87
|
+
Writers change values on the twin and are _not_ propagated to the model.
|
72
88
|
|
73
|
-
|
89
|
+
```ruby
|
90
|
+
twin.title = "Skamobile"
|
91
|
+
twin.title #=> "Skamobile"
|
92
|
+
album.title #=> "Nice Try"
|
93
|
+
```
|
74
94
|
|
75
|
-
|
95
|
+
Writers on nested twins will "twin" the value.
|
76
96
|
|
77
|
-
|
97
|
+
```ruby
|
98
|
+
twin.songs #=> []
|
99
|
+
twin.songs << Song.new("Adondo", 1)
|
100
|
+
twin.songs #=> [<Twin::Song name="Adondo" index=1 model=<Song ..>>]
|
101
|
+
album.songs #=> []
|
102
|
+
```
|
78
103
|
|
79
|
-
|
104
|
+
The added twin is _not_ passed to the model. Note that the nested song is a twin, not the model itself.
|
80
105
|
|
81
|
-
|
106
|
+
## Sync
|
107
|
+
|
108
|
+
Given the above state change on the twin, here is what happens after calling `#sync`.
|
82
109
|
|
83
110
|
```ruby
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
111
|
+
album.title #=> "Nice Try"
|
112
|
+
album.songs #=> []
|
113
|
+
|
114
|
+
twin.sync
|
115
|
+
|
116
|
+
album.title #=> "Skamobile"
|
117
|
+
album.songs #=> [<Song name="Adondo" index=1>]
|
118
|
+
```
|
119
|
+
|
120
|
+
`#sync` writes all configured attributes back to the models using public setters as `album.name=` or `album.songs=`. This is recursive and will sync the entire object graph.
|
121
|
+
|
122
|
+
Note that `sync` might already trigger saving the model as persistence layers like ActiveRecord can't deal with `collection= []` and instantly persist that.
|
123
|
+
|
124
|
+
You may implement your syncing manually by passing a block to `sync`.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
twin.sync do |hash|
|
128
|
+
hash #=> {
|
129
|
+
# "title" => "Skamobile",
|
130
|
+
# "playable?" => true,
|
131
|
+
# "songs" => [{"name"=>"Adondo"...}..]
|
132
|
+
# }
|
88
133
|
end
|
89
134
|
```
|
90
135
|
|
91
|
-
|
136
|
+
Invoking `sync` with block will _not_ write anything to the models.
|
137
|
+
|
138
|
+
Needs to be included explicitly (`Sync`).
|
139
|
+
|
140
|
+
## Save
|
141
|
+
|
142
|
+
Calling `#save` will do `sync` plus calling `save` on all nested models. This implies that the models need to implement `#save`.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
twin.save
|
146
|
+
#=> album.save
|
147
|
+
#=> .songs[0].save
|
148
|
+
|
149
|
+
```
|
150
|
+
|
151
|
+
Needs to be included explicitly (`Save`).
|
152
|
+
|
153
|
+
## Nested Twin
|
154
|
+
|
155
|
+
Nested objects can be declared with an inline twin.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
property :artist do
|
159
|
+
property :full_name
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
The setter will automatically "twin" the model.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
twin.artist = Artist.new
|
167
|
+
twin.artist #=> <Twin::Artist model=<Artist ..>>
|
168
|
+
```
|
169
|
+
|
170
|
+
You can also specify nested objects with an explicit class.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
property :artist, twin: TwinArtist
|
174
|
+
```
|
175
|
+
|
176
|
+
## Collections
|
177
|
+
|
178
|
+
Collections can be defined analogue to `property`. The exposed API is the `Array` API.
|
179
|
+
|
180
|
+
* `twin.songs = [..]` will override the existing value and "twin" every item.
|
181
|
+
* `twin.songs << Song.new` will add and twin.
|
182
|
+
* `twin.insert(0, Song.new)` will insert at the specified position and twin.
|
183
|
+
|
184
|
+
You can also delete, replace and move items.
|
185
|
+
|
186
|
+
* `twin.songs.delete( twin.songs[0] )`
|
187
|
+
|
188
|
+
None of these operations are propagated to the model.
|
189
|
+
|
190
|
+
## Collection Semantics
|
191
|
+
|
192
|
+
In addition to the standard `Array` API the collection adds a handful of additional semantics.
|
193
|
+
|
194
|
+
* `songs=`, `songs<<` and `songs.insert` track twin via `#added`.
|
195
|
+
* `songs.delete` tracks via `#deleted`.
|
196
|
+
* `twin.destroy( twin.songs[0] )` deletes the twin and marks it for destruction in `#to_destroy`.
|
197
|
+
* `twin.songs.save` will call `destroy` on all models marked for destruction in `to_destroy`. Tracks destruction via `#destroyed`.
|
198
|
+
|
199
|
+
Again, the model is left alone until you call `sync` or `save`.
|
200
|
+
|
201
|
+
## Change Tracking
|
202
|
+
|
203
|
+
The `Changed` module will allow tracking of state changes in all properties, even nested structures.
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class AlbumTwin < Disposable::Twin
|
207
|
+
feature Changed
|
208
|
+
```
|
209
|
+
|
210
|
+
Now, consider the following operations.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
twin.name = "Skamobile"
|
214
|
+
twin.songs << Song.new("Skate", 2) # this adds second song.
|
215
|
+
```
|
216
|
+
|
217
|
+
This results in the following tracking results.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
twin.changed? #=> true
|
221
|
+
twin.changed?(:name) #=> true
|
222
|
+
twin.changed?(:playable?) #=> false
|
223
|
+
twin.songs.changed? #=> true
|
224
|
+
twin.songs[0].changed? #=> false
|
225
|
+
twin.songs[1].changed? #=> true
|
226
|
+
```
|
227
|
+
|
228
|
+
Assignments from the constructor are _not_ tracked as changes.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
twin = AlbumTwin.new(album)
|
232
|
+
twin.changed? #=> false
|
233
|
+
```
|
234
|
+
|
235
|
+
## Persistance Tracking
|
236
|
+
|
237
|
+
The `Persisted` module will track the `persisted?` field of the model, implying that your model exposes this field.
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
twin.persisted? #=> false
|
241
|
+
twin.save
|
242
|
+
twin.persisted? #=> true
|
243
|
+
```
|
244
|
+
|
245
|
+
The `persisted?` field is a copy of the model's persisted? flag.
|
246
|
+
|
247
|
+
You can also use `created?` to find out whether a twin's model was already persisted or just got created in this session.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
twin = AlbumTwin.new(Album.create) # assuming we were using ActiveRecord.
|
251
|
+
twin.created? #=> false
|
252
|
+
twin.save
|
253
|
+
twin.created? #=> false
|
254
|
+
```
|
255
|
+
|
256
|
+
This will only return true when the `persisted?` field has flipped.
|
257
|
+
|
258
|
+
## Renaming
|
259
|
+
|
260
|
+
The `Expose` module allows renaming properties.
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
class AlbumTwin < Disposable::Twin
|
264
|
+
feature Expose
|
265
|
+
|
266
|
+
property :song_title, from: :title
|
267
|
+
```
|
268
|
+
|
269
|
+
The public accessor is now `song_title` whereas the model's accessor needs to be `title`.
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
album = OpenStruct.new(title: "Run For Cover")
|
273
|
+
AlbumTwin.new(album).song_title #=> "Run For Cover"
|
274
|
+
```
|
275
|
+
|
276
|
+
## Composition
|
277
|
+
|
278
|
+
Compositions of objects can be mapped, too.
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class AlbumTwin < Disposable::Twin
|
282
|
+
feature Composition
|
283
|
+
|
284
|
+
property :id, on: :album
|
285
|
+
property :title, on: :album
|
286
|
+
property :songs, on: :cd
|
287
|
+
property :cd_id, on: :cd, from: :id
|
288
|
+
```
|
289
|
+
|
290
|
+
When initializing a composition, you have to pass a hash that contains the composees.
|
92
291
|
|
93
292
|
```ruby
|
94
|
-
|
293
|
+
AlbumTwin.new(album: album, cd: CD.find(1))
|
95
294
|
```
|
96
295
|
|
296
|
+
Note that renaming works here, too.
|
297
|
+
|
298
|
+
## Struct
|
299
|
+
|
300
|
+
Twins can also map hash properties, e.g. from a deeply nested serialized JSON column.
|
97
301
|
|
98
|
-
|
302
|
+
```ruby
|
303
|
+
album.permissions #=> {admin: {read: true, write: true}, user: {destroy: false}}
|
304
|
+
```
|
305
|
+
|
306
|
+
Map that using the `Struct` module.
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
class AlbumTwin < Disposable::Twin
|
310
|
+
property :permissions do
|
311
|
+
include Struct
|
312
|
+
property :admin do
|
313
|
+
include Struct
|
314
|
+
property :read
|
315
|
+
property :write
|
316
|
+
end
|
317
|
+
|
318
|
+
property :user # you don't have to use Struct everywhere!
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
You get fully object-oriented access to your properties.
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
twin.permissions.admin.read #=> true
|
326
|
+
```
|
327
|
+
|
328
|
+
Note that you do not have to use `Struct` everywhere.
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
twin.permissions.user #=> {destroy: false}
|
332
|
+
```
|
333
|
+
|
334
|
+
Of course, this works for writing, too.
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
twin.permissions.admin.read = :MAYBE
|
338
|
+
```
|
339
|
+
|
340
|
+
After `sync`ing, you will find a hash in the model.
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
album.permissions #=> {admin: {read: :MAYBE, write: true}, user: {destroy: false}}
|
344
|
+
```
|
99
345
|
|
100
346
|
## With Representers
|
101
347
|
|
@@ -103,33 +349,52 @@ they indirect data, the twin's attributes get assigned without writing to the pe
|
|
103
349
|
|
104
350
|
## With Contracts
|
105
351
|
|
106
|
-
##
|
352
|
+
## Overriding Getter for Presentation
|
107
353
|
|
108
|
-
|
354
|
+
You can override getters for presentation.
|
109
355
|
|
110
356
|
```ruby
|
111
357
|
class AlbumTwin < Disposable::Twin
|
112
|
-
|
358
|
+
property :title
|
113
359
|
|
360
|
+
def title
|
361
|
+
super.upcase
|
362
|
+
end
|
114
363
|
end
|
115
364
|
```
|
116
365
|
|
117
|
-
|
366
|
+
Be careful, though. The getter normally is also called in `sync` when writing properties to the models.
|
367
|
+
|
368
|
+
You can skip invocation of getters in `sync` and read values from `@fields` directly by including `Sync::SkipGetter`.
|
118
369
|
|
119
|
-
|
370
|
+
```ruby
|
371
|
+
class AlbumTwin < Disposable::Twin
|
372
|
+
feature Sync
|
373
|
+
feature Sync::SkipGetter
|
374
|
+
```
|
120
375
|
|
121
|
-
|
122
|
-
* `#insert(i, model)`, see `#<<`.
|
123
|
-
* `#delete(twin)`, removes twin from collection and tracks via `#deleted`.
|
124
|
-
* `#destroy(twin)`, removes twin from collection and tracks via `#deleted` and `#to_destroy` for destruction in `#save`.
|
376
|
+
## Manual Coercion
|
125
377
|
|
126
|
-
|
378
|
+
You can override setters for manual coercion.
|
127
379
|
|
128
|
-
|
380
|
+
```ruby
|
381
|
+
class AlbumTwin < Disposable::Twin
|
382
|
+
property :title
|
129
383
|
|
130
|
-
|
384
|
+
def title=(v)
|
385
|
+
super(v.trim)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
```
|
131
389
|
|
132
|
-
|
390
|
+
Be careful, though. The setter normally is also called in `setup` when copying properties from the models to the twin.
|
391
|
+
|
392
|
+
Analogue to `SkipGetter`, include `Setup::SkipSetter` to write values directly to `@fields`.
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
class AlbumTwin < Disposable::Twin
|
396
|
+
feature Setup::SkipSetter
|
397
|
+
```
|
133
398
|
|
134
399
|
|
135
400
|
## Imperative Callbacks
|
@@ -235,18 +500,14 @@ This will add the `rewind!` callback to the `songs` property, resulting in the f
|
|
235
500
|
|
236
501
|
```ruby
|
237
502
|
collection :songs do
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
503
|
+
on_add :notify_album!
|
504
|
+
on_add :reset_song!
|
505
|
+
on_delete :rewind!
|
506
|
+
end
|
242
507
|
```
|
243
508
|
|
244
509
|
## Builders
|
245
510
|
|
246
|
-
## Overriding Accessors
|
247
|
-
|
248
|
-
super
|
249
|
-
|
250
511
|
## Used In
|
251
512
|
|
252
513
|
* [Reform](https://github.com/apotonick/reform) forms are based on twins and add a little bit of form decoration on top. Every nested form is a twin.
|
data/database.sqlite3
CHANGED
Binary file
|
@@ -4,8 +4,10 @@
|
|
4
4
|
# For a scalar property, this will be run once and yield the property's value.
|
5
5
|
# For a collection, this is run per item and yields the item.
|
6
6
|
class Disposable::Twin::PropertyProcessor
|
7
|
-
def initialize(definition, twin)
|
8
|
-
|
7
|
+
def initialize(definition, twin, value=nil)
|
8
|
+
value ||= twin.send(definition.getter)
|
9
|
+
@definition = definition
|
10
|
+
@value = value
|
9
11
|
end
|
10
12
|
|
11
13
|
def call(&block)
|
@@ -19,11 +21,11 @@ class Disposable::Twin::PropertyProcessor
|
|
19
21
|
private
|
20
22
|
def collection!
|
21
23
|
# FIXME: the nil collection is not tested, yet!
|
22
|
-
(@
|
24
|
+
(@value || []).collect { |nested_twin| yield(nested_twin) }
|
23
25
|
end
|
24
26
|
|
25
27
|
def property!
|
26
|
-
twin = @
|
28
|
+
twin = @value or return nil
|
27
29
|
nested_model = yield(twin)
|
28
30
|
end
|
29
31
|
end
|
@@ -27,12 +27,24 @@ module Disposable
|
|
27
27
|
name = dfn.name
|
28
28
|
value = options[name.to_sym] || mapper.send(name) # model.title.
|
29
29
|
|
30
|
-
|
30
|
+
setup_write!(dfn, value)
|
31
31
|
end
|
32
32
|
|
33
33
|
@fields.merge!(options) # FIXME: hash/string. # FIXME: call writer!!!!!!!!!!
|
34
34
|
# from_hash(options) # assigns known properties from options.
|
35
35
|
end
|
36
|
+
|
37
|
+
def setup_write!(dfn, value)
|
38
|
+
send(dfn.setter, value)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Including this will _not_ use the property's setter in Setup and allow you to override it.
|
43
|
+
module SkipSetter
|
44
|
+
def setup_write!(dfn, value)
|
45
|
+
write_property(dfn.name, value, dfn)
|
46
|
+
end
|
47
|
+
end
|
36
48
|
end # Setup
|
37
49
|
end
|
38
50
|
end
|
@@ -1,14 +1,43 @@
|
|
1
|
-
module Disposable
|
1
|
+
module Disposable
|
2
2
|
class Twin
|
3
3
|
# Twin that uses a hash to populate.
|
4
4
|
#
|
5
5
|
# Twin.new(id: 1)
|
6
6
|
module Struct
|
7
|
-
def
|
8
|
-
|
7
|
+
def setup_properties!(model, options={})
|
8
|
+
hash_representer.new(self).from_hash(model.merge(options))
|
9
|
+
end
|
10
|
+
|
11
|
+
def hash_representer
|
12
|
+
Class.new(schema) do
|
13
|
+
include Representable::Hash
|
14
|
+
include Representable::Hash::AllowSymbols
|
15
|
+
|
16
|
+
representable_attrs.each do |dfn|
|
17
|
+
dfn.merge!(
|
18
|
+
prepare: lambda { |model, *| model },
|
19
|
+
instance: lambda { |model, *| model }, # FIXME: this is because Representable thinks this is typed? in Deserializer.
|
20
|
+
representable: false,
|
21
|
+
) if dfn[:twin]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def sync_hash_representer
|
27
|
+
hash_representer.clone.tap do |rpr|
|
28
|
+
rpr.representable_attrs.each do |dfn|
|
29
|
+
dfn.merge!(
|
30
|
+
serialize: lambda { |model, *| model.sync! },
|
31
|
+
representable: true
|
32
|
+
) if dfn[:twin]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
9
36
|
|
10
|
-
|
37
|
+
def sync(options={})
|
38
|
+
sync_hash_representer.new(self).to_hash
|
11
39
|
end
|
40
|
+
alias_method :sync!, :sync
|
12
41
|
end
|
13
42
|
end
|
14
43
|
end
|
data/lib/disposable/twin/sync.rb
CHANGED
@@ -20,12 +20,14 @@ class Disposable::Twin
|
|
20
20
|
options_for_sync = sync_options(Decorator::Options[options])
|
21
21
|
|
22
22
|
schema.each(options_for_sync) do |dfn|
|
23
|
+
property_value = sync_read(dfn) #
|
24
|
+
|
23
25
|
unless dfn[:twin]
|
24
|
-
mapper.send(dfn.setter,
|
26
|
+
mapper.send(dfn.setter, property_value) # always sync the property
|
25
27
|
next
|
26
28
|
end
|
27
29
|
|
28
|
-
nested_model = PropertyProcessor.new(dfn, self).() { |twin| twin.sync!({}) }
|
30
|
+
nested_model = PropertyProcessor.new(dfn, self, property_value).() { |twin| twin.sync!({}) }
|
29
31
|
|
30
32
|
next if nested_model.nil?
|
31
33
|
|
@@ -35,11 +37,14 @@ class Disposable::Twin
|
|
35
37
|
model
|
36
38
|
end
|
37
39
|
|
40
|
+
private
|
38
41
|
def self.included(includer)
|
39
42
|
includer.extend ToNestedHash::ClassMethods
|
40
43
|
end
|
41
44
|
|
42
|
-
|
45
|
+
def sync_read(definition)
|
46
|
+
send(definition.getter)
|
47
|
+
end
|
43
48
|
|
44
49
|
module ToNestedHash
|
45
50
|
def to_nested_hash(*)
|
@@ -110,5 +115,13 @@ class Disposable::Twin
|
|
110
115
|
super
|
111
116
|
end
|
112
117
|
end
|
118
|
+
|
119
|
+
|
120
|
+
# Include this won't use the getter #title in #sync but read directly from @fields.
|
121
|
+
module SkipGetter
|
122
|
+
def sync_read(dfn)
|
123
|
+
@fields[dfn.name]
|
124
|
+
end
|
125
|
+
end
|
113
126
|
end
|
114
127
|
end
|
data/lib/disposable/version.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SkipGetterTest < MiniTest::Spec
|
4
|
+
Album = Struct.new(:title, :artist)
|
5
|
+
Artist = Struct.new(:name)
|
6
|
+
|
7
|
+
class AlbumTwin < Disposable::Twin
|
8
|
+
feature Sync
|
9
|
+
feature Sync::SkipGetter
|
10
|
+
|
11
|
+
property :title
|
12
|
+
property :artist do
|
13
|
+
property :name
|
14
|
+
|
15
|
+
def name
|
16
|
+
super.upcase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def title
|
21
|
+
super.reverse
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it do
|
26
|
+
album = Album.new("Wild Frontier", Artist.new("Gary Moore"))
|
27
|
+
twin = AlbumTwin.new(album)
|
28
|
+
|
29
|
+
twin.title.must_equal "reitnorF dliW"
|
30
|
+
twin.artist.name.must_equal "GARY MOORE"
|
31
|
+
|
32
|
+
twin.sync # does NOT call getter.
|
33
|
+
|
34
|
+
album.title.must_equal "Wild Frontier"
|
35
|
+
album.artist.name.must_equal "Gary Moore"
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SkipSetterTest < MiniTest::Spec
|
4
|
+
Album = Struct.new(:title, :artist)
|
5
|
+
Artist = Struct.new(:name)
|
6
|
+
|
7
|
+
class AlbumTwin < Disposable::Twin
|
8
|
+
feature Setup::SkipSetter
|
9
|
+
|
10
|
+
property :title
|
11
|
+
property :artist do
|
12
|
+
property :name
|
13
|
+
|
14
|
+
def name=(v)
|
15
|
+
super(v.upcase)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def title=(v)
|
20
|
+
super(v.reverse)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it do
|
25
|
+
twin = AlbumTwin.new(Album.new("Wild Frontier", Artist.new("Gary Moore")))
|
26
|
+
|
27
|
+
twin.title.must_equal "Wild Frontier"
|
28
|
+
twin.artist.name.must_equal "Gary Moore"
|
29
|
+
end
|
30
|
+
end
|
data/test/twin/struct_test.rb
CHANGED
@@ -1,168 +1,99 @@
|
|
1
|
-
|
1
|
+
require 'test_helper'
|
2
2
|
# require "representable/debug"
|
3
3
|
|
4
|
-
|
4
|
+
require 'disposable/twin/struct'
|
5
5
|
|
6
|
+
class TwinStructTest < MiniTest::Spec
|
7
|
+
class Song < Disposable::Twin
|
8
|
+
include Struct
|
9
|
+
property :number#, default: 1 # FIXME: this should be :default_if_nil so it becomes clear with a model.
|
10
|
+
property :cool?
|
11
|
+
end
|
6
12
|
|
13
|
+
# empty hash
|
14
|
+
# it { Song.new({}).number.must_equal 1 }
|
15
|
+
it { Song.new({}).number.must_equal nil } # TODO: implement default.
|
7
16
|
|
8
|
-
#
|
9
|
-
|
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
|
17
|
+
# model hash
|
18
|
+
it { Song.new(number: 2).number.must_equal 2 }
|
20
19
|
|
20
|
+
# with hash and options as one hash.
|
21
|
+
it { Song.new(number: 3, cool?: true).cool?.must_equal true }
|
22
|
+
it { Song.new(number: 3, cool?: true).number.must_equal 3 }
|
21
23
|
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
# end
|
26
|
-
# end
|
24
|
+
# with model hash and options hash separated.
|
25
|
+
it { Song.new({number: 3}, {cool?: true}).cool?.must_equal true }
|
26
|
+
it { Song.new({number: 3}, {cool?: true}).number.must_equal 3 }
|
27
27
|
|
28
|
-
# def from_object(data, options={}, binding_builder=Binding)
|
29
|
-
# update_properties_from(data, options, binding_builder)
|
30
|
-
# end
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# end
|
36
|
-
# end
|
37
|
-
# end
|
29
|
+
describe "writing" do
|
30
|
+
let (:song) { Song.new(model, {cool?: true}) }
|
31
|
+
let (:model) { {number: 3} }
|
38
32
|
|
33
|
+
# writer
|
34
|
+
it do
|
35
|
+
song.number = 9
|
36
|
+
song.number.must_equal 9
|
37
|
+
model[:number].must_equal 3
|
38
|
+
end
|
39
39
|
|
40
|
+
# writer with sync
|
41
|
+
it do
|
42
|
+
song.number = 9
|
43
|
+
model = song.sync
|
40
44
|
|
45
|
+
song.number.must_equal 9
|
46
|
+
model["number"].must_equal 9
|
41
47
|
|
48
|
+
# song.send(:model).object_id.must_equal model.object_id
|
49
|
+
end
|
50
|
+
end
|
42
51
|
|
43
|
-
|
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
|
52
|
+
end
|
49
53
|
|
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
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
class TwinWithNestedStructTest < MiniTest::Spec
|
56
|
+
class Song < Disposable::Twin
|
57
|
+
property :title
|
58
|
+
include Sync
|
58
59
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
60
|
+
property :options, twin: true do # don't call #to_hash, this is triggered in the twin's constructor.
|
61
|
+
include Struct
|
62
|
+
property :recorded
|
63
|
+
property :released
|
62
64
|
|
65
|
+
property :preferences, twin: true do
|
66
|
+
include Struct
|
67
|
+
property :show_image
|
68
|
+
property :play_teaser
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
63
72
|
|
64
|
-
#
|
65
|
-
|
66
|
-
|
73
|
+
# FIXME: test with missing hash properties, e.g. without released and with released:false.
|
74
|
+
let (:model) { OpenStruct.new(title: "Seed of Fear and Anger", options: {recorded: true, released: 1,
|
75
|
+
preferences: {show_image: true, play_teaser: 2}}) }
|
67
76
|
|
68
|
-
#
|
69
|
-
|
70
|
-
# song.number = 9
|
71
|
-
# song.number.must_equal 9
|
72
|
-
# model[:number].must_equal 3
|
73
|
-
# end
|
77
|
+
# public "hash" reader
|
78
|
+
it { Song.new(model).options.recorded.must_equal true }
|
74
79
|
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
# song.sync
|
80
|
+
# public "hash" writer
|
81
|
+
it ("xxx") {
|
82
|
+
song = Song.new(model)
|
79
83
|
|
80
|
-
|
81
|
-
|
84
|
+
song.options.recorded = "yo"
|
85
|
+
song.options.recorded.must_equal "yo"
|
82
86
|
|
83
|
-
|
84
|
-
|
85
|
-
# end
|
87
|
+
song.options.preferences.show_image.must_equal true
|
88
|
+
song.options.preferences.play_teaser.must_equal 2
|
86
89
|
|
87
|
-
|
90
|
+
song.options.preferences.show_image= 9
|
88
91
|
|
89
92
|
|
90
|
-
|
91
|
-
# # instantiating the twin.
|
93
|
+
song.sync # this is only called on the top model, e.g. in Reform#save.
|
92
94
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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?)
|
95
|
+
model.title.must_equal "Seed of Fear and Anger"
|
96
|
+
model.options["recorded"].must_equal "yo"
|
97
|
+
model.options["preferences"].must_equal({"show_image" => 9, "play_teaser"=>2})
|
98
|
+
}
|
99
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: disposable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Sutterer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uber
|
@@ -165,6 +165,8 @@ files:
|
|
165
165
|
- test/example.rb
|
166
166
|
- test/expose_test.rb
|
167
167
|
- test/persisted_test.rb
|
168
|
+
- test/skip_getter_test.rb
|
169
|
+
- test/skip_setter_test.rb
|
168
170
|
- test/test_helper.rb
|
169
171
|
- test/twin/benchmarking.rb
|
170
172
|
- test/twin/builder_test.rb
|
@@ -218,6 +220,8 @@ test_files:
|
|
218
220
|
- test/example.rb
|
219
221
|
- test/expose_test.rb
|
220
222
|
- test/persisted_test.rb
|
223
|
+
- test/skip_getter_test.rb
|
224
|
+
- test/skip_setter_test.rb
|
221
225
|
- test/test_helper.rb
|
222
226
|
- test/twin/benchmarking.rb
|
223
227
|
- test/twin/builder_test.rb
|