GithubHelp home page GithubHelp logo

disposable's Introduction

Disposable

Decorators on top of your ORM layer.

Gitter Chat TRB Newsletter Build Status Gem Version

Introduction

Disposable is the missing API of ActiveRecord*. The mission:

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.
  2. Twin#"reader" returns the value or nested twin of the property.
  3. Twin#"writer"=(v) writes the value to the twin, not the model.
  4. Twin#sync writes all values to the model.
  5. 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.

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.

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.

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.

twin.title     #=> "Nice Try"
twin.playable? #=> true

You can also override property values in the constructor:

twin = AlbumTwin.new(album, title: "Plasticash")
twin.title #=> "Plasticash"

Writers

Writers change values on the twin and are not propagated to the model.

twin.title = "Skamobile"
twin.title  #=> "Skamobile"
album.title #=> "Nice Try"

Writers on nested twins will "twin" the value.

twin.songs #=> []
twin.songs << Song.new("Adondo", 1)
twin.songs  #=> [<Twin::Song name="Adondo" index=1 model=<Song ..>>]
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.

album.title  #=> "Nice Try"
album.songs #=> []

twin.sync

album.title  #=> "Skamobile"
album.songs #=> [<Song name="Adondo" index=1>]

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

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.

twin.save
#=> album.save
#=>      .songs[0].save

Needs to be included explicitly (Save).

Nested Twin

Nested objects can be declared with an inline twin.

property :artist do
  property :full_name
end

The setter will automatically "twin" the model.

twin.artist = Artist.new
twin.artist #=> <Twin::Artist model=<Artist ..>>

You can also specify nested objects with an explicit class.

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.

class AlbumTwin < Disposable::Twin
  feature Coercion

  property :artist do
    # this will now include Coercion, too.

Coercion

Twins can use 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/.

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.

twin.id = "1"
twin.id #=> 1

Again, coercion only happens in the setter.

Defaults

Default values can be set via :default.

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.

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.

class AlbumTwin < Disposable::Twin
  feature Changed

Now, consider the following operations.

twin.name = "Skamobile"
twin.songs << Song.new("Skate", 2) # this adds second song.

This results in the following tracking results.

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.

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.

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.

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.

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.

album = OpenStruct.new(title: "Run For Cover")
AlbumTwin.new(album).song_title #=> "Run For Cover"

Composition

Compositions of objects can be mapped, too.

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.

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.

album.permissions #=> {admin: {read: true, write: true}, user: {destroy: false}}

Map that using the Struct module.

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.

twin.permissions.admin.read #=> true

Note that you do not have to use Struct everywhere.

twin.permissions.user #=> {destroy: false}

Of course, this works for writing, too.

twin.permissions.admin.read = :MAYBE

After syncing, you will find a hash in the model.

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.

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.

class AlbumTwin < Disposable::Twin
  feature Sync
  feature Sync::SkipGetter

Manual Coercion

You can override setters for manual coercion.

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.

class AlbumTwin < Disposable::Twin
  feature Setup::SkipSetter

Imperative Callbacks

Please refer to the full documentation.

Note: Chapter 8 of the 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.

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 changed).

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

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.

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.

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.

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.

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.

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.

twin = AlbumTwin.new(album, current_user: User.find(1))
twin.current_user #=> <User id:1>

Parent

By using the Parent feature you can access the parent twin of a nested one.

class AlbumTwin < Disposable::Twin
  feature Parent

  property :artist do
    property :name
  end
end

Use parent to grab the nested's container twin.

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 forms are based on twins and add a little bit of form decoration on top. Every nested form is a twin.
  • 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

disposable's People

Contributors

apotonick avatar audionerd avatar bryant1410 avatar emaglio avatar fran-worley avatar hermanverschooten avatar monkbroc avatar niels avatar petergoldstein avatar richardboehme avatar seuros avatar timoschilling avatar yogeshjain999 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

disposable's Issues

Nilify true on a collection of strings

class TradeInConfigurationForm < Disposable::Twin
  include Property::Hash

  property :options, field: :hash do
    collection :active_features, nilify: true
  end
end

config = EmbedConfigurations::TradeIn.find(id)
twin = TradeInConfigurationForm.new(config)
twin.options.active_features = ["lead_form", ""]

twin.options.active_features
#=> ["lead_form", ""]

I would expect the output to be ["lead_form"]

I'm actually using Reform but the example here is simplified.

This happens especially when using Rails and submitting collection_check_boxes through a form (it appends an empty string so that the update works when de-selecting all check boxes).

Is this not supported or is it a bug?

Order collection

Hi Nick,
I wonder whether it is possible or not to somehow order collection. Something like:

class AlbumTwin < Disposable::Twin
  collection :songs, order: [:by_duration, :by_created] do
    property :name
    property :duration
  end
end

class Album < ActiveRecord::Base
  scope :by_duration, -> { order(by_duration: :asc) }
  scope :by_created, -> { order(by_created: :desc) }
end

Well, I mean, I know that it is not in current API right now, but maybe I could somehow implement desired behavior?

Composition Documentation and Behaviour

Currently, there is no documentation for compositions in the Readme, however looking in the source code, we see that Compositions should be defined as follows

class Album
  include Disposable::Composition

  map( {cd: [[:id], [:name]], band: [[:id, :band_id], [:title]]} )
end

The current version of the Trailblazer book however, alludes to being able to define Compositions like this

class Comment::Opinion < Disposable::Twin
  include Composition

  property :body,  on: :comment
  property :email, on: :author 
end

Which I feel is a much nicer DSL

Is the latter the plan for how compositions will work?

Support sequel association attributes

Sequel by design does not save entire object graphs. Its association modification methods are designed to be very direct and not offer a lot of abstraction.

Rather than creating the records inline, we need to support creating the associated objects and then adding them to the parent.

Another option would be to make use of the nested attributes plugin but that might be even messier...

Twin::Collection can't remove members backed by a DB table with not-null constraint

Suppose I have a Twin A that contains a collection of Twins B. Twin B is backed by a database table. Collection membership is indicated by a twin_a_id column on the Twin B database table. That column has a not-null constraint as no Twin B should ever exist that isn't associated with a Twin A.

Given the above, I can't remove a Twin B from the collection. Twin::Collection always wants to call Twin::Collection#delete, even when actually using #destroy. #delete will try to persist to the database by nullifying twin_a_id, thus violating the not-null constraint, thus raising an exception.

Perhaps #destroy should just destroy the associated Twin (as one might naively expect the name to imply)? Users who actually want to delete, then destroy, would then have to do so manually by calling both methods. Of course this would be a backwards-incompatible change..

Relationship keys

Even if just as an alias it would be nice.

class CartTwin < ...
  relationship :items
end

I know you want to be agnostic, but we're still talking about wrapping models and most models I've seen (mongoid, activemodel, activerecord, etc) have relationships.

item in has_many collection not being destroyed

I have what I believe is a pretty direct translation of the collection item removal from the gemgem code but for some reason the record from the collection is never actually destroyed.

I've put all the related code in this gist and added comments to the item deletion test showing the values I'm seeing in the reform/disposable instance at each stage: https://gist.github.com/kevinansfield/5ac5c612921418ea874a#file-crud_test-rb-L72

Do you have any idea what's causing the deletion to fail? Thanks!

HABTM Association causes sync() to save model.

I've been experimenting today and found that if you have a HABTM association which is set through disposable such as tag_ids then the underlying active record will save itself when the setter for that attribute is invoked.

This means that calling sync() on disposable can inadvertently save the underlying record. This would presumably also happen with has_many associations of a similar nature that AR also likes to autosave.

This is a quirk with AR rather than anything specific to Disposable, but after a Gitter conversation with @apotonick he asked me to file this as a reminder to anyone.

Improve Dirty Tracking API

The dirty tracking API could be made much more useful with some additional methods being provided. Taking from Sequel:

#column_changes
{name: ['old', 'new']}

#initial_value(column)
'old'

#initial_values
{name: 'old', amount: 0}

#reset_column(column)
obj.name => 'old'

Subclassing removes custom accessors

