Skip to content

Commit

Permalink
#119 ActiveRecord helper methods (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronmallen committed Jan 21, 2020
1 parent 164ea99 commit 4c7a017
Show file tree
Hide file tree
Showing 12 changed files with 639 additions and 157 deletions.
13 changes: 12 additions & 1 deletion lib/active_interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@ module ActiveInteractor
extend ActiveSupport::Autoload

autoload :Base
autoload :Context

# ActiveInteractor::Context classes
# @author Aaron Allen <hello@aaronmallen.me>
# @since 0.0.1
module Context
extend ActiveSupport::Autoload

autoload :Base
autoload :Loader
autoload :Status
end

autoload :Organizer

eager_autoload do
Expand Down
13 changes: 0 additions & 13 deletions lib/active_interactor/context.rb

This file was deleted.

120 changes: 1 addition & 119 deletions lib/active_interactor/context/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Context
class Base < OpenStruct
include ActiveModel::Validations
include Attributes
include Status

# @param context [Hash|Context::Base] attributes to assign to the context
# @return [Context::Base] a new instance of {Context::Base}
Expand All @@ -29,58 +30,6 @@ def initialize(context = {})
# https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations.rb#L305
# ActiveModel::Validations#valid?

# @api private
# Track that an Interactor has been called. The {#called!} method
# is used by the interactor being invoked with this context. After an
# interactor is successfully called, the interactor instance is tracked in
# the context for the purpose of potential future rollback
# @param interactor [ActiveInteractor::Base] the called interactor
# @return [Array<ActiveInteractor::Base>] all called interactors
def called!(interactor)
_called << interactor
end

# Fail the context instance. Failing a context raises an error
# that may be rescued by the calling interactor. The context is also flagged
# as having failed
#
# @example Fail an interactor context
# class MyInteractor < ActiveInteractor::Base
# def perform
# context.fail!
# end
# end
#
# MyInteractor.perform!
# #=> ActiveInteractor::Error::ContextFailure: <#MyInteractor::Context>
# @param errors [ActiveModel::Errors|nil] errors to add to the context on failure
# @see https://api.rubyonrails.org/classes/ActiveModel/Errors.html ActiveModel::Errors
# @raise [Error::ContextFailure]
def fail!(errors = nil)
merge_errors!(errors) if errors
@_failed = true
raise Error::ContextFailure, self
end

# Whether the context instance has failed. By default, a new
# context is successful and only changes when explicitly failed
# @note The {#failure?} method is the inverse of the {#success?} method
# @example Check if a context has failed
# class MyInteractor < ActiveInteractor::Base
# def perform; end
# end
#
# result = MyInteractor.perform
# #=> <#MyInteractor::Context>
#
# result.failure?
# #=> false
# @return [Boolean] `false` by default or `true` if failed
def failure?
@_failed || false
end
alias fail? failure?

# Merge an instance of context or a hash into an existing context
# @since 1.0.0
# @example
Expand Down Expand Up @@ -112,75 +61,8 @@ def merge!(context)
self
end

# Roll back an interactor context. Any interactors to which this
# context has been passed and which have been successfully called are asked
# to roll themselves back by invoking their
# {ActiveInteractor::Base#rollback} instance methods.
# @example Rollback an interactor's context
# class MyInteractor < ActiveInteractor::Base
# def perform
# context.fail!
# end
#
# def rollback
# context.user&.destroy
# end
# end
#
# user = User.create
# #=> <#User>
#
# result = MyInteractor.perform(user: user)
# #=> <#MyInteractor::Context user=<#User>>
#
# result.user.destroyed?
# #=> true
# @return [Boolean] `true` if rolled back successfully or `false` if already
# rolled back
def rollback!
return false if @_rolled_back

_called.reverse_each(&:rollback)
@_rolled_back = true
end

# Whether the context instance is successful. By default, a new
# context is successful and only changes when explicitly failed
# @note the {#success?} method is the inverse of the {#failure?} method
# @example Check if a context is successful
# class MyInteractor < ActiveInteractor::Base
# def perform; end
# end
#
# result = MyInteractor.perform
# #=> <#MyInteractor::Context>
#
# result.success?
# #=> true
# @return [Boolean] `true` by default or `false` if failed
def success?
!failure?
end
alias successful? success?

private

def _called
@_called ||= []
end

def copy_called!(context)
value = context.instance_variable_get('@_called') || []
instance_variable_set('@_called', value)
end

def copy_flags!(context)
%w[_failed _rolled_back].each do |flag|
value = context.instance_variable_get("@#{flag}")
instance_variable_set("@#{flag}", value)
end
end

def merge_errors!(errors)
if errors.is_a? String
self.errors.add(:context, errors)
Expand Down
131 changes: 131 additions & 0 deletions lib/active_interactor/context/status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# frozen_string_literal: true

module ActiveInteractor
module Context
# Context status methods included by all {Context::Base}
# @author Aaron Allen <hello@aaronmallen.me>
# @since 1.0.0
module Status
# @api private
# Track that an Interactor has been called. The {#called!} method
# is used by the interactor being invoked with this context. After an
# interactor is successfully called, the interactor instance is tracked in
# the context for the purpose of potential future rollback
# @param interactor [ActiveInteractor::Base] the called interactor
# @return [Array<ActiveInteractor::Base>] all called interactors
def called!(interactor)
_called << interactor
end

