Skip to content

Instantly share code, notes, and snippets.

@sirsean
Last active August 29, 2015 14:04
Show Gist options
  • Save sirsean/c53fc4d934e085988841 to your computer and use it in GitHub Desktop.
Save sirsean/c53fc4d934e085988841 to your computer and use it in GitHub Desktop.
Command pattern with required/optional fields, support for type coercion, and validation.
module Command
extend ActiveSupport::Concern
include ActiveModel::Validations
module ClassMethods
def required(key, as: nil)
add_field(key, as)
validates key, presence: true
end
private :required
def optional(key, as: nil, default: nil)
add_field(key, as, default)
end
private :optional
def add_field(key, as=nil, default=nil)
define_method(key) do
@fields[key] || Coercer.coerce(default, as)
end
private key
define_method("#{key}=") do |value|
@fields[key] = Coercer.coerce(value, as)
end
private "#{key}="
end
private :add_field
def command(&block)
@command_block = block
end
private :command
attr_reader :command_block
end
def initialize(opts={})
@fields = {}
opts.each do |key, value|
send("#{key}=", value)
end
end
def execute
if valid?
begin
result = instance_eval(&self.class.command_block)
rescue => e
add_error(:runtime, e.message)
end
end
Outcome.new(result: result, errors: errors)
end
def add_error(key, message)
errors[key] << message
end
private :add_error
end
class Coercer
class << self
def coerce(value, type)
if coercer = @coercers[type]
coercer.new.coerce(value)
else
value
end
end
def add_type(type, coercer)
coercers[type] = coercer
end
def coercers
@coercers ||= {}
end
end
end
class IntegerCoercer
def coerce(value)
value.to_i
end
end
Coercer.add_type(:integer, IntegerCoercer)
class BooleanCoercer
FALSY = ["false", "0", 0]
def coerce(value)
FALSY.each do |falsy|
value = false if value == falsy
end
!!value
end
end
Coercer.add_type(:boolean, BooleanCoercer)
class Outcome
def initialize(result: nil, errors: {})
@result = result
@errors = errors
end
attr_reader :result, :errors
def success?
errors.empty?
end
end
class King
def initialize(opts)
opts.each do |k,v|
instance_variable_set("@#{k}", v)
end
end
end
class KingMaker
include Command
required :name
optional :age, as: :integer, default: 100
optional :replacement
optional :abdicated, as: :boolean, default: false
optional :extra
validates :abdicated, presence: true, if: ->{ replacement }
command do
#raise "something failed!"
#add_error(:other, "things")
King.new(name: name, age: age)
end
end
maker = KingMaker.new(name: "henry", age: 8, replacement: "some guy", abdicated: false)
pp maker
outcome = maker.execute
pp outcome
@sirsean
Copy link
Author

sirsean commented Jul 28, 2014

My problem with either having a required method or allowing the user to specify a method to call is that I want to discourage anyone from calling anything other than #execute.

For example:

class KingMaker
  include Command

  def _execute
    #do stuff
  end
end

KingMaker.new._execute # no!

or

class KingMaker
  include Command

  command :my_logic
  def my_logic
    # do stuff
  end
end

KingMaker.new.my_logic # no!

There's nothing stopping you from breaking your command out into good Ruby code.

class KingMaker
  include Command

  command do
    if complicated_thing?
      do_something_good
    else
      do_something_bad
    end
  end

  private

  def complicated_thing?
  end

  def do_something_good
  end

  def do_something_bad
  end
end

But it does prevent you from doing what we don't want you do, like calling the command block without calling #execute which would prevent you from getting an Outcome object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment