diff --git a/lib/stealth/base.rb b/lib/stealth/base.rb index 1a60fce..8a25eb5 100644 --- a/lib/stealth/base.rb +++ b/lib/stealth/base.rb @@ -59,6 +59,7 @@ def self.load_environment require_directory("config/initializers") # Require explicitly to ensure it loads first require File.join(Stealth.root, 'bot', 'controllers', 'bot_controller') + require File.join(Stealth.root, 'config', 'flow_map') require_directory("bot") end diff --git a/lib/stealth/controller/controller.rb b/lib/stealth/controller/controller.rb index 98c6dec..15078e9 100644 --- a/lib/stealth/controller/controller.rb +++ b/lib/stealth/controller/controller.rb @@ -129,8 +129,7 @@ def get_flow_and_state(session: nil, flow: nil, state: nil) if flow.present? if state.blank? - flow_klass = [flow.to_s, 'flow'].join('_').classify.constantize - state = flow_klass.flow_spec.states.keys.first.to_s + state = FlowMap.flow_spec[flow.to_sym].states.keys.first.to_s end return flow.to_s, state.to_s diff --git a/lib/stealth/flow/base.rb b/lib/stealth/flow/base.rb index 526b138..fa260a9 100644 --- a/lib/stealth/flow/base.rb +++ b/lib/stealth/flow/base.rb @@ -13,39 +13,41 @@ module Flow class_methods do attr_reader :flow_spec - def flow(&specification) - @flow_spec = Specification.new(&specification) + def flow(flow_name, &specification) + @flow_spec = {} unless @flow_spec.present? + @flow_spec[flow_name.to_sym] = Specification.new(&specification) end end included do - attr_accessor :flow_state, :user_id + attr_accessor :flow, :flow_state, :user_id def current_state - res = spec.states[@flow_state.to_sym] if @flow_state - res || spec.initial_state + res = self.spec.states[@flow_state.to_sym] if @flow_state + res || self.spec.initial_state end - def spec - # check the singleton class first - class << self - return flow_spec if flow_spec - end + def current_flow + @flow || self.class.flow_spec.keys.first + end - self.class.flow_spec + def spec + self.class.flow_spec[current_flow] end def states self.spec.states.keys end - def init_state(state) - raise(ArgumentError, 'No state was specified.') if state.blank? - + def init(flow:, state:) + new_flow = flow.to_sym new_state = state.to_sym - unless states.include?(new_state) - raise(Stealth::Errors::InvalidStateTransition, "Unknown state '#{state}' for #{self.class.to_s}") + + unless state_exists?(potential_flow: new_flow, potential_state: new_state) + raise(Stealth::Errors::InvalidStateTransition, "Unknown state '#{new_state}' for '#{new_flow}' flow") end + + @flow = new_flow @flow_state = new_state self @@ -54,7 +56,11 @@ def init_state(state) private def flow_and_state - [self.class.to_s, current_state].join("->") + [current_flow, current_state].join("->") + end + + def state_exists?(potential_flow:, potential_state:) + self.class.flow_spec[potential_flow].states.include?(potential_state) end end diff --git a/lib/stealth/session.rb b/lib/stealth/session.rb index 033133e..f5a338f 100644 --- a/lib/stealth/session.rb +++ b/lib/stealth/session.rb @@ -31,11 +31,7 @@ def self.flow_and_state_from_session_slug(slug:) def flow return nil if flow_string.blank? - @flow ||= begin - flow_klass = [flow_string, 'flow'].join('_').classify.constantize - flow = flow_klass.new.init_state(state_string) - flow - end + @flow ||= FlowMap.new.init(flow: flow_string, state: state_string) end def state diff --git a/spec/controller/callbacks_spec.rb b/spec/controller/callbacks_spec.rb index bf64064..8632ca9 100644 --- a/spec/controller/callbacks_spec.rb +++ b/spec/controller/callbacks_spec.rb @@ -143,20 +143,16 @@ def run_around_filter end end -class FlowTesterFlow +class FlowMap include Stealth::Flow - flow do + flow :flow_tester do state :my_action state :my_action2 state :my_action3 end -end - -class OtherFlowTesterFlow - include Stealth::Flow - flow do + flow :other_flow_tester do state :other_action state :other_action2 state :other_action3 diff --git a/spec/controller/controller_spec.rb b/spec/controller/controller_spec.rb index 99165e7..179f120 100644 --- a/spec/controller/controller_spec.rb +++ b/spec/controller/controller_spec.rb @@ -33,20 +33,16 @@ def other_action3 end end - class MrRobotFlow + class FlowMap include Stealth::Flow - flow do + flow :mr_robot do state :my_action state :my_action2 state :my_action3 end - end - - class MrTronFlow - include Stealth::Flow - flow do + flow :mr_tron do state :other_action state :other_action2 state :other_action3 diff --git a/spec/flow/flow_spec.rb b/spec/flow/flow_spec.rb index da37b7e..4b5605d 100644 --- a/spec/flow/flow_spec.rb +++ b/spec/flow/flow_spec.rb @@ -5,57 +5,78 @@ describe Stealth::Flow do - class NewTodoFlow + class CustomFlowMap include Stealth::Flow - flow do + flow :new_todo do state :new - state :get_due_date - state :created - state :error end + + flow :hello do + state :say_hello + state :say_oi + end + + flow "howdy" do + state :say_howdy + end end - let(:flow) { NewTodoFlow.new } + let(:flow_map) { CustomFlowMap.new } describe "inititating with states" do it "should init a state given a state name" do - flow.init_state(:created) - expect(flow.current_state).to eq :created + flow_map.init(flow: 'new_todo', state: 'created') + expect(flow_map.current_state).to eq :created - flow.init_state('error') - expect(flow.current_state).to eq :error + flow_map.init(flow: 'new_todo', state: 'error') + expect(flow_map.current_state).to eq :error end it "should raise an error if an invalid state is specified" do expect { - flow.init_state(:invalid) + flow_map.init(flow: 'new_todo', state: 'invalid') }.to raise_error(Stealth::Errors::InvalidStateTransition) end end describe "accessing states" do - it "should start out in the initial state" do - expect(flow.current_state).to eq :new + it "should default to the first flow and state" do + expect(flow_map.current_flow).to eq(:new_todo) + expect(flow_map.current_state).to eq(:new) end it "should support comparing states" do - first_state = NewTodoFlow.flow_spec.states[:new] - last_state = NewTodoFlow.flow_spec.states[:error] + first_state = CustomFlowMap.flow_spec[:new_todo].states[:new] + last_state = CustomFlowMap.flow_spec[:new_todo].states[:error] expect(first_state < last_state).to be true expect(last_state > first_state).to be true end - it "should allow every state to be fetched for the class" do - expect(NewTodoFlow.flow_spec.states.length).to eq 4 - expect(NewTodoFlow.flow_spec.states.keys).to eq([:new, :get_due_date, :created, :error]) + it "should allow every state to be fetched for a flow" do + expect(CustomFlowMap.flow_spec[:new_todo].states.length).to eq 4 + expect(CustomFlowMap.flow_spec[:hello].states.length).to eq 2 + expect(CustomFlowMap.flow_spec[:new_todo].states.keys).to eq([:new, :get_due_date, :created, :error]) + expect(CustomFlowMap.flow_spec[:hello].states.keys).to eq([:say_hello, :say_oi]) + end + + it "should return the states in an array for a given FlowMap instance" do + expect(flow_map.states).to eq [:new, :get_due_date, :created, :error] + flow_map.init(flow: :hello, state: :say_oi) + expect(flow_map.states).to eq [:say_hello, :say_oi] + end + + it "should allow flows to be specified with strings" do + expect(CustomFlowMap.flow_spec[:howdy].states.length).to eq 1 + expect(CustomFlowMap.flow_spec[:howdy].states.keys).to eq([:say_howdy]) end - it "should return the states in an array for a given flow instance" do - expect(flow.states).to eq [:new, :get_due_date, :created, :error] + it "should allow FlowMaps to be intialized with strings" do + flow_map.init(flow: "hello", state: "say_oi") + expect(flow_map.states).to eq [:say_hello, :say_oi] end end diff --git a/spec/flow/state_spec.rb b/spec/flow/state_spec.rb index 158e3ec..3e8a8d6 100644 --- a/spec/flow/state_spec.rb +++ b/spec/flow/state_spec.rb @@ -5,66 +5,63 @@ describe Stealth::Flow::State do - class NewTodoFlow + class SuperFlowMap include Stealth::Flow - flow do + flow :new_todo do state :new - state :get_due_date - state :created, fails_to: :new - state :error end end - let(:flow) { NewTodoFlow.new } + let(:flow_map) { SuperFlowMap.new } describe "flow states" do it "should convert itself to a string" do - expect(flow.current_state.to_s).to be_a(String) + expect(flow_map.current_state.to_s).to be_a(String) end it "should convert itself to a symbol" do - expect(flow.current_state.to_sym).to be_a(Symbol) + expect(flow_map.current_state.to_sym).to be_a(Symbol) end end describe "fails_to" do it "should be nil for a state that has not specified a fails_to" do - expect(flow.current_state.fails_to).to be_nil + expect(flow_map.current_state.fails_to).to be_nil end it "should return the fail_state if a fails_to was specified" do - flow.init_state(:created) - expect(flow.current_state.fails_to).to be_a(Stealth::Flow::State) - expect(flow.current_state.fails_to).to eq :new + flow_map.init(flow: :new_todo, state: :created) + expect(flow_map.current_state.fails_to).to be_a(Stealth::Flow::State) + expect(flow_map.current_state.fails_to).to eq :new end end describe "state incrementing and decrementing" do it "should increment the state" do - flow.init_state(:get_due_date) - new_state = flow.current_state + 1.state + flow_map.init(flow: :new_todo, state: :get_due_date) + new_state = flow_map.current_state + 1.state expect(new_state).to eq(:created) end it "should decrement the state" do - flow.init_state(:error) - new_state = flow.current_state - 2.states + flow_map.init(flow: :new_todo, state: :error) + new_state = flow_map.current_state - 2.states expect(new_state).to eq(:get_due_date) end it "should return the first state if the decrement is out of bounds" do - flow.init_state(:get_due_date) - new_state = flow.current_state - 5.states + flow_map.init(flow: :new_todo, state: :get_due_date) + new_state = flow_map.current_state - 5.states expect(new_state).to eq(:new) end it "should return the last state if the increment is out of bounds" do - flow.init_state(:created) - new_state = flow.current_state + 5.states + flow_map.init(flow: :new_todo, state: :created) + new_state = flow_map.current_state + 5.states expect(new_state).to eq(:error) end end diff --git a/spec/session_spec.rb b/spec/session_spec.rb index 1f3fbfb..b316424 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -3,18 +3,19 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -class NewTodoFlow +class FlowMap include Stealth::Flow - flow do + flow :new_todo do state :new - state :get_due_date - state :created, fails_to: :new - state :error end + + flow :marco do + state :polo + end end describe "Stealth::Session" do @@ -50,22 +51,14 @@ class NewTodoFlow end describe "with a session" do - class MarcoFlow - include Stealth::Flow - - flow do - state :polo - end - end - let(:session) do session = Stealth::Session.new(user_id: user_id) - session.set(flow: 'Marco', state: 'polo') + session.set(flow: 'marco', state: 'polo') session end - it "should return the flow" do - expect(session.flow).to be_a(MarcoFlow) + it "should return the FlowMap" do + expect(session.flow).to be_a(FlowMap) end it "should return the state" do @@ -74,7 +67,7 @@ class MarcoFlow end it "should return the flow_string" do - expect(session.flow_string).to eq "Marco" + expect(session.flow_string).to eq "marco" end it "should return the state_string" do @@ -91,25 +84,25 @@ class MarcoFlow let(:session) { Stealth::Session.new(user_id: user_id) } it "should increment the state" do - session.set(flow: 'NewTodo', state: 'get_due_date') + session.set(flow: 'new_todo', state: 'get_due_date') new_session = session + 1.state expect(new_session.state_string).to eq('created') end it "should decrement the state" do - session.set(flow: 'NewTodo', state: 'error') + session.set(flow: 'new_todo', state: 'error') new_session = session - 2.states expect(new_session.state_string).to eq('get_due_date') end it "should return the first state if the decrement is out of bounds" do - session.set(flow: 'NewTodo', state: 'get_due_date') + session.set(flow: 'new_todo', state: 'get_due_date') new_session = session - 5.states expect(new_session.state_string).to eq('new') end it "should return the last state if the increment is out of bounds" do - session.set(flow: 'NewTodo', state: 'created') + session.set(flow: 'new_todo', state: 'created') new_session = session + 5.states expect(new_session.state_string).to eq('error') end