lib/strict_states.rb
require "strict_states/version"
require "strict_states/strict_hash"
require "strict_states/checker"
# The *STRICT* paradigm:
#
# * Will raise an error if states are spelled wrong when lookups happen through this paradigm.
# * Typos will be noisy, and many of them will error at app-load, so impossible to miss.
#
# Uses the StrictHash to accomplish this. See lib/strict_states/strict_hash.rb
#
# The *INCLUDE WITH ARGUMENTS* paradigm:
#
# * future-proof support for any/all state machines
# * easily integrate with any state machine engine not already supported by this gem
#
# Uses a method (StrictStates.checker) that returns a module (StrictStates::Checker) to accomplish this.
module StrictStates
# Usage:
#
# class MyModel < ActiveRecord::Base
# # ...
# # <<<===--- AFTER STATE MACHINE DEFINITION ---===>>>
# # ...
# include StrictStates.checker(
# klass: self,
# machines: {
# state: :pluginaweek,
# awesome_level: :pluginaweek,
# bogus_level: ->(context, machine_name) {
# context.state_machines[machine_name.to_sym].states.map(&:name)
# }
# }
# )
# end
#
def self.checker(**config)
validate_config(config)
config[:machines] = states_for_machines(config[:klass], config[:machines])
set_strict_state_lookup(config)
::StrictStates::Checker
end
private
# Supported engines:
#
# :pluginaweek - for pluginaweek/state_machine
# :seuros - for seuros/state_machine
# :state_machines - for state-machines/state_machines
# :aasm - for aasm/aasm version < 4.3.0
# :aasm_multiple - for aasm/aasm version >= 4.3.0
#
def self.engine_name_apis
{
pluginaweek: ->(context, machine_name) { context.state_machines[machine_name.to_sym].states.map(&:name) },
seuros: ->(context, machine_name) { context.state_machines[machine_name.to_sym].states.map(&:name) },
state_machines: ->(context, machine_name) { context.state_machines[machine_name.to_sym].states.map(&:name) },
aasm: ->(context, _) { context.aasm.states.map(&:name) }, # aasm gem version < 4.3.0
aasm_multiple: ->(context, machine_name) { context.aasm(machine_name).states.map(&:name) } # aasm gem version >= 4.3.0
}
end
def self.strict_states_to_stings(states)
states.map {|state| state.to_s }
end
def self.create_strict_state_lookup(names)
default_strict_hash = names.each_with_object({}) do |state, memo|
memo[state.to_sym] = state
end
StrictHash[**default_strict_hash]
end
def self.validate_config(**config)
raise ArgumentError, "config must have a :machines key with Hash value but was #{config[:machines]}" unless config[:machines] && config[:machines].is_a?(Hash)
raise ArgumentError, ":machines Hash must have values either from #{engine_name_apis.keys} or as Procs but was #{config[:machines]}" unless test_machines(config[:machines])
raise ArgumentError, "config must have a :klass key with a Class value but was #{config[:klass]}" unless config[:klass] && config[:klass].class == Class
true
end
def self.test_machines(machines)
machines.values.all? do |engine|
engine_name_apis.keys.include?(engine) ||
engine.respond_to?(:call)
end
end
# params:
# klass - any Class object with a state machine
# machines -
# {
# state: :pluginaweek,
# awesome_level: :pluginaweek,
# bogus_level: ->(context, machine_name) {
# context.state_machines[machine_name.to_sym].states.map(&:name)}
# }
#
# Example result
#
# {
# state: ["one", "two", "three"],
# awesome_level: ["not_awesome", "awesome_11", "bad", "good"],
# bogus_level: ["new", "pending", "goofy"]
# }
#
def self.states_for_machines(klass, machines)
machines.inject({}) do |memo, (machine_name, engine)|
proc = get_proc_for_engine(engine)
memo[machine_name] =
strict_states_to_stings(
proc.call(klass, machine_name)
)
memo
end
end
def self.get_proc_for_engine(engine)
if (proc = engine_name_apis[engine])
# Predefined Engine within this gem
proc
else
# Custom state machine name extraction Proc provided by caller
engine
end
end
# params:
# config -
# {
# klass: Car, # any Class object with a state machine
# machines: { # the machine names, and states defined within each
# state: ["one", "two", "three"],
# awesome_level: ["not_awesome", "awesome_11", "bad", "good"],
# bogus_level: ["new", "pending", "goofy"]
# }
# }
#
# Result:
#
# MyModel.strict_state_lookup
# => {
# :state =>
# { :one => "one", :two => "two", :three => "three" }
# :awesome_level =>
# { :not_awesome => "not_awesome", :awesome_11 => "awesome_11", :bad => "bad", :good => "good" },
# :bogus_level =>
# { :new => "new", :pending => "pending", :goofy => "goofy" }
# }
def self.set_strict_state_lookup(config)
klass = config[:klass]
machines = config[:machines]
class << klass
attr_reader :strict_state_lookup
end
klass.instance_variable_set(:@strict_state_lookup, StrictStates::StrictHash.new)
machines.each do |machine_name, state_array|
klass.strict_state_lookup[machine_name.to_sym] = StrictStates.create_strict_state_lookup(state_array).freeze
end
end
end