Last active
August 29, 2015 14:04
-
-
Save sirsean/c53fc4d934e085988841 to your computer and use it in GitHub Desktop.
Command pattern with required/optional fields, support for type coercion, and validation.
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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
or
There's nothing stopping you from breaking your command out into good Ruby code.
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 anOutcome
object.