Upon subclassing a Twin that defines custom accessors via instance methods, they get overwritten with the stock implementation. To wit:

class ParentTwin < Disposable::Twin
  property :my_property

  def my_property
    "auto-getter overwritten!"
  end
end

class ChildTwin < ParentTwin
end

parent_twin = ParentTwin.new(OpenStruct.new)
puts parent_twin.my_property.inspect #=> is "auto-getter overwritten!" as expected

child_twin = ChildTwin.new(OpenStruct.new)
puts child_twin.my_property.inspect #=> is nil, but should be "auto-getter overwritten"

I believe that this is due to the stock accessors being included through a module in https://github.com/apotonick/disposable/blob/master/lib/disposable/twin.rb#L52. As the module inclusion happens after the actual class inheritance, the module's (included) methods overwrite the parent classes (inherited) methods.

This is very counter-intuitive and makes subclassing extremely painful in cases where you want to propagate custom methods down the inheritance chain.

I would suggest either revisiting the design choice of including the getters as a module, or delegating to super (if present) from within the module.

Thoughts?

Collection property that doesn't write itself, but still recursively calls save

I'm using a collection property to handle scopes in ActiveRecord. Example:

class Cart
   has_many :items
end

class Item
  belongs_to :product, polymorphic: true
  belongs_to :cart

  scope :shoes, -> { where(product_type: "Shoe") }
end

class Shoe
  has_many :items
end

And my twins:

class CartTwin
  property :items, twin: ItemCollectionTwin
end

class ItemCollectionTwin
  collection :shoes, twin: ShoeItemTwin
end

class ShoeItemTwin
  property :product, twin: ShoeTwin
  property :cart, twin: CartTwin
end

class ShoeTwin
  property :items, twin: ItemCollectionTwin
end

So this all works from a read standpoint. It fails as soon as I sync from either end node. The problem is that *CollectionTwin want to do shoes=, but it's a scope and not a column. If i do collection :shoes, twin: ShoeItemTwin, writeable: false it "syncs" but stops saving recursively at this point.

I need to be able to say "This property/collection has no write, but it still contains something that needs to be synced".

Nested hash field defined by external class doesn't work

Model = Struct.new(:content)

class Nested < Disposable::Twin
  property :nested_property
end

class Outer < Disposable::Twin
  include Property::Hash
  property :content, field: hash, twin: Nested 
end

Outer.new(Model.new({nested_property: 1}))  # will fail with message: 
# NoMethodError: undefined method `nested_property' for {:nested_property=>1}}:Hash

The reason of failing is that the Property::Hash module includes the following three modules (NestedDefaults, Property::Struct, Hash::Sync) in the nested class by means of the feature mechanism which works only for nested fields defined in the block but not in the separate class.

Since this feature behaviour seems to be correct (in general modules should be explicitly included in the classes) I can see two possible ways to solve an issue:

  • use another dedicated way to include these specific modules into the nested class (in this particular case I see it to be appropriated)
  • explicitly include these modules in the nested class. For now it is just a workaround which uses undocumented functions. In order to become a solution it should be documented and preserved from the unannounced changes but actually it looks ugly:
class Nested < Disposable::Twin
  feature Disposable::Twin::Property::Hash::NestedDefaults
  feature Disposable::Twin::Property::Struct
  feature Disposable::Twin::Property::Hash::Sync

  property :nested_property
end

It's nicer to have just one module that should be included and which will do this work. Something like this:

class Nested < Disposable::Twin
  feature Disposable::Twin::Property::Hashable

  property :nested_property
end

"Reverse" sync

Would sync'ing the model to the twin be an anti-pattern? Sometimes it happens that callbacks in the model (yeah I know) would change an attribute, which is a property in the twin which in turn would now be stale.

Accessing properties source without lots of code

My code needs to know which model a property is on, after going through the source, I ended up with:

on_model = form.class.representer_class.representable_attrs[:definitions][property_name][:on]
model = form.object.model[on_model]

I'd really like to just do

form.property_definiton(property_name).model

or something

Using a twin with nested properties to decorate a flat model

