Announcing ActiveInteraction 2

by Taylor Fausak on

(Aaron Lasseigne co-authored this post for the OrgSync Dev Blog.)

ActiveInteraction logo

We are proud to announce the release of ActiveInteraction 2. This is the first major version change since we released ActiveInteraction more than a year ago. We made some backwards-incompatible changes that make working with interactions easier and faster.

To simplify transitioning from 1.5.1 to 2.0.0, we are also releasing 1.6.0. It backports some features from 2.0.0 and adds deprecations warnings for features that will be removed.

This blog post explains the changes we made and why we made them. It also shows you how to update interactions from 1.5.1 to 2.0.0.

Transactions

We removed support for ActiveRecord transactions (issue 205). This means that interactions are not wrapped in a transaction by default. To retain the old behavior, wrap your execute method in an ActiveRecord::Base.transaction block.

We also removed the transaction method, since it does not do anything anymore.

We decided to remove transactions because we saw that most interactions did not need them. The added cost and chance of deadlocks was not worth it in general.

# v1.6
class Example < ActiveInteraction::Base
  # This is the default.
  transaction true

  def execute
    # ...
  end
end

# v2.0
class Example < ActiveInteraction::Base
  def execute
    ActiveRecord::Base.transaction do
      # ...
    end
  end
end

You will get a deprecation warning if you use transaction with 1.6.

Errors

We replaced symbolic errors with detailed errors (issue 250). Detailed errors will be part of Rails 5. We started using symbolic errors in version 0.6.0 and are happy to see something similar make its way into Rails 5. Unfortunately their APIs differ slightly. See the example below for details.

If you want to use detailed errors in your own code, check out the active_model-errors_details gem.

# v1.6
class Example < ActiveInteraction::Base
  def execute
    errors.add_sym :base, :invalid
    errors.add_sym :base, :custom, '...'
  end
end
Example.run.errors.symbolic
# => {:base=>[:invalid,:custom]}

# v2.0
class Example < ActiveInteraction::Base
  def execute
    errors.add :base, :invalid
    errors.add :base, :custom, message: '...'
  end
end
Example.run.errors.details
# => {:base=>[{:error=>:invalid},{:error=>:custom,:message=>'...'}]}

You will get a deprecation warning if you use either add_sym or symbolic with 1.6.

Objects

We renamed the model filter to object (issue 264). This more accurately reflects what the filter can be used for. We initially used the model filter for ActiveModel objects. But it works with any object, so the name was misleading.

# v1.6
class Example < ActiveInteraction::Base
  model :regexp
end
Example.run(regexp: /.../)

# v2.0
class Example < ActiveInteraction::Base
  object :regexp
end
Example.run(regexp: /.../)

You will get a deprecation warning if you use model with 1.6.

Hashes

We switched the hash filter to use indifferent access (issue 164). This prevents a possible denial of service attack. As a result, hash keys will be strings instead of symbols.

class Example < ActiveInteraction::Base
  hash :options,
    strip: false

  def execute
    options.keys
  end
end

# v1.6
Example.run!(options: { enable: true })
# => [:enable]

# v2.0
Example.run!(options: { enable: true })
# => ["enable"]

Files

We changed the file filter to accept anything that responds to eof? (pull 236). It used to accept only Files and Tempfiles. Now it accepts a wider range of IO objects.

class Example < ActiveInteraction::Base
  file :io

  def execute
    io.read
  end
end

# v1.6
Example.run!(io: StringIO.new('Hello, world!'))
# ActiveInteraction::InvalidInteractionError: Io is not a valid file

# v2.0
Example.run!(io: StringIO.new('Hello, world!'))
# => "Hello, world!"

Results

We added the ability to return results from invalid interactions (issue 168). Setting the result to nil was unnecessary. The right way to check for validity is to use valid?. This change allows you to return something from an interaction even if there are errors. This can be very useful when updating an existing record.

class Example < ActiveInteraction::Base
  def execute
    errors.add(:base)
    'something'
  end
end

# v1.6
outcome = Example.run
outcome.valid?
# => false
outcome.result
# => nil

# v2.0
outcome = Example.run
outcome.valid?
# => false
outcome.result
# => "something"

Defaults

We made it so Proc defaults are not eagerly evaluated (issue 269). They never should have been in the first place. This was an oversight when we introduced this feature.

class Example < ActiveInteraction::Base
  boolean :flag,
    default: -> {
      puts 'defaulting...'
      true
    }

  def execute
    puts 'executing...'
  end
end

# v1.6
# defaulting...
Example.run
# executing...

# v2.0
Example.run
# defaulting...
# executing...

Contributors

A big thanks to everyone who contributed to ActiveInteraction! We could not have done it without you.