Skip to content

Instantly share code, notes, and snippets.

@rbarraud
Forked from jimsynz/01_turnstile.rb
Created July 24, 2014 07:16
Show Gist options
  • Select an option

  • Save rbarraud/069ae2d03057bf961dbf to your computer and use it in GitHub Desktop.

Select an option

Save rbarraud/069ae2d03057bf961dbf to your computer and use it in GitHub Desktop.

Revisions

  1. James Harton revised this gist Jul 23, 2014. 8 changed files with 27 additions and 21 deletions.
    1 change: 0 additions & 1 deletion 01_turnstile.rb
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,6 @@
    # Our simple example of a Turnstile state machine.

    class Turnstile

    def initialize
    @state = "Locked"
    end
    2 changes: 2 additions & 0 deletions 03_shopping_cart_state.rb
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    # This is the final version of our ShoppingCartState code as presented.

    module ShoppingCartState
    StateError = Class.new(RuntimeError)

    18 changes: 1 addition & 17 deletions 04_shopping_cart_state_spec.rb
    Original file line number Diff line number Diff line change
    @@ -1,21 +1,5 @@
    require './03_shopping_cart_state'

    # This is code I pulled in from another project to mock the
    # `update_attributes!` method. Sorry about the noise.
    module MockUpdateAttributes
    def mock_update_attributes_on(model)
    update_proc = proc do |attrs|
    attrs.each do |attr,value|
    setter = "#{attr}=".to_sym
    expect(model).to respond_to attr
    expect(model).to respond_to setter
    model.public_send setter, value
    end
    end
    allow(model).to receive(:update_attributes!, &update_proc)
    model
    end
    end
    require './09_mock_update_attributes'

    describe ShoppingCartState do
    include MockUpdateAttributes
    4 changes: 2 additions & 2 deletions 05_shopping_cart.rb
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,8 @@
    # This is a fake AR base class.

    require './03_shopping_cart_state'
    require './99_ar'

    # The final version of our ShoppingCart class, as described in the talk.

    class ShoppingCart < AR::Base

    has_many :line_items
    2 changes: 2 additions & 0 deletions 09_shopping_cart.rb
    Original file line number Diff line number Diff line change
    @@ -3,6 +3,8 @@
    require './99_ar'

    # This is the ShoppingCart using the StateDelegator mixin.
    # As mentioned in the improvements section of the talk.

    class ShoppingCart < AR::Base
    include StateDelegator

    3 changes: 3 additions & 0 deletions 10_shopping_cart_spec.rb
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,8 @@
    require './09_shopping_cart.rb'

    # Exactly the same specs as in `06_shopping_cart_spec.rb` except we're testing
    # the StateDelegator version of the ShoppingCart.

    describe ShoppingCart do
    %w| line_items add_item! cancel! pay! ship! fulfil! state state= |.each do |method|
    it { should respond_to method }
    16 changes: 16 additions & 0 deletions 98_mock_update_attributes.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    # This is code I pulled in from another project to mock the
    # `update_attributes!` method. Sorry about the noise.
    module MockUpdateAttributes
    def mock_update_attributes_on(model)
    update_proc = proc do |attrs|
    attrs.each do |attr,value|
    setter = "#{attr}=".to_sym
    expect(model).to respond_to attr
    expect(model).to respond_to setter
    model.public_send setter, value
    end
    end
    allow(model).to receive(:update_attributes!, &update_proc)
    model
    end
    end
    2 changes: 1 addition & 1 deletion 99_ar.rb
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # This is a fake AR base class for our purposes..
    # This is a fake AR base class sufficient for our purposes.
    module AR

    class Base
  2. James Harton revised this gist Jul 23, 2014. 6 changed files with 166 additions and 22 deletions.
    23 changes: 1 addition & 22 deletions 05_shopping_cart.rb
    Original file line number Diff line number Diff line change
    @@ -1,25 +1,7 @@
    # This is a fake AR base class.

    require './03_shopping_cart_state'

    module AR
    class Base
    attr_accessor :state, :line_items

    def initialize
    @state = 'New'
    @line_items = []
    end

    def update_attributes! attrs={}
    attrs.each do |attr, value|
    public_send "#{attr}=", value
    end
    end

    def self.has_many _; end
    end
    end
    require './99_ar'

    class ShoppingCart < AR::Base

    @@ -52,7 +34,4 @@ def send_successful_shipping_email
    def current_state
    ShoppingCartState.const_get(state).new(self)
    end



    end
    31 changes: 31 additions & 0 deletions 07_state_delegator.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,31 @@
    # Mix this module into your model to automatically create delegates
    # to the relevant state class.
    # This module assumes that if you are mixing it into the `Order` class
    # then it will be delegating to the `OrderState` class heirarchy.

    module StateDelegator

    def self.included model_class
    model_name = model_class.to_s
    state_name = "#{model_name}State"
    state_base = const_get "#{state_name}::Base"

    state_methods = state_base.public_instance_methods(false)

    mixin = Module.new do
    state_methods.each do |state_method|
    define_method state_method do |*args|
    current_state.public_send(state_method, *args)
    end
    end

    define_method :current_state do
    Object.const_get("::#{state_name}").const_get(state).new(self)
    end

    private :current_state
    end
    model_class.send :include, mixin
    end

    end
    48 changes: 48 additions & 0 deletions 08_state_delegator_spec.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    require './07_state_delegator'

    describe StateDelegator do
    before do
    class DemoState
    class Base
    def initialize stateful; end
    def foo?; false; end
    def bar?; false; end
    end
    end

    class Demo < Struct.new(:state)
    include StateDelegator
    end
    end

    after do
    DemoState.send :remove_const, :Base
    Object.send :remove_const, :DemoState
    Object.send :remove_const, :Demo
    end

    subject { Demo.new 'Base' }

    describe 'delegator definition' do
    %w| foo? bar? |.each do |predicate|
    describe "##{predicate}" do
    it { should respond_to predicate }

    it 'delegates to the state instance' do
    expect_any_instance_of(DemoState::Base).to receive(predicate)
    subject.public_send predicate
    end
    end
    end
    end

    describe 'state finder method' do
    it 'has the current_state method' do
    expect(subject.private_methods).to include(:current_state)
    end

    it 'delegates to the state class' do
    expect(subject.send :current_state).to be_a(DemoState::Base)
    end
    end
    end
    28 changes: 28 additions & 0 deletions 09_shopping_cart.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    require './03_shopping_cart_state'
    require './07_state_delegator'
    require './99_ar'

    # This is the ShoppingCart using the StateDelegator mixin.
    class ShoppingCart < AR::Base
    include StateDelegator

    has_many :line_items

    def ship!
    current_state.ship! do
    get_tracking_ticket_no
    end

    send_successful_shipping_email
    end

    private

    def get_tracking_ticket_no
    # noop
    end

    def send_successful_shipping_email
    # noop
    end
    end
    38 changes: 38 additions & 0 deletions 10_shopping_cart_spec.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    require './09_shopping_cart.rb'

    describe ShoppingCart do
    %w| line_items add_item! cancel! pay! ship! fulfil! state state= |.each do |method|
    it { should respond_to method }
    end

    describe 'State delegation' do
    %w| add_item! cancel! pay! ship! fulfil! |.each do |action|
    let(:current_state) { double :current_state }
    before do
    allow(subject).to receive(:current_state).and_return(current_state)
    end

    describe "##{action}" do
    it 'delegates to current state' do
    expect(current_state).to receive(action)
    subject.public_send(action)
    end
    end
    end
    end

    describe '#ship!' do
    before { subject.state = 'ReadyToShip' }

    it 'retrieves tracking info' do
    expect(subject).to receive(:get_tracking_ticket_no)
    subject.ship!
    end

    it 'sends a shipping email' do
    expect(subject).to receive(:send_successful_shipping_email)
    subject.ship!
    end
    end

    end
    20 changes: 20 additions & 0 deletions 99_ar.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,20 @@
    # This is a fake AR base class for our purposes..
    module AR

    class Base
    attr_accessor :state, :line_items

    def initialize
    @state = 'New'
    @line_items = []
    end

    def update_attributes! attrs={}
    attrs.each do |attr, value|
    public_send "#{attr}=", value
    end
    end

    def self.has_many _; end
    end
    end
  3. James Harton revised this gist Jul 23, 2014. 3 changed files with 97 additions and 0 deletions.
    1 change: 1 addition & 0 deletions 03_shopping_cart_state.rb
    Original file line number Diff line number Diff line change
    @@ -47,6 +47,7 @@ def fulfil!

    class ReadyToShip < Cancellable
    def ship!
    yield if block_given?
    @cart.update_attributes! state: 'Shipped'
    end
    end
    58 changes: 58 additions & 0 deletions 05_shopping_cart.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,58 @@
    # This is a fake AR base class.

    require './03_shopping_cart_state'

    module AR
    class Base
    attr_accessor :state, :line_items

    def initialize
    @state = 'New'
    @line_items = []
    end

    def update_attributes! attrs={}
    attrs.each do |attr, value|
    public_send "#{attr}=", value
    end
    end

    def self.has_many _; end
    end
    end

    class ShoppingCart < AR::Base

    has_many :line_items

    %w| add_item! cancel! pay! fulfil! |.each do |action|
    define_method action do |*args|
    current_state.public_send action, *args
    end
    end

    def ship!
    current_state.ship! do
    get_tracking_ticket_no
    end

    send_successful_shipping_email
    end

    private

    def get_tracking_ticket_no
    # noop
    end

    def send_successful_shipping_email
    # noop
    end

    def current_state
    ShoppingCartState.const_get(state).new(self)
    end



    end
    38 changes: 38 additions & 0 deletions 06_shopping_cart_spec.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    require './05_shopping_cart.rb'

    describe ShoppingCart do
    %w| line_items add_item! cancel! pay! ship! fulfil! state state= |.each do |method|
    it { should respond_to method }
    end

    describe 'State delegation' do
    %w| add_item! cancel! pay! ship! fulfil! |.each do |action|
    let(:current_state) { double :current_state }
    before do
    allow(subject).to receive(:current_state).and_return(current_state)
    end

    describe "##{action}" do
    it 'delegates to current state' do
    expect(current_state).to receive(action)
    subject.public_send(action)
    end
    end
    end
    end

    describe '#ship!' do
    before { subject.state = 'ReadyToShip' }

    it 'retrieves tracking info' do
    expect(subject).to receive(:get_tracking_ticket_no)
    subject.ship!
    end

    it 'sends a shipping email' do
    expect(subject).to receive(:send_successful_shipping_email)
    subject.ship!
    end
    end

    end
  4. James Harton revised this gist Jul 23, 2014. 2 changed files with 173 additions and 0 deletions.
    59 changes: 59 additions & 0 deletions 03_shopping_cart_state.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,59 @@
    module ShoppingCartState
    StateError = Class.new(RuntimeError)

    class Base
    def initialize cart
    @cart = cart
    end

    %w| add_item! pay! cancel! ship! fulfil! |.each do |action|
    define_method action do |*_|
    raise StateError, "Can't call #{action} from #{@cart.state}"
    end
    end
    end

    class Cancellable < Base
    def cancel!
    @cart.update_attributes! state: 'Cancelled'
    end
    end

    class New < Cancellable
    def add_item! item
    @cart.line_items << item
    end

    def pay!
    @cart.update_attributes! state: 'Paid'
    end
    end

    class Paid < Cancellable
    def fulfil!
    if @cart.all_in_stock?
    @cart.update_attributes! state: 'ReadyToShip'
    else
    @cart.update_attributes! state: 'Backorder'
    end
    end
    end

    class Backorder < Cancellable
    def fulfil!
    @cart.update_attributes! state: 'ReadyToShip'
    end
    end

    class ReadyToShip < Cancellable
    def ship!
    @cart.update_attributes! state: 'Shipped'
    end
    end

    class Cancelled < Base
    end

    class Shipped < Base
    end
    end
    114 changes: 114 additions & 0 deletions 04_shopping_cart_state_spec.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,114 @@
    require './03_shopping_cart_state'

    # This is code I pulled in from another project to mock the
    # `update_attributes!` method. Sorry about the noise.
    module MockUpdateAttributes
    def mock_update_attributes_on(model)
    update_proc = proc do |attrs|
    attrs.each do |attr,value|
    setter = "#{attr}=".to_sym
    expect(model).to respond_to attr
    expect(model).to respond_to setter
    model.public_send setter, value
    end
    end
    allow(model).to receive(:update_attributes!, &update_proc)
    model
    end
    end

    describe ShoppingCartState do
    include MockUpdateAttributes

    let(:cart) { mock_update_attributes_on Struct.new(:state).new }
    subject { described_class.new cart }

    describe ShoppingCartState::Base do

    %w| add_item! pay! cancel! ship! fulfil! |.each do |action|
    describe "##{action}" do
    it 'raises a StateError' do
    expect { subject.public_send action }.to raise_error ShoppingCartState::StateError
    end
    end
    end

    end

    describe ShoppingCartState::Cancellable do
    it { should be_a ShoppingCartState::Base }

    describe '#cancel!' do
    it 'changes the cart state to Cancelled' do
    expect { subject.cancel! }.to change { cart.state }.to('Cancelled')
    end
    end
    end

    describe ShoppingCartState::New do
    it { should be_a ShoppingCartState::Cancellable }

    describe '#add_item!' do
    it 'adds an item to the cart' do
    allow(cart).to receive(:line_items).and_return([])
    expect { subject.add_item! :item }.to change { cart.line_items.length }.by(1)
    end
    end

    describe '#pay!' do
    it 'changes the cart state to Paid' do
    expect { subject.pay! }.to change { cart.state }.to('Paid')
    end
    end
    end

    describe ShoppingCartState::Paid do
    it { should be_a ShoppingCartState::Cancellable }

    describe '#fulfil!' do
    context "When all line items are in stock" do
    it 'changes the cart state to ReadyToShip' do
    allow(cart).to receive(:all_in_stock?).and_return(true)
    expect { subject.fulfil! }.to change { cart.state }.to('ReadyToShip')
    end
    end

    context "When all line items are not in stock" do
    it 'changes the cart state to Backorder' do
    allow(cart).to receive(:all_in_stock?).and_return(false)
    expect { subject.fulfil! }.to change { cart.state }.to('Backorder')
    end
    end
    end
    end

    describe ShoppingCartState::Backorder do
    it { should be_a ShoppingCartState::Cancellable }

    describe '#fulfil!' do
    it 'changes the cart state to ReadyToShip' do
    expect { subject.fulfil! }.to change { cart.state }.to('ReadyToShip')
    end
    end
    end

    describe ShoppingCartState::ReadyToShip do
    it { should be_a ShoppingCartState::Cancellable }

    describe '#ship!' do
    it 'changes the cart state to Shipped' do
    expect { subject.ship! }.to change { cart.state }.to('Shipped')
    end
    end
    end

    describe ShoppingCartState::Cancelled do
    it { should be_a ShoppingCartState::Base }
    it { should_not be_a ShoppingCartState::Cancellable }
    end

    describe ShoppingCartState::Shipped do
    it { should be_a ShoppingCartState::Base }
    it { should_not be_a ShoppingCartState::Cancellable }
    end
    end
  5. James Harton revised this gist Jul 23, 2014. 2 changed files with 56 additions and 0 deletions.
    24 changes: 24 additions & 0 deletions 01_turnstile.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,24 @@
    # Our simple example of a Turnstile state machine.

    class Turnstile

    def initialize
    @state = "Locked"
    end

    def push!
    @state = "Locked" if unlocked?
    end

    def pay!
    @state = "Unlocked" if locked?
    end

    def locked?
    @state == "Locked"
    end

    def unlocked?
    @state = "Unlocked"
    end
    end
    32 changes: 32 additions & 0 deletions 02_turnstile_spec.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    require './01_turnstile'

    describe Turnstile do
    subject { described_class.new }
    it { should be_locked }

    context "When it is locked" do
    context "And we push it" do
    before { subject.push! }
    it { should be_locked }
    end

    context "And we pay it" do
    before { subject.pay! }
    it { should be_unlocked }
    end
    end

    context "When it is unlocked" do
    before { subject.pay! }

    context "And we push it" do
    before { subject.push! }
    it { should be_locked }
    end

    context "And we pay it" do
    before { subject.pay! }
    it { should be_unlocked }
    end
    end
    end
  6. James Harton revised this gist Jul 23, 2014. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion foo.rb
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    sdfsd
  7. James Harton created this gist Jul 23, 2014.
    1 change: 1 addition & 0 deletions foo.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    sdfsd