Imagine you have some complex logic you want to make pluggable and extendable. You might use hooks like:
def do_stuff
before_hook if respond_to?(:before_hook)
# do stuff
after_hook if respond_to?(:after_hook)
endPeople inheriting from this can provide hooks your code would call.
But lets say you're an appliance and people are not writing code inheriting from you, they just use your appliance. The appliance deploys stuff:
def deploy!(stuff, deployer)
["netapp", "nfs", "server", "os", "vm"].each do |thing|
send("deploy_#{thing}".to_sym, stuff, deployer)
end
end
def do_netapp(stuff, deployer)
# bunch of shitty netapp logic
end
# def do_many_more_things_its_so_bad_please_dont_judge....This is pretty terrible for oh so many reasons. But this code is inaccessible and no-one can really extend it and if you want to extend it with new support you have to do all sorts of horrible things in whats probably a really massive class full of gross things.
Now you wanted to make this more generic you could do this many ways, but users are still going to not be inheriting from this code but they want to inject custom logic
So lets make some rules for every possible type of thing we do. Here we make netapp volumes whenever there are deployments - and not teardowns:
# rule to deploy netapp stuff
Rules::create_rule(:deploy_netapp) do
# we want to deploy some stuff
require_state :stuff, Deployment::Stuff
# we have some deployment helper thing
require_state :deployer, Deployer
# a set of named conditions we'll use later, they can inspect any aspect of state
condition(:is_deployment) { state[:deployer].is_deployment? # vs is_teardown? }
condition(:is_netapp) do
state[:stuff]["hardware_type"] == "storage" && state[:stuff]["vendor"] == "netapp"
end
# now lets combine the named conditionals
execute_when { is_deployment && is_netapp }
# do this stuff pretty early on
set_priority 1
# this is effectively vendor specific hardware logic that only makes sense in the case
# of a deployment (and not a tear down) of netapp kit and will only ever be run when incoming
# stuff is netapp related in a build scenario
execute do
volume = My::Thing::NetAppVolumeBuilder.new(state[:stuff])
volume.build_the_things!
end
endBut pesky users want to notify their slack channel once we're done building stuff and we dont want to be supporting every possible chat system/notifier etc ever cos we'd never finish....so, user makes a rule and drops it in /etc/mything/rules/slack_all_the_things.rb
Rule::create_rule(:slack_all_the_things) do
# at the end of every deploy
priority 999
require_state :stuff, Deployment::Stuff
require_state :deployer, Deployer
condition(:is_deployment) { state[:deployer].is_deployment? || state[:deployer].is_teardown? }
execute_when { is_deployment }
execute do
msg = "Deployment of %s: status: %s" % [state[:deployer].name, state[:deployer].status]
require 'some_slack_gem'
Some_slask_gem.do_some_magic_to_post(msg)
end
endAnd now for any rule based logic thats either a deployment or a teardown the user will be notified.
This will also call all the rules thats interested in deploying this stuff in whatever mode deployer is in. All logic extracted and arbitrarily extensible.
Adding new types of hardware like switches doesn't require a change to this code - just rules that detect when stuff is a deployment for new types of hardware.
def deploy!(stuff, deployer)
rules = Thing::Rules.new
rules.load_rules!
rules.run_with_state(:stuff => stuff, :deployer => deployer).each do |result|
if result[:error]
warn("Processing rule %s failed because: %s: %s" % [result[:name], result[:error].class, result[:error].to_s]
else
debug("Processing rule %s passed with output:\n%s" % [result[:name], result[:out]]
end
end
end