https://github.com/apotonick/disposable
Decorators on top of your ORM layer.
https://github.com/apotonick/disposable
objectmapper
Last synced: 11 months ago
JSON representation
Decorators on top of your ORM layer.
- Host: GitHub
- URL: https://github.com/apotonick/disposable
- Owner: apotonick
- License: mit
- Created: 2013-11-20T00:31:01.000Z (over 12 years ago)
- Default Branch: master
- Last Pushed: 2024-12-31T15:48:49.000Z (about 1 year ago)
- Last Synced: 2025-04-13T13:58:46.526Z (12 months ago)
- Topics: objectmapper
- Language: Ruby
- Homepage:
- Size: 637 KB
- Stars: 172
- Watchers: 10
- Forks: 38
- Open Issues: 31
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGES.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# Disposable
_Decorators on top of your ORM layer._
[](https://gitter.im/trailblazer/chat)
[](http://trailblazer.to/newsletter/)
[](https://travis-ci.org/apotonick/disposable)
[](http://badge.fury.io/rb/disposable)
## Introduction
Disposable is the missing API of ActiveRecord*. The mission:
* Maintain a manipulatable object graph that is a copy/map of a persistent structure.
* Prevent any write to the persistence layer until you say `sync`.
* Help designing your domain layer without being restricted to database layouts ([renaming](#renaming), [compositions](#composition), [hash fields](#struct)).
* Provide additional behavior like [change tracking](#change-tracking), [imperative callbacks](#imperative-callbacks) and [collection semantics](#collection-semantics).
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.
## API
The public twin API is unbelievably simple.
1. `Twin::new` creates and populates the twin.
1. `Twin#"reader"` returns the value or nested twin of the property.
1. `Twin#"writer"=(v)` writes the value to the twin, not the model.
1. `Twin#sync` writes all values to the model.
1. `Twin#save` writes all values to the model and calls `save` on configured models.
## Twin
Twins are only # FIXME % slower than AR alone.
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.
Every twin is based on a defined schema.
```ruby
class AlbumTwin < Disposable::Twin
property :title
property :playable?, virtual: true # context-sensitive, e.g. current_user dependent.
collection :songs do
property :name
property :index
end
property :artist do
property :full_name
end
end
```
## Constructor
Twins get populated from the decorated models.
```ruby
Song = Struct.new(:name, :index)
Artist = Struct.new(:full_name)
Album = Struct.new(:title, :songs, :artist)
```
You need to pass model and the facultative options to the twin constructor.
```ruby
album = Album.new("Nice Try")
twin = AlbumTwin.new(album, playable?: current_user.can?(:play))
```
## Readers
This will create a composition object of the actual model and the hash.
```ruby
twin.title #=> "Nice Try"
twin.playable? #=> true
```
You can also override `property` values in the constructor:
```ruby
twin = AlbumTwin.new(album, title: "Plasticash")
twin.title #=> "Plasticash"
```
## Writers
Writers change values on the twin and are _not_ propagated to the model.
```ruby
twin.title = "Skamobile"
twin.title #=> "Skamobile"
album.title #=> "Nice Try"
```
Writers on nested twins will "twin" the value.
```ruby
twin.songs #=> []
twin.songs << Song.new("Adondo", 1)
twin.songs #=> [>]
album.songs #=> []
```
The added twin is _not_ passed to the model. Note that the nested song is a twin, not the model itself.
## Sync
Given the above state change on the twin, here is what happens after calling `#sync`.
```ruby
album.title #=> "Nice Try"
album.songs #=> []
twin.sync
album.title #=> "Skamobile"
album.songs #=> []
```
`#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.
Note that `sync` might already trigger saving the model as persistence layers like ActiveRecord can't deal with `collection= []` and instantly persist that.
You may implement your syncing manually by passing a block to `sync`.
```ruby
twin.sync do |hash|
hash #=> {
# "title" => "Skamobile",
# "playable?" => true,
# "songs" => [{"name"=>"Adondo"...}..]
# }
end
```
Invoking `sync` with block will _not_ write anything to the models.
Needs to be included explicitly (`Sync`).
## Save
Calling `#save` will do `sync` plus calling `save` on all nested models. This implies that the models need to implement `#save`.
```ruby
twin.save
#=> album.save
#=> .songs[0].save
```
Needs to be included explicitly (`Save`).
## Nested Twin
Nested objects can be declared with an inline twin.
```ruby
property :artist do
property :full_name
end
```
The setter will automatically "twin" the model.
```ruby
twin.artist = Artist.new
twin.artist #=> >
```
You can also specify nested objects with an explicit class.
```ruby
property :artist, twin: TwinArtist
```
## Unnest
# todo: document
## Features
You can simply `include` feature modules into twins. If you want a feature to be included into all inline twins of your schema, use `::feature`.
```ruby
class AlbumTwin < Disposable::Twin
feature Coercion
property :artist do
# this will now include Coercion, too.
```
## Coercion
Twins can use [dry-types](https://github.com/dry-rb/dry-types) coercion. This will override the setter in your twin, coerce the incoming value, and call the original setter. _Nothing more_ will happen.
Disposable already defines a module `Disposable::Twin::Coercion::Types` with all the Dry::Types built-in types. So you can use any of the types documented in http://dry-rb.org/gems/dry-types/built-in-types/.
```ruby
class AlbumTwin < Disposable::Twin
feature Coercion
feature Setup::SkipSetter
property :id, type: Types::Params::Integer
```
The `:type` option defines the coercion type. You may incluce `Setup::SkipSetter`, too, as otherwise the coercion will happen at initialization time and in the setter.
```ruby
twin.id = "1"
twin.id #=> 1
```
Again, coercion only happens in the setter.
## Defaults
Default values can be set via `:default`.
```ruby
class AlbumTwin < Disposable::Twin
feature Default
property :title, default: "The Greatest Songs Ever Written"
property :composer, default: Composer.new do
property :name, default: -> { "Object-#{id}" }
end
end
```
Default value is applied when the model's getter returns `nil` when _initializing_ the twin.
Note that `:default` also works with `:virtual` and `readable: false`. `:default` can also be a lambda which is then executed in twin context.
## Collections
Collections can be defined analogue to `property`. The exposed API is the `Array` API.
* `twin.songs = [..]` will override the existing value and "twin" every item.
* `twin.songs << Song.new` will add and twin.
* `twin.insert(0, Song.new)` will insert at the specified position and twin.
You can also delete, replace and move items.
* `twin.songs.delete( twin.songs[0] )`
None of these operations are propagated to the model.
## Collection Semantics
In addition to the standard `Array` API the collection adds a handful of additional semantics.
* `songs=`, `songs<<` and `songs.insert` track twin via `#added`.
* `songs.delete` tracks via `#deleted`.
* `twin.destroy( twin.songs[0] )` deletes the twin and marks it for destruction in `#to_destroy`.
* `twin.songs.save` will call `destroy` on all models marked for destruction in `to_destroy`. Tracks destruction via `#destroyed`.
Again, the model is left alone until you call `sync` or `save`.
## Twin Collections
To twin a collection of models, you can use `::from_collection`.
```ruby
SongTwin.from_collection([song, song])
```
This will decorate every song instance using a fresh twin.
## Change Tracking
The `Changed` module will allow tracking of state changes in all properties, even nested structures.
```ruby
class AlbumTwin < Disposable::Twin
feature Changed
```
Now, consider the following operations.
```ruby
twin.name = "Skamobile"
twin.songs << Song.new("Skate", 2) # this adds second song.
```
This results in the following tracking results.
```ruby
twin.changed? #=> true
twin.changed?(:name) #=> true
twin.changed?(:playable?) #=> false
twin.songs.changed? #=> true
twin.songs[0].changed? #=> false
twin.songs[1].changed? #=> true
```
Assignments from the constructor are _not_ tracked as changes.
```ruby
twin = AlbumTwin.new(album)
twin.changed? #=> false
```
## Persistance Tracking
The `Persisted` module will track the `persisted?` field of the model, implying that your model exposes this field.
```ruby
twin.persisted? #=> false
twin.save
twin.persisted? #=> true
```
The `persisted?` field is a copy of the model's persisted? flag.
You can also use `created?` to find out whether a twin's model was already persisted or just got created in this session.
```ruby
twin = AlbumTwin.new(Album.create) # assuming we were using ActiveRecord.
twin.created? #=> false
twin.save
twin.created? #=> false
```
This will only return true when the `persisted?` field has flipped.
## Renaming
The `Expose` module allows renaming properties.
```ruby
class AlbumTwin < Disposable::Twin
feature Expose
property :song_title, from: :title
```
The public accessor is now `song_title` whereas the model's accessor needs to be `title`.
```ruby
album = OpenStruct.new(title: "Run For Cover")
AlbumTwin.new(album).song_title #=> "Run For Cover"
```
## Composition
Compositions of objects can be mapped, too.
```ruby
class AlbumTwin < Disposable::Twin
include Composition
property :id, on: :album
property :title, on: :album
property :songs, on: :cd
property :cd_id, on: :cd, from: :id
```
When initializing a composition, you have to pass a hash that contains the composees.
```ruby
AlbumTwin.new(album: album, cd: CD.find(1))
```
Note that renaming works here, too.
## Struct
Twins can also map hash properties, e.g. from a deeply nested serialized JSON column.
```ruby
album.permissions #=> {admin: {read: true, write: true}, user: {destroy: false}}
```
Map that using the `Struct` module.
```ruby
class AlbumTwin < Disposable::Twin
property :permissions do
include Struct
property :admin do
include Struct
property :read
property :write
end
property :user # you don't have to use Struct everywhere!
end
```
You get fully object-oriented access to your properties.
```ruby
twin.permissions.admin.read #=> true
```
Note that you do not have to use `Struct` everywhere.
```ruby
twin.permissions.user #=> {destroy: false}
```
Of course, this works for writing, too.
```ruby
twin.permissions.admin.read = :MAYBE
```
After `sync`ing, you will find a hash in the model.
```ruby
album.permissions #=> {admin: {read: :MAYBE, write: true}, user: {destroy: false}}
```
## With Representers
they indirect data, the twin's attributes get assigned without writing to the persistence layer, yet.
## With Contracts
## Overriding Getter for Presentation
You can override getters for presentation.
```ruby
class AlbumTwin < Disposable::Twin
property :title
def title
super.upcase
end
end
```
Be careful, though. The getter normally is also called in `sync` when writing properties to the models.
You can skip invocation of getters in `sync` and read values from `@fields` directly by including `Sync::SkipGetter`.
```ruby
class AlbumTwin < Disposable::Twin
feature Sync
feature Sync::SkipGetter
```
## Manual Coercion
You can override setters for manual coercion.
```ruby
class AlbumTwin < Disposable::Twin
property :title
def title=(v)
super(v.trim)
end
end
```
Be careful, though. The setter normally is also called in `setup` when copying properties from the models to the twin.
Analogue to `SkipGetter`, include `Setup::SkipSetter` to write values directly to `@fields`.
```ruby
class AlbumTwin < Disposable::Twin
feature Setup::SkipSetter
```
## Imperative Callbacks
Please refer to the [full documentation](http://trailblazer.to/gems/disposable/callback.html).
Note: [Chapter 8 of the Trailblazer](http://leanpub.com/trailblazer) book is dedicated to callbacks and discusses them in great detail.
Callbacks use the fact that twins track state changes. This allows to execute callbacks on certain conditions.
```ruby
Callback.new(twin).on_create { |twin| .. }
Callback.new(twin.songs).on_add { |twin| .. }
Callback.new(twin.songs).on_add { |twin| .. }
```
It works as follows.
1. Twins track state changes, like _"item added to collection (`on_add`)"_ or _"property changed (`on_change`)"_.
2. You decide when to invoke one or a group of callbacks. This is why there's no `before_save` and the like anymore.
3. You also decide _what_ events to consider by calling the respective events only, like `on_add`.
4. The `Callback` will now find out which properties of the twin are affected and exectue your passed code for each of them.
This is called _Imperative Callback_ and the opposite of what you've learned from Rails.
By inversing the control, we don't need `before_` or `after_`. This is in your hands now and depends on where you invoke your callbacks.
## Events
The following events are available in `Callback`.
Don't confuse that with event triggering, though! Callbacks are passive, calling an event method means the callback will look for twins that have tracked the respective event (e.g. an twin has `change`d).
* `on_update`: Invoked when the underlying model was persisted, yet, at twin initialization and attributes have changed since then.
* `on_add`: For every twin that has been added to a collection.
* `on_add(:create)`: For every twin that has been added to a collection and got persisted. This will only pick up collection items after `sync` or `save`.
* `on_delete`: For every item that has been deleted from a collection.
* `on_destroy`: For every item that has been removed from a collection and physically destroyed.
* `on_change`: For every item that has changed attributes. When `persisted?` has flippend, this will be triggered, too.
* `on_change(:email)`: When the scalar field changed.
## Callback Groups
`Callback::Group` simplifies grouping callbacks and allows nesting.
```ruby
class AfterSave < Disposable::Callback::Group
on_change :expire_cache!
collection :songs do
on_add :notify_album!
on_add :reset_song!
end
on_update :rehash_name!, property: :title
property :artist do
on_change :sing!
end
end
```
Calling that group on a twin will invoke all callbacks that apply, in the order they were added.
```ruby
AfterSave.new(twin).(context: self)
```
Methods like `:sing!` will be invoked on the `:context` object. Likewise, nested properties will be retrieved by simply calling the getter on the twin, like `twin.songs`.
An options hash is passed as the second argument. # TODO: document Group.(operation: Object.new).
Again, only the events that match will be invoked. If the top level twin hasn't changed, `expire_cache!` won't be invoked. This works by simply using `Callback` under the hood.
## Callback Inheritance
You can inherit groups, add and remove callbacks.
```ruby
class EnhancedAfterSave < AfterSave
on_change :redo!
collection :songs do
on_add :rewind!
end
remove! :on_change, :expire_cache!
end
```
The callbacks will be _appended_ to the existing chain.
Instead of appending, you may also refine existing callbacks.
```ruby
class EnhancedAfterSave < AfterSave
collection :songs, inherit: true do
on_delete :rewind!
end
end
```
This will add the `rewind!` callback to the `songs` property, resulting in the following chain.
```ruby
collection :songs do
on_add :notify_album!
on_add :reset_song!
on_delete :rewind!
end
```
## Readable, Writeable, Virtual
Properties can have various access settings.
* `readable: false` won't read from the model in `Setup`.
* `writeable: false` won't write to model in `Sync`.
* `virtual: true` is both settings above combined.
## Options
To inject context data into a twin that is not part of any model, you can simply use `:virtual` properties.
```ruby
class AlbumTwin < Disposable::Twin
property :title
property :current_user, virtual: true
end
```
You can now pass the `current_user` as an option into the constructor and then access it via the reader.
```ruby
twin = AlbumTwin.new(album, current_user: User.find(1))
twin.current_user #=>
```
## Parent
By using the `Parent` feature you can access the parent twin of a nested one.
```ruby
class AlbumTwin < Disposable::Twin
feature Parent
property :artist do
property :name
end
end
```
Use `parent` to grab the nested's container twin.
```ruby
twin = AlbumTwin.new(Album.new(artist: Artist.new))
twin.artist.parent #=> twin
```
Note that this will internally add a `parent` property.
## Builders
## Used In
* [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.
* [Trailblazer](https://github.com/apotonick/trailblazer) uses twins as decorators and callbacks in operations to structure business logic.
## Development
* `rake test` runs all tests without `builder_test.rb`. For the latter, run `BUNDLE_GEMFILE=Gemfile_builder_test.rb bundle exec rake test_builder`