Code Monkey home page Code Monkey logo

disposable's People

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

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

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]

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?

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.

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?

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

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.

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

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

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!

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!

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?

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.

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.

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.

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!

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.

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?

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

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?

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

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

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.

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?

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

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

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

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

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'

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.

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

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.