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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8eb1cfd5be42eb0cf007c4b9d84aee95b7a4275
4
- data.tar.gz: 76ab1e429806470d007164fd17bdf80d0abeab1a
3
+ metadata.gz: 333ff0a256285692e2f30e1dd908b671a8f93a0a
4
+ data.tar.gz: ff98cc474bd5b508066d459489c711643d6164e1
5
5
  SHA512:
6
- metadata.gz: dba5e116162e6fe0f33ff944ef1dbadb9fb8a5208c790fb92c5c0d68091a7edf83e91b9fca49029d8df00db458fb131e1be6c554293fbfcef23730034ada8602
7
- data.tar.gz: e3d4ac5b51ea771c2444bbdb6b34bcfa05e5b2ff9513050915a470920fd2238e5a639a8ce69225a93cc2a876412759335a0cda068c4b11c0f61aab38ba8603b9
6
+ metadata.gz: 9ca99470a8d3eed471d107648d17fb8d9d3525e4191cb2d635404c5c4e9dcacf10c6fefec61ea4835a6a38a7083667a33f131a0fc442e6418c7d549e9f8c1dc1
7
+ data.tar.gz: c56cb7f98015746abf7ce43d05d6479e0bfa2b7c25df41a2c41f64fd3519c9613e08d755d335cb2128bc7a0b29f5b4e14dd5530bd6cf83c9b521217ad7475c21
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.1.1
2
+
3
+ * Adding `Setup::SkipSetter` and `Sync::SkipGetter`.
4
+
1
5
  # 0.1.0
2
6
 
3
7
  * This is the official first serious release.
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in disposable.gemspec
4
4
  gemspec
5
5
 
6
- gem 'representable', path: '../representable'
6
+ # gem 'representable', path: '../representable'
data/README.md CHANGED
@@ -4,19 +4,26 @@ _Decorators on top of your ORM layer._
4
4
 
5
5
  ## Introduction
6
6
 
7
- Disposable gives you "_Twins_" which are domain objects, decoupled from ActiveRecord, DataMapper or whatever ORM you use.
7
+ Disposable is the missing API of ActiveRecord*. The mission:
8
8
 
9
- Twins are non-persistent domain objects. That is reflected in the name of the gem. However, they can read and write values from a persistent object.
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
- They give you an encapsulated alternative to delegators that many projects use to separate domain and persistence and help you restricting the domain API.
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
- ## Why?
17
+ ## API
16
18
 
17
- The goal is to have one object that delegates reading and writing to underlying object(s). This is a fundamental concept for cells view models, representers, and reform form objects.
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
- Let me show you what I mean.
34
+ Every twin is based on a defined schema.
28
35
 
29
36
  ```ruby
30
- song = Song.create(title: "Savior", length: 242)
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
- Twins need to define every field they expose.
41
+ collection :songs do
42
+ property :name
43
+ property :index
44
+ end
36
45
 
37
- ```ruby
38
- class Song::Twin < Disposable::Twin
39
- property :title
40
- property :length
41
- option :good?
46
+ property :artist do
47
+ property :full_name
48
+ end
42
49
  end
43
50
  ```
44
51
 
45
- ## Creation
52
+ ## Constructor
46
53
 
47
- You need to pass model and the optional options to the twin constructor.
54
+ Twins get populated from the decorated models.
48
55
 
49
56
  ```ruby
50
- twin = Song::Twin.new(song, good?: true)
57
+ Song = Struct.new(:name, :index)
58
+ Artist = Struct.new(:full_name)
59
+ Album = Struct.new(:title, :songs, :artist)
51
60
  ```
52
61
 
53
- ++++++ builders
62
+ You need to pass model and the facultative options to the twin constructor.
54
63
 
55
- ## Reading
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 #=> "Savior"
61
- twin.good? #=> true
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 = Song::Twin.new(song, title: "Plasticash")
81
+ twin = AlbumTwin.new(album, title: "Plasticash")
68
82
  twin.title #=> "Plasticash"
69
83
  ```
70
84
 
71
- Let's talk about what happens to the actual model when setting values?
85
+ ## Writers
86
+
87
+ Writers change values on the twin and are _not_ propagated to the model.
72
88
 
73
- ## Writing
89
+ ```ruby
90
+ twin.title = "Skamobile"
91
+ twin.title #=> "Skamobile"
92
+ album.title #=> "Nice Try"
93
+ ```
74
94
 
