disposable 0.1.0 → 0.1.1
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/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
|