-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
164ea99
commit 4c7a017
Showing
12 changed files
with
639 additions
and
157 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.