75
- It doesn't happen. The model is only queried when _reading_ values. Writing only happens in additional modules: Syncing and Saving is where the values held in the twin are written to the model.
95
+ Writers on nested twins will "twin" the value.
76
96
 
77
- ## Renaming
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
- ## Structs
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
- If you don't have a model but a simple hash, use `Struct`.
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
- class Song::Twin < Disposable::Twin
85
- include Struct
86
- property :title
87
- property :length
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
- Note that a hash goes into the constructor now.
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
- twin = Song::Twin.new(title: "Savior", good?: true)
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
- ## Compositions
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
- ## Collections
352
+ ## Overriding Getter for Presentation
107
353
 
108
- Define collections using `::collection`.
354
+ You can override getters for presentation.
109
355
 
110
356
  ```ruby
111
357
  class AlbumTwin < Disposable::Twin
112
- collection :songs do
358
+ property :title
113
359
 
360
+ def title
361
+ super.upcase
362
+ end
114
363
  end
115
364
  ```
116
365
 
117
- ### API
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
- The API is identical to `Array` with the following additions.
370
+ ```ruby
371
+ class AlbumTwin < Disposable::Twin
372
+ feature Sync
373
+ feature Sync::SkipGetter
374
+ ```
120
375
 
121
- * `#<<(model)` adds item, wraps it in twin and tracks it via `#added`.
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
- ### Semantics
378
+ You can override setters for manual coercion.
127
379
 
128
- Include `Twin::Collection::Semantics`.
380
+ ```ruby
381
+ class AlbumTwin < Disposable::Twin
382
+ property :title
129
383
 
130
- Semantics are extensions to the pure Ruby array behavior and designed to deal with persistence layers like ActiveRecord or ROM.
384
+ def title=(v)
385
+ super(v.trim)
386
+ end
387
+ end
388
+ ```
131
389
 
132
- * `#save` will call `destroy` on all models marked for destruction in `to_destroy`. Tracks destruction via `#destroyed`.
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
- on_add :notify_album!
239
- on_add :reset_song!
240
- on_delete :rewind!
241
- end
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
- @definition, @twin = definition, twin
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
- (@twin.send(@definition.getter) || []).collect { |nested_twin| yield(nested_twin) }
24
+ (@value || []).collect { |nested_twin| yield(nested_twin) }
23
25
  end
24
26
 
25
27
  def property!
26
- twin = @twin.send(@definition.getter) or return nil
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
- send(dfn.setter, value)
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 initialize(model, options={})
8
- super # call from_hash(options) # FIXME: this is wrong and already calls from_hash(options)
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
- from_hash(model.merge(options))
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
@@ -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, send(dfn.getter)) # always sync the property
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
- private
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
@@ -1,3 +1,3 @@
1
1
  module Disposable
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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
@@ -1,168 +1,99 @@
1
- # require 'test_helper'
1
+ require 'test_helper'
2
2
  # require "representable/debug"
3
3
 
4
- # require 'disposable/twin/struct'
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
- # 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
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
- # module ClassMethods
23
- # def collection_representer_class
24
- # Collection
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
- # # 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
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
- # 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
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
- # # 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 }
55
+ class TwinWithNestedStructTest < MiniTest::Spec
56
+ class Song < Disposable::Twin
57
+ property :title
58
+ include Sync
58
59
 
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 }
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
- # describe "writing" do
65
- # let (:song) { Song.new(model, {cool?: true}) }
66
- # let (:model) { {number: 3} }
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
- # # writer
69
- # it do
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
- # # writer with sync
76
- # it do
77
- # song.number = 9
78
- # song.sync
80
+ # public "hash" writer
81
+ it ("xxx") {
82
+ song = Song.new(model)
79
83
 
80
- # song.number.must_equal 9
81
- # model["number"].must_equal 9
84
+ song.options.recorded = "yo"
85
+ song.options.recorded.must_equal "yo"
82
86
 
83
- # song.send(:model).object_id.must_equal model.object_id
84
- # end
85
- # end
87
+ song.options.preferences.show_image.must_equal true
88
+ song.options.preferences.play_teaser.must_equal 2
86
89
 
87
- # end
90
+ song.options.preferences.show_image= 9
88
91
 
89
92
 
90
- # # Non-lazy initialization. This will copy all properties from the wrapped object to the twin when
91
- # # instantiating the twin.
93
+ song.sync # this is only called on the top model, e.g. in Reform#save.
92
94
 
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?)
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.0
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-26 00:00:00.000000000 Z
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