Skip to content

Instantly share code, notes, and snippets.

@robyurkowski
Created November 23, 2014 14:20
Show Gist options
  • Save robyurkowski/0567a483e3e070f7ff16 to your computer and use it in GitHub Desktop.
Save robyurkowski/0567a483e3e070f7ff16 to your computer and use it in GitHub Desktop.
require 'active_record'
module Skywalker
class Command
################################################################################
# Class interface
################################################################################
def self.call(*args)
new(*args).call
end
################################################################################
# Instantiates command, setting all arguments.
################################################################################
def initialize(**args)
self.args = args
self.args.freeze
parse_arguments
validate_arguments!
end
attr_accessor :on_success,
:on_failure,
:error,
:args
################################################################################
# Ensure required keys are present.
################################################################################
private def validate_arguments!
missing_args = required_args.map(&:to_s) - args.keys.map(&:to_s)
raise ArgumentError, "#{missing_args.join(", ")} required but not given" \
if missing_args.any?
end
################################################################################
# Any required keys should go here as either strings or symbols.
################################################################################
private def required_args
[]
end
private def parse_arguments
args.each_pair do |reader_method, value|
writer_method = "#{reader_method}="
singleton_class.class_eval do
send(:attr_reader, reader_method) unless respond_to?(reader_method)
send(:attr_writer, reader_method) unless respond_to?(writer_method)
end
self.send(writer_method, value)
end
end
################################################################################
# Call: runs the transaction and all operations.
################################################################################
def call
transaction do
execute!
confirm_success
end
rescue Exception => error
confirm_failure error
end
################################################################################
# Operations should be defined in this method.
################################################################################
private def execute!
end
################################################################################
# Override to customize.
################################################################################
private def transaction(&block)
::ActiveRecord::Base.transaction(&block)
end
################################################################################
# Trigger the given callback on success
################################################################################
private def confirm_success
run_success_callbacks
end
private def run_success_callbacks
on_success.call(self) if on_success.respond_to?(:call)
end
################################################################################
# Set the error so we can get it with `command.error`, and trigger error.
################################################################################
private def confirm_failure(error)
self.error = error
run_failure_callbacks
end
private def run_failure_callbacks
on_failure.call(self) if on_failure.respond_to?(:call)
end
end
end
require 'spec_helper'
require 'skywalker/command'
module Skywalker
RSpec.describe Command do
describe "convenience" do
it "provides a class call method that instantiates and calls" do
expect(Command).to receive_message_chain('new.call')
Command.call
end
end
describe "instantiation" do
it "freezes the arguments given to it" do
command = Command.new(a_symbol: :my_symbol)
expect(command.args).to be_frozen
end
it "accepts a variable list of arguments" do
expect { Command.new(a_symbol: :my_symbol, a_string: "my string") }.not_to raise_error
end
it "sets a reader for each argument" do
command = Command.new(a_symbol: :my_symbol)
expect(command).to respond_to(:a_symbol)
end
it "sets a writer for each argument" do
command = Command.new(a_symbol: :my_symbol)
expect(command).to respond_to(:a_symbol=)
end
it "sets the instance variable to the passed value" do
command = Command.new(a_symbol: :my_symbol)
expect(command.a_symbol).to eq(:my_symbol)
end
it "raises an error if an argument in its required_args is not present" do
allow_any_instance_of(Command).to receive(:required_args).and_return([:required_arg])
expect { Command.new }.to raise_error
end
it "does not raise an error if an argument in its required_args is present" do
allow_any_instance_of(Command).to receive(:required_args).and_return([:required_arg])
expect { Command.new(required_arg: :blah) }.not_to raise_error
end
end
describe "validity control" do
let(:command) { Command.new }
it "executes in a transaction" do
expect(command).to receive(:transaction)
command.call
end
end
describe "execution" do
before do
allow(command).to receive(:transaction).and_yield
end
describe "success handling" do
let(:on_success) { double("on_success callback") }
let(:command) { Command.new(on_success: on_success) }
before do
allow(command).to receive(:execute!).and_return(true)
end
it "triggers the confirm_success method" do
expect(command).to receive(:confirm_success)
command.call
end
it "runs the success callbacks" do
expect(command).to receive(:run_success_callbacks)
command.call
end
describe "on_success" do
context "when on_success is callable" do
it "calls the on_success callback with itself" do
expect(on_success).to receive(:call).with(command)
command.call
end
end
context "when on_success is not callable" do
let(:nil_callback) { double("nil_callback") }
let(:command) { Command.new(on_success: nil_callback) }
it "does not call on_success" do
expect(nil_callback).not_to receive(:call)
command.call
end
end
end
end
describe "failure handling" do
let(:on_failure) { double("on_failure callback") }
let(:command) { Command.new(on_failure: on_failure) }
before do
allow(command).to receive(:execute!).and_raise(ScriptError)
end
it "triggers the confirm_failure method" do
expect(command).to receive(:confirm_failure)
command.call
end
it "sets the error on the command" do
allow(on_failure).to receive(:call)
expect(command).to receive(:error=)
command.call
end
it "runs the failure callbacks" do
allow(command).to receive(:error=)
expect(command).to receive(:run_failure_callbacks)
command.call
end
describe "on_failure" do
before do
allow(command).to receive(:error=)
end
context "when on_failure is callable" do
it "calls the on_failure callback with itself" do
expect(on_failure).to receive(:call).with(command)
command.call
end
end
context "when on_failure is not callable" do
let(:nil_callback) { double("nil_callback") }
let(:command) { Command.new(on_failure: nil_callback) }
it "does not call on_failure" do
expect(nil_callback).not_to receive(:call)
command.call
end
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment