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