Skip to content

Commit

Permalink
A different take on the Interactor pattern
Browse files Browse the repository at this point in the history
The Interactor pattern is quite simple, but trying to use it leaves two
issues unresolved: the verbose passing of variables, either with context
or between methods; and the structure of the actions we're trying to
encapsulate, most which have a somewhat hidden lock/release mechanism.

Re-implementing the Interactor pattern locally gives the opportunity to
address some of these issues, but also to go completely off-the-rails!
This is really another example of what the end-result could look like,
which has some good and bad attributes over vanilla Interactors.
  • Loading branch information
Ben Thorner committed Mar 27, 2019
1 parent 64f7511 commit 9f6e34b
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 72 deletions.
31 changes: 31 additions & 0 deletions app/beneractors/images/create_actor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require "beneractors/beneractor"

module Images
class CreateActor < Beneractors::Beneractor
def pre_op
export :edition, Edition.find_current(document: document)
edition.lock!

export :issues, ::Requirements::ImageUploadChecker.new(image).issues
abort!(:issues) if issues.any?
end

def op
export :image_revision, ImageUploadService
.new(image, edition.revision)
.call(user)

updater = Versioning::RevisionUpdater.new(edition.revision, user)
updater.update_image(image_revision)

edition.assign_revision(updater.next_revision, user)
edition.save!
end

def post_op
PreviewService.new(edition).try_create_preview
end
end
end
51 changes: 51 additions & 0 deletions app/beneractors/images/update_actor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require "beneractors/beneractor"

module Images
class UpdateActor < Beneractors::Beneractor
def pre_op
export :edition, Edition.find_current(document: document)
edition.lock!

export :image_revision, edition.image_revisions.find_by!(image_id: image_id)
image_updater = Versioning::ImageRevisionUpdater.new(image_revision, user)

image_updater.assign(update_params)
export :next_image_revision, image_updater.next_revision

export :issues, Requirements::ImageRevisionChecker.new(next_image_revision)
.pre_preview_metadata_issues

abort!(:issues) if issues.any?

export :updater, Versioning::RevisionUpdater.new(edition.revision, user)
updater.update_image(next_image_revision, lead_image == "on")
abort! unless updater.changed?
end

def op
export :timeline_entry_type,
if updater.selected_lead_image?
:lead_image_selected
elsif updater.removed_lead_image?
:lead_image_removed
else
:image_updated
end

TimelineEntry.create_for_revision(
entry_type: timeline_entry_type,
edition: edition,
)

edition.assign_revision(updater.next_revision, user)
edition.save!
success!(timeline_entry_type)
end

def post_op
PreviewService.new(edition).try_create_preview
end
end
end
120 changes: 48 additions & 72 deletions app/controllers/images_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,27 @@ def index
end

def create
Edition.find_and_lock_current(document: params[:document]) do |edition|
@issues = ::Requirements::ImageUploadChecker.new(params[:image]).issues

if @issues.any?
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.index.flashes.upload_requirements"),
"items" => @issues.items,
}

render :index,
assigns: { edition: edition },
layout: rendering_context,
status: :unprocessable_entity
next
end

image_revision = ImageUploadService.new(params[:image], edition.revision).call(current_user)
updater = Versioning::RevisionUpdater.new(edition.revision, current_user)
updater.update_image(image_revision)
edition.assign_revision(updater.next_revision, current_user).save!
PreviewService.new(edition).try_create_preview
redirect_to crop_image_path(params[:document], image_revision.image_id, wizard: "upload")
result = Images::CreateActor.call(document: params[:document],
image: params[:image],
user: current_user)

if result.aborted?(:issues)
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.index.flashes.upload_requirements"),
"items" => result.issues.items,
}

render :index,
assigns: { issues: result.issues, edition: result.edition },
layout: rendering_context,
status: :unprocessable_entity

return
end

redirect_to crop_image_path(params[:document],
result.image_revision.image_id,
wizard: "upload")
end

def crop
Expand Down Expand Up @@ -63,57 +61,35 @@ def edit
end

def update
Edition.find_and_lock_current(document: params[:document]) do |edition| # rubocop:disable Metrics/BlockLength
image_revision = edition.image_revisions.find_by!(image_id: params[:image_id])
image_updater = Versioning::ImageRevisionUpdater.new(image_revision, current_user)

image_updater.assign(update_params)
next_image_revision = image_updater.next_revision

issues = Requirements::ImageRevisionChecker.new(next_image_revision)
.pre_preview_metadata_issues

if issues.any?
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.edit.flashes.requirements"),
"items" => issues.items,
}

render :edit,
assigns: { edition: edition, image_revision: next_image_revision, issues: issues },
layout: rendering_context,
status: :unprocessable_entity
next
end

updater = Versioning::RevisionUpdater.new(edition.revision, current_user)
updater.update_image(next_image_revision, params[:lead_image] == "on")

if updater.changed?
timeline_entry_type = if updater.selected_lead_image?
:lead_image_selected
elsif updater.removed_lead_image?
:lead_image_removed
else
:image_updated
end

TimelineEntry.create_for_revision(entry_type: timeline_entry_type, edition: edition)
edition.assign_revision(updater.next_revision, current_user).save!
PreviewService.new(edition).try_create_preview
end
result = Images::UpdateActor.call(document: params[:document],
image_id: params[:image_id],
lead_image: params[:lead_image],
user: current_user,
update_params: update_params)

if result.aborted?(:issues)
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.edit.flashes.requirements"),
"items" => result.issues.items,
}

render :edit,
assigns: { edition: result.edition, image_revision: result.next_image_revision, issues: result.issues },
layout: rendering_context,
status: :unprocessable_entity
return
end

if updater.selected_lead_image?
redirect_to document_path(edition.document),
notice: t("documents.show.flashes.lead_image.selected",
file: image_revision.filename)
elsif updater.removed_lead_image?
redirect_to images_path(edition.document),
notice: t("images.index.flashes.lead_image.removed",
file: image_revision.filename)
else
redirect_to images_path(edition.document)
end
if result.success?(:lead_image_selected)
redirect_to document_path(params[:document]),
notice: t("documents.show.flashes.lead_image.selected",
file: result.image_revision.filename)
elsif result.success?(:lead_image_removed)
redirect_to images_path(params[:document]),
notice: t("images.index.flashes.lead_image.removed",
file: result.image_revision.filename)
else
redirect_to images_path(params[:document])
end
end

Expand Down
47 changes: 47 additions & 0 deletions lib/beneractors/beneractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "beneractors/context"

module Beneractors
class Beneractor
attr_reader :context
delegate_missing_to :@context

def self.call(context = {})
context = Context.new(context)

ActiveRecord::Base.transaction do
new(context).call
context
end
rescue Context::FailError
context
end

private_class_method :new

def export(name, value)
context[name] = value
end

def initialize(context)
@context = context
end

def call
pre_op
op
post_op
end

def pre_op
context.fail!
end

def op
raise "not implemented"
end

def post_op; end
end
end
26 changes: 26 additions & 0 deletions lib/beneractors/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Beneractors
class Context < OpenStruct
class FailError < RuntimeError; end

def abort!(failure_type)
@failure_type = failure_type
@success_type = false
raise FailError
end

def success!(success_type = true)
@success_type = success_type
@failure_type = false
end

def success?(success_type = true)
@success_type == success_type
end

def aborted?(failure_type = true)
@failure_type == failure_type
end
end
end

0 comments on commit 9f6e34b

Please sign in to comment.