Running with Ruby

Tag: ActiveModel (page 1 of 4)

ActiveResource relations – a bit of magic to make it look and feel more like ActiveModel relations

ActiveResource collection new problem

ActiveResource can be pretty helpful when you have a RESTful JSON API. Although it has some limitations. One of the most irritating is a lack of nested resources new scope method. When you have structure like this:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats
end

class DailyStat  < ActiveResource::Base
  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

You can do some basic stuff:

User.last #=> User instance
User.all #=> [User, User]
user = User.last
user.daily_stats #=> [DailyStat, DailyStat]

But unfortunately if you try something like this:

user = User.last
stat = user.daily_stats.new
stat.save!

You’ll get following error:

user.daily_stats.new
NoMethodError: undefined method `new' for #<ActiveResource::Collection:0x000000080d1a30>

Of course we can do this the other way around:

user = User.last
stats = DailyStat.new(user_id: user.id)
stats.save!

But it just doesn’t seem right (well at least after working with ActiveRecord). ActiveResource::Collection doesn’t support building resources through it.

alias_method and a bit of magic as a solution

To obtain such a behaviour we have to:

  • save original relation method using alias_method
  • create a module that will contain our extension that will allow us to build new resources directly
  • define relation method that will mix the module with original relation method output
  • return mixed relation output

Once we have all of this, we will be able to just:

user = User.last
new_stat = user.daily_stats.new
new_stat.save!

overwriting method without losing the original one

Ok, so we have our relation daily_stats method that will return us a given ActiveResource::Collection. We will have to overwrite it, but we can’t lose the original one. To obtain this, we can use alias_method:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :native_daily_stats, :daily_stats

  def daily_stats
    # Now we can do whatever we want here, because we can get
    # the ActiveResource::Collection of DailyStat via
    # native_daily_stats method
    # Something fancy will happen here...
    # and after that we will just return native_daily_stats
    native_daily_stats
  end
end

It’s worth pointing out, that you can use this trick to redefine/change method without losing the original one. Especially when you can’t use super (because it’s not an inherited one, etc).

Extension module for our new daily_stats

Now we have to create our extension that will be used to modify the ActiveResource::Collection (but only in a daily_stats scope):

class DailyStat < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  # Here should go previously declarated DailyStat class code...
end

Hooking it all togethers

Finally, we can join all previously created elements in a new daily_stats method:

class User < ActiveResource::Base
  # User code...  

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

Now you can create resources that belong to other, directly via their scope.

TLl;DR version

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :original_daily_stats, :daily_stats

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

class DailyStat  < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      # Here magic happens - original params contain relation details (user_id: user.id)
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

State Machine gem and Rails (ActiveModel) 4.1 – NoMethodError – protected method ‘around_validation’ called for StateMachine

State machine is a great gem, unfortunately it’s not working with Rails 4.1. If you upgrade your app and try to use it, you’ll end up with following error:

NoMethodError - protected method `around_validation' called for #<StateMachine::Machine:0x007fd4ea002c80>:
  activesupport (4.1.4) lib/active_support/callbacks.rb:447:in `public_send'
  activesupport (4.1.4) lib/active_support/callbacks.rb:447:in `block in make_lambda'
  activesupport (4.1.4) lib/active_support/callbacks.rb:298:in `call'
  activesupport (4.1.4) lib/active_support/callbacks.rb:298:in `block in halting'
  activesupport (4.1.4) lib/active_support/callbacks.rb:86:in `call'
  activesupport (4.1.4) lib/active_support/callbacks.rb:86:in `run_callbacks'
  activemodel (4.1.4) lib/active_model/validations/callbacks.rb:111:in `run_validations!'
  activemodel (4.1.4) lib/active_model/validations.rb:317:in `valid?'
  activerecord (4.1.4) lib/active_record/validations.rb:70:in `valid?'
  activerecord (4.1.4) lib/active_record/validations.rb:77:in `perform_validations'
  activerecord (4.1.4) lib/active_record/validations.rb:57:in `save!'
  activerecord (4.1.4) lib/active_record/attribute_methods/dirty.rb:29:in `save!'
  activerecord (4.1.4) lib/active_record/transactions.rb:273:in `block in save!'
  activerecord (4.1.4) lib/active_record/transactions.rb:329:in `block in with_transaction_returning_status'
  activerecord (4.1.4) lib/active_record/connection_adapters/abstract/database_statements.rb:199:in `transaction'
  activerecord (4.1.4) lib/active_record/transactions.rb:208:in `transaction'
  activerecord (4.1.4) lib/active_record/transactions.rb:326:in `with_transaction_returning_status'
  activerecord (4.1.4) lib/active_record/transactions.rb:273:in `save!'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:487:in `block in save!'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:502:in `block (2 levels) in around_save'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:150:in `block in run_actions'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:170:in `catch_exceptions'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:148:in `run_actions'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:133:in `run_callbacks'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:212:in `run_callbacks'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:63:in `block (2 levels) in perform'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:63:in `catch'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:63:in `block in perform'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:186:in `within_transaction'
  state_machine (1.2.0) lib/state_machine/transition_collection.rb:62:in `perform'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:502:in `block in around_save'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:530:in `block in transaction'
  activerecord (4.1.4) lib/active_record/connection_adapters/abstract/database_statements.rb:201:in `block in transaction'
  activerecord (4.1.4) lib/active_record/connection_adapters/abstract/database_statements.rb:209:in `within_new_transaction'
  activerecord (4.1.4) lib/active_record/connection_adapters/abstract/database_statements.rb:201:in `transaction'
  activerecord (4.1.4) lib/active_record/transactions.rb:208:in `transaction'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:529:in `transaction'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:501:in `around_save'
  state_machine (1.2.0) lib/state_machine/integrations/active_record.rb:487:in `save!'
  activerecord (4.1.4) lib/active_record/validations.rb:41:in `create!'
  app/models/concerns/channel/tracked.rb:139:in `find_or_create_channel'
  app/models/concerns/channel/tracked.rb:128:in `track'
  app/controllers/reporting/tracked_channels_controller.rb:20:in `create'
  actionpack (4.1.4) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
  actionpack (4.1.4) lib/abstract_controller/base.rb:189:in `process_action'
  actionpack (4.1.4) lib/action_controller/metal/rendering.rb:10:in `process_action'
  actionpack (4.1.4) lib/abstract_controller/callbacks.rb:20:in `block in process_action'
  activesupport (4.1.4) lib/active_support/callbacks.rb:113:in `call'
  activesupport (4.1.4) lib/active_support/callbacks.rb:113:in `call'

Here’s a fix. Just put it into your initializers:

# Stathe machine
module StateMachine
  # Extensions for integrations of state machine
  module Integrations
    # ActiveModel extension that fixes the non-public around_validation error
    module ActiveModel
      send :public, :around_validation
    end
  end
end

Note: It works also for any non-Rails, ActiveModel based apps.

Olderposts

Copyright © 2017 Running with Ruby

Theme by Anders NorenUp ↑