# Fail the context instance. Failing a context raises an error
# that may be rescued by the calling interactor. The context is also flagged
# as having failed
#
# @example Fail an interactor context
# class MyInteractor < ActiveInteractor::Base
# def perform
# context.fail!
# end
# end
#
# MyInteractor.perform!
# #=> ActiveInteractor::Error::ContextFailure: <#MyInteractor::Context>
# @param errors [ActiveModel::Errors|nil] errors to add to the context on failure
# @see https://api.rubyonrails.org/classes/ActiveModel/Errors.html ActiveModel::Errors
# @raise [Error::ContextFailure]
def fail!(errors = nil)
merge_errors!(errors) if errors
@_failed = true
raise Error::ContextFailure, self
end

# Whether the context instance has failed. By default, a new
# context is successful and only changes when explicitly failed
# @note The {#failure?} method is the inverse of the {#success?} method
# @example Check if a context has failed
# class MyInteractor < ActiveInteractor::Base
# def perform; end
# end
#
# result = MyInteractor.perform
# #=> <#MyInteractor::Context>
#
# result.failure?
# #=> false
# @return [Boolean] `false` by default or `true` if failed
def failure?
@_failed || false
end
alias fail? failure?

# Roll back an interactor context. Any interactors to which this
# context has been passed and which have been successfully called are asked
# to roll themselves back by invoking their
# {ActiveInteractor::Base#rollback} instance methods.
# @example Rollback an interactor's context
# class MyInteractor < ActiveInteractor::Base
# def perform
# context.fail!
# end
#
# def rollback
# context.user&.destroy
# end
# end
#
# user = User.create
# #=> <#User>
#
# result = MyInteractor.perform(user: user)
# #=> <#MyInteractor::Context user=<#User>>
#
# result.user.destroyed?
# #=> true
# @return [Boolean] `true` if rolled back successfully or `false` if already
# rolled back
def rollback!
return false if @_rolled_back

_called.reverse_each(&:rollback)
@_rolled_back = true
end

# Whether the context instance is successful. By default, a new
# context is successful and only changes when explicitly failed
# @note the {#success?} method is the inverse of the {#failure?} method
# @example Check if a context is successful
# class MyInteractor < ActiveInteractor::Base
# def perform; end
# end
#
# result = MyInteractor.perform
# #=> <#MyInteractor::Context>
#
# result.success?
# #=> true
# @return [Boolean] `true` by default or `false` if failed
def success?
!failure?
end
alias successful? success?

private

def _called
@_called ||= []
end

def copy_called!(context)
value = context.instance_variable_get('@_called') || []
instance_variable_set('@_called', value)
end

def copy_flags!(context)
%w[_failed _rolled_back].each do |flag|
value = context.instance_variable_get("@#{flag}")
instance_variable_set("@#{flag}", value)
end
end
end
end
end
3 changes: 1 addition & 2 deletions lib/active_interactor/rails.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# frozen_string_literal: true

require 'rails'

require 'active_interactor'
require 'active_interactor/rails/active_record'
require 'active_interactor/rails/config'
require 'active_interactor/rails/railtie'

Expand Down
56 changes: 56 additions & 0 deletions lib/active_interactor/rails/active_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module ActiveInteractor
module Rails
# ActiveRecord helper methods
# @author Aaron Allen <hello@aaronmallen.me>
# @since 1.0.0
module ActiveRecord
# Include ActiveRecord helper methods on load
def self.include_helpers
ActiveSupport.on_load(:active_record_base) do
extend ClassMethods
end
end

# ActiveRecord class helper methods
# @author Aaron Allen <hello@aaronmallen.me>
# @since 1.0.0
module ClassMethods
# Include {Context::Status} methods
def acts_as_context
class_eval do
include InstanceMethods
include ActiveInteractor::Context::Status
delegate :each_pair, to: :attributes
end
end
end

module InstanceMethods
# Override ActiveRecord's initialize method to ensure
# context flags are copied to the new instance
# @param context [Hash|nil] attributes to assign to the class
# @param options [Hash|nil] options for the class
def initialize(context = nil, options = {})
copy_flags!(context) if context
copy_called!(context) if context
attributes = context.to_h if context
super(attributes, options)
end

# Merge an ActiveRecord::Base instance and ensure
# context flags are copied to the new instance
# @param context [*] the instance to be merged
# @return [*] the merged instance
def merge!(context)
copy_flags!(context)
context.each_pair do |key, value|
self[key] = value
end
self
end
end
end
end
end
4 changes: 4 additions & 0 deletions lib/active_interactor/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class Railtie < ::Rails::Railtie

config.eager_load_namespaces << ActiveInteractor

initializer 'active_interactor.active_record_helpers' do
ActiveInteractor::Rails::ActiveRecord.include_helpers
end

config.to_prepare do
ActiveInteractor.configure do |c|
c.logger = ::Rails.logger
Expand Down
Loading

0 comments on commit 4c7a017

Please sign in to comment.