Hey Nick, long time no talk, you may not even remember me haha! I've been busy with other stuff and had to pause my work using trailblazer, but I'm back at it now.

Here is the deal, suppose I have the following flat model (no nested objects):

Song = Struct.new(:title, :track, :length)

But from my form I receive this nested hash:

{
  song: {
    title: 'Roxanne',
    details: {
      track: 3,
      length: '4:10'
   }
}

So I have this form that maps nicely to the nested hash:

class SongForm < Reform::Form
  property :title

  property :details do
    property :track
    property :length
  end

  # validations...
end

Now, I don't want to do the mapping nested hash -> flat model directly in my form, the form should not know about this. So I want introduce a twin that the form will use to do the mapping and syncing, like this:

params = {
  song: {
    title: 'Roxanne',
    details: {
      track: 3,
      length: '4:10'
   }
}

twin = SongTwin.new(Song.new)
form = SongForm.new(twin)

if form.validate(params)
  # will sync to the twin and call save on it
  # the twin will sync to the model (mapping the nested hash to the flat model) and then call save it
  form.save
end

So I need something close to what I can do with nested in Representable (in fact, I stole this example from its documentation), but with the twin getters and setters, since I plan on using the twin to implement simple business logic:

class SongTwin < Disposable::Twin
  feature Sync
  feature Nested

  property :title

  nested :details do
    property :track
    property :length
  end

  # business logic using the getters and setters (including the nested ones) 
end

I believe that currently the simpler way to do this would be to override the sync method of the twin:

def sync
  super do |hash|
    model.title = hash[:title]
    model.track = hash[:details][:track]
    model.length = hash[:details][:length]
  end
end

But this is obviously not a nice solution and it doesn't work for reading...

This relates to this Reform issue: trailblazer/reform#277

Rails default validations not catched

class Customer < ApplicationRecord
   belongs_to :organization
end

by default Rails validates this association as required, meaning if you do net set an organization_id, record creation will fail
Rails default validations should be catched by default

Public `build_twin` method

Hello, this method is useful when constructing a default record for button at has_many form, because it does not append the record to base form collection. When constructing a nested twin using any other method I am losing parent property value from Disposable::Twin::Parent. Can you make it public, please?
Hope I have explained the thing I am trying to deal with good enough. I can bring more explanation if not. Thanks.

Breaking uber changes

Gemspec does not lock uber at or under a version number. Uber recently updated to v0.1.0 and introduced breaking changes (undefined method class_builder). Took me a little while to track down...

Collection#find_by does not filter on all options passed

Hi! I don't know if this is intended, but at the very least it might be misleading. It turns out that if you pass a hash of options to Collection#find_by, it does not actually filter the returned records by all the options, but rather only looks at the first key/value pair of options. This led to a (potential and possibly unimportant) bug in some code someone on my team had written, because the find_by method looks like it should behave like ActiveRecord's method, but it does not.

https://github.com/apotonick/disposable/blob/610308800376510344cdba59174fb32a59d5b095/lib/disposable/twin/collection.rb#L17..L20

It might be good to at least document this behavior, if it's not desirable to change it right now. Thanks!

How to set writeable dynamically?

How to set writeable dynamically? (with a block / lambda or...)

Tried these, all act as writeable: true no matter what's in the model

module User
  module Contract
    class Update < Reform::Form
      property :nickname, writeable: -> { model.nickname.blank? }
      property :address, writeable: model_nickname_blank?
      property :phone, writeable: :model_phone_blank

      def model_nickname_blank?
          model.nickname.blank?
      end

      def model_phone_blank
          model.phone.blank?
      end
    end
  end
end

Thanks!

Twin Twin works, Twin Twin Relationship Twin doesn't work

Okay, so the title is a bit screwey, but here's the example:

class CartTwin
  collection :redemptions, twin: RedemptionTwin

  property :discount_cents
end

class RedemptionTwin
  property :cart
end

Okay, everything looks good so far.

cart = Cart.find_by(id: '...')
cart.redemptions.count # => 2
cart.redemptions.map(&:class) # => [Redemption, Redemption]
cart.class # => Cart

twin = CartTwin.new(cart)
twin.redemptions.count # => 2
twin.redemptions.map(&:class) # => [RedemptionTwin, RedemptionTwin]
twin.redemptions.map(&:model).map(&:class) # => [Redemption, Redemption]
twin.class # => CartTwin
twin.model.class # => Cart

Still fine, but now:

shadow = CartTwin.new(twin)
shadow.class # => CartTwin
shadow.model # => Cart

Amazing! Each new subsequent twinning will pick up the correct properties, but know the right model. Perfect, until...

shadow.redemptions.map(&:class) # => [RedemptionTwin, RedemptionTwin]
shadow.redemptions.map(&:model).map(&:class) # => [RedemptionTwin, RedemptionTwin]

Uh-oh.

shadow.redemptions.map(&:model).map(&:model).map(&:class) # => [Redemption, Redemption]

Calling destroy on populator causes 442 Unprocessable Entity error

I have a populator like this, according to this document

populator:->(collection:, index:, fragment:, **) {
item = codes.find_by(id: fragment["id"])
if fragment['id']
codes.delete(item)
return skip!
end
}

I verified that the #destroy was actually called, but this caused an error:

SQL (1.4ms) DELETE FROM codes WHERE codes.id = 14
(2.7ms) ROLLBACK
Completed 422 Unprocessable Entity in 1709ms (ActiveRecord: 23.4ms)

I simplified the table name because of my project's security, but the 'codes' table is the association table of an has_many through association.
I tried to run active record's destroy method in rails console and it did destroy the record from db successfully.
Do you have any idea why this cause the error?

Neither #clone nor #dup produce independent Twin copies (changes to one's properties modify all copies' properties)

On master, as well as at least in versions 0.3.2+, one can neither clone nor duplicate a Twin as expected. Property changes in one of the twins will propagate to all twins.

Setup

class TestTwin < Disposable::Twin
  property :foo
end

original   = TestTwin.new(OpenStruct.new)
cloned     = original.clone
duplicated = original.dup

original.foo = :bar

Expected

original.foo   #=> :bar
cloned.foo     #=> nil
duplicated.foo #=> nil

(I suppose it would be fine if #dup behaved different to #clone, but at least one should achieve the above behaviour and the difference should be documented.)

Actual

original.foo   #=> :bar
cloned.foo     #=> :bar
duplicated.foo #=> :bar

Note that changing the property on either cloned or duplicated will likewise modify it on the other two twins as well. The state of the properties is shared among all twins.

Support for scopes

I realize #scopes is a feature specific to ActiveRecord and this library is pretty agnostic. Here's an example:

class Cart
  has_many :items
end

class Item
  belongs_to :cart

  scope :nikes, -> { where(brand: "nike") }
end

cart.items.nikes

With this library it raises an error:

NoMethodError: undefined method `nikes' for #<Disposable::Twin::Collection:...>

I'm not sure how it would work, but I would love to see this:

class ItemTwin < Disposable::Twin
  property :brand
  property :cart

  subset :nikes, twin: ItemTwin
end

No self.model_name for twin

Something expects self.model_name, which is kinda annoying to handle.

It'd be really awesome if I could, as a feature, dump in ActiveModel or ActiveRecord assumptions (table_name is another).

Module that has 'unnest' throws an error if included more than once

I have a Reform::Form::Module where I'm using unnest. If I try to include it more than once I get an error on startup: gems/disposable-0.4.7/lib/disposable/twin/property/unnest.rb:14:in `unnest': undefined method `[]' for nil:NilClass (NoMethodError)

A setup similar to this:

module Contract::Component
  module Foo
    include Reform::Form::Module

    property :foo do
      property :bar
    end

    unnest :bar, from: :foo
  end
end

module Contract
  class Create < Reform::Form
    include Component::Foo
  end
end

module Contract
  class Update < Reform::Form
    include Component::Foo
  end
end

This is the relevant part of the code in disposable

    def unnest(name, options)
      from = options.delete(:from)
      # needed to make reform process this field.

      options = definitions.get(from)[:nested].definitions.get(name).instance_variable_get(:@options) # FIXME.
      options = options.merge(virtual: true, _inherited: true, private_name: nil)

      property(name, options)
      delegates from, name, "#{name}="
    end

Since unnest is a class method, options.delete(:from) will actually delete the key from the method argument itself, causing each subsequent call to fail.
Changing that line to from = options[:from] seems to fix the issue for me, without introducing any noticeable side-effects.

Nesting compositions

It would be more readable if you were able to do something like

on :something do
  property :blah
  # ... x100
end

instead of

property :blah, on: :something
# ... x100

Seems like I'm repeating on: :something all day long. I'd make a PR for that - if thats something you'd happily see join the code base.

undefined method 'sync!' for #<ImageUploader:xxx>

A number of my models use Carrierwave for image uploads, I've just started seeing these errors when saving an instance through an operation:

NoMethodError: undefined method `sync!' for #<ImageUploader:0x007f8fb9263388>
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/sync.rb:23:in `block (2 levels) in sync!'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/property_processor.rb:34:in `property!'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/property_processor.rb:15:in `call'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/sync.rb:23:in `block in sync!'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/representer.rb:40:in `block in each'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/representer.rb:34:in `each'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/sync.rb:20:in `sync!'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/sync.rb:11:in `sync_models'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/disposable-e0c4282118d0/lib/disposable/twin/save.rb:5:in `save'
    /Users/kevinansfield/code/rails/competio-app/app/concepts/horse/crud.rb:44:in `block in process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation.rb:111:in `validate'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation/crud.rb:43:in `validate'
    /Users/kevinansfield/code/rails/competio-app/app/concepts/horse/crud.rb:43:in `process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation.rb:66:in `run'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation.rb:23:in `run'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation/controller.rb:42:in `block in run'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation/controller.rb:73:in `operation!'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/bundler/gems/trailblazer-c077c6579a7f/lib/trailblazer/operation/controller.rb:42:in `run'
    /Users/kevinansfield/code/rails/competio-app/app/controllers/horses_controller.rb:10:in `create'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/metal/implicit_render.rb:4:in `send_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/abstract_controller/base.rb:198:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/metal/rendering.rb:10:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/abstract_controller/callbacks.rb:20:in `block in process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:117:in `call'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:117:in `call'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:555:in `block (2 levels) in compile'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:505:in `call'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:505:in `call'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:92:in `_run_callbacks'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:776:in `_run_process_action_callbacks'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/callbacks.rb:81:in `run_callbacks'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/abstract_controller/callbacks.rb:19:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/metal/rescue.rb:29:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/notifications.rb:164:in `block in instrument'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.1/lib/active_support/notifications.rb:164:in `instrument'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/metal/instrumentation.rb:30:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/metal/params_wrapper.rb:250:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.1/lib/active_record/railties/controller_runtime.rb:18:in `process_action'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/abstract_controller/base.rb:137:in `process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionview-4.2.1/lib/action_view/rendering.rb:30:in `process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/test_case.rb:632:in `process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/test_case.rb:65:in `process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/devise-3.5.1/lib/devise/test_helpers.rb:19:in `block in process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/devise-3.5.1/lib/devise/test_helpers.rb:72:in `catch'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/devise-3.5.1/lib/devise/test_helpers.rb:72:in `_catch_warden'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/devise-3.5.1/lib/devise/test_helpers.rb:19:in `process'
    /Users/kevinansfield/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.1/lib/action_controller/test_case.rb:514:in `post'
    /Users/kevinansfield/code/rails/competio-app/test/controllers/horses_controller_test.rb:70:in `block (4 levels) in <class:HorsesControllerTest>'

This was working last week, I'll see if I can track down when exactly it started throwing this error.

Using 0.1.10 with Reform causes form field values to change to Uber::Options::Values

Hello,

We did a full bundle update today while upgrading our Rails project and ran into the following issue:
We are using Reform v2.0.3 and with the full update, Disposable was bumped to v0.1.10. In an #update, the form calls #validate on a param which changes all but one of the form fields to Uber::Options::Values. I have included the controller, form and test outputs below.

Report Controller:

  def update
    authorize! :manage, 'Report'
    report = Report.find(params[:id])
    form = RemoteReportForm.new(report)

    if form.validate(params[:remote_report])
      form.save do |hash|
        report.update_attributes('report' => hash.stringify_keys)
      end
      redirect_to report_path(params[:id])
    else
      render :edit
    end
  end

Form:

class RemoteReportForm < Reform::Form
  property :id
  property :name
  property :notes
  property :a_ref

  validates :name, presence: true

  module RemoteReportFormExtensions
    def name=(name)
      self.w_ref = Website.where(name: name).try(:first).try(:id)
      super(name)
    end
  end

  collection :anchors, populate_if_empty: Reports::Anchor do
    property :id
    property :name
    property :w_ref
    property :_destroy

    include RemoteReportFormExtensions

    collection :feeders, populate_if_empty: Reports::Feeder do
      property :id
      property :name
      property :w_ref
      property :_destroy

      include RemoteReportFormExtensions

    end
  end
end

Test:

    describe 'PUT update' do
      it 'should update the report' do
        expected = {
          'report' => {
            'name' => 'New name',
            'id' => 23,
            'notes' => 'These are some notes',
            'a_ref' => 56,
            'anchors' => []
          }
        }
        put :update, id: 23, remote_report: { name: 'New name' }
        expect(report).to have_received(:update_attributes).with(expected)
      end
    end

Output:
form before #validate is called:

=> #<RemoteReportForm:0x007fe01f5cd698
 @_changes={},
 @fields={"id"=>23, "name"=>"One", "notes"=>"These are some notes", "a_ref"=>56, "anchors"=>[]},
 @mapper=#<#<Class:0x007fe01f5ccb80>:0x007fe01c2edc28 @model=#<InstanceDouble(Report) (anonymous)>>, @model=#<InstanceDouble(Report) (anonymous)>>

form after #validate is called:

form.validate(params[:remote_report])
form
=> #<RemoteReportForm:0x007fe004b74288
 @_changes={"id"=>true, "name"=>true, "notes"=>true, "account_ref"=>true},
 @errors=#<Reform::Contract::Errors:0x007fe00488b238 @base=#<RemoteReportForm:0x007fe004b74288 ...>, @messages={}>,
 @fields=
  {"id"=>#<Uber::Options::Value:0x007fe01c149930 @callable=false, @dynamic=false, @method=false, @proc=false, @value=nil>,
   "name"=>"New name",
   "notes"=>#<Uber::Options::Value:0x007fe01f3a5078 @callable=false, @dynamic=false, @method=false, @proc=false, @value=nil>,
   "account_ref"=>#<Uber::Options::Value:0x007fe01f33c460 @callable=false, @dynamic=false, @method=false, @proc=false, @value=nil>,
   "anchors"=>[]},
 @mapper=#<#<Class:0x007fe01f5ccb80>:0x007fe004b741c0 @model=#<InstanceDouble(Report) (anonymous)>>,
 @model=#<InstanceDouble(Report) (anonymous)>>

As you can see, before #validate is called, the form retains it's original form values and after validation, they are changed to Uber::Options::Values. The only form field that does not change is the 'name' which is also the only attribute with a validation in the RemoteReportForm.

Ruby 2.3.3, Rails 4.2 Disposable::Twin::Struct refers to ::Struct

As the title of this issue

when I try to include Disposable::Twin::Struct module
Ruby raises error, because I am trying to include ::Struct class

I know that Disposable::Twin::Struct is deprecated, I should use Disposable::Twin::Property::Struct
but I couldn't require it, maybe because Disposable::Twin::Property::Struct is in the property.rb file

so my gem unable to load it

property :foo do
  include Disposable::Twin::Struct
end
property :foo do
  include ::Disposable::Twin::Struct
end

all of the code above doesn't work

adding/deleting from collection is not detected as change

If populator only adds/removes elements from collection, changed? returns nil.
To detect collection change - at least one member must be changed.
But it seems counter intuitive - if I added something to collection, that means, that collection as whole is changed, isn't it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.