statesman-events 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2c7a41793f54b53f461686fbe163233c2d268e7d
4
+ data.tar.gz: 17c393eb70b31a656c7777cbc3d34362cd7d717b
5
+ SHA512:
6
+ metadata.gz: dcb60ad78a61130a3628f3030ea67a193692705747f721b013aeb5b0685ba7aa20d6901e4523cf3680d1aa2af9c383d2dcfe0ab61befd50b0a7a4cd7188f6175
7
+ data.tar.gz: c636be370afa76f3871c1f8da47a0863af25a6042c8b444729d8be787891f2ef0bb2673ba3128df275c9ca9bd8b469e703e3c65b4e6e4086b5bb680653810038
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rspec
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
@@ -0,0 +1,48 @@
1
+ # For all options see https://github.com/bbatsov/rubocop/tree/master/config
2
+
3
+ AllCops:
4
+ Include:
5
+ - Rakefile
6
+ - statesman.gemfile
7
+ - lib/tasks/*.rake
8
+ Exclude:
9
+ - vendor/**/*
10
+ - .*/**
11
+ - spec/fixtures/**/*
12
+
13
+ StringLiterals:
14
+ Enabled: false
15
+
16
+ Documentation:
17
+ Enabled: false
18
+
19
+ SignalException:
20
+ EnforcedStyle: only_raise
21
+
22
+ # Avoid methods longer than 15 lines of code
23
+ MethodLength:
24
+ CountComments: false
25
+ Max: 15
26
+
27
+ AbcSize:
28
+ Max: 25
29
+
30
+ # Don't require utf-8 encoding comment
31
+ Encoding:
32
+ Enabled: false
33
+
34
+ LineLength:
35
+ Max: 80
36
+
37
+ GuardClause:
38
+ Enabled: false
39
+
40
+ SingleSpaceBeforeFirstArg:
41
+ Enabled: false
42
+
43
+ DotPosition:
44
+ EnforcedStyle: trailing
45
+
46
+ # Allow class and message or instance raises
47
+ Style/RaiseArgs:
48
+ Enabled: false
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.2
5
+ - 2.1
6
+ - 2.0
7
+
8
+ sudo: false
9
+
10
+ script:
11
+ - bundle exec rubocop
12
+ - bundle exec rake
@@ -0,0 +1,2 @@
1
+ ## v0.0.1, 5 December 2015
2
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, all_on_start: true, cmd: 'bundle exec rspec --color' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
10
+ guard :rubocop, all_on_start: true, cli: ['--format', 'clang'] do
11
+ watch(%r{.+\.rb$})
12
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
13
+ watch(%r{(?:.+/)?\rubocop-todo\.yml$}) { |m| File.dirname(m[0]) }
14
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Grey Baker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,86 @@
1
+ Event support for [Statesman](https://github.com/gocardless/statesman).
2
+
3
+ ## TL;DR Usage
4
+
5
+ ```ruby
6
+ class TaskStateMachine
7
+ include Statesman::Machine
8
+ include Statesman::Events
9
+
10
+ state :unstarted, initial: true
11
+ state :started
12
+ state :finished
13
+ state :cancelled
14
+
15
+ event :start do
16
+ transition from: :unstarted, to: :started
17
+ end
18
+
19
+ event :finish do
20
+ transition from: :started, to: :finished
21
+ end
22
+
23
+ event :cancel do
24
+ transition from: :unstarted, to: :cancelled
25
+ transition from: :started, to: :cancelled
26
+ end
27
+
28
+ event :restart do
29
+ transition from: :finished, to: :started
30
+ transition from: :cancelled, to: :started
31
+ end
32
+ end
33
+
34
+ class Task < ActiveRecord::Base
35
+ delegate :current_state, :trigger, :available_events, to: :state_machine
36
+
37
+ def state_machine
38
+ @state_machine ||= TaskStateMachine.new(self)
39
+ end
40
+ end
41
+
42
+ task = Task.new
43
+
44
+ task.current_state # => "unstarted"
45
+ task.trigger(:start) # => true/false
46
+ task.current_state # => "started"
47
+ task.available_events # => [:finish, :cancel]
48
+ ```
49
+
50
+ ## Class methods
51
+
52
+ #### `Events.event`
53
+ ```ruby
54
+ ExampleMachine.event(:some_event) do
55
+ transition from: :some_state, to: :another_state
56
+ transition from: :some_other_state, to: :yet_another_state
57
+ end
58
+ ```
59
+ Define an event rule. When triggered, the first available transition from the
60
+ current state will be called.
61
+
62
+ ## Instance methods
63
+
64
+ #### `Event#trigger`
65
+ ```ruby
66
+ instance.trigger(:some_event)
67
+ ```
68
+ Triggers the passed event, returning `true` on success. Returns false on
69
+ failure.
70
+
71
+ #### `Event#trigger!`
72
+ ```ruby
73
+ instance.trigger(:some_event)
74
+ ```
75
+ Triggers the passed event, returning `true` on success. Raises
76
+ `Statesman::GuardFailedError` or `Statesman::TransitionFailedError` on failure.
77
+
78
+ #### `Event#available_events`
79
+ ```ruby
80
+ instance.available_events
81
+ ```
82
+ Returns an array of events you can `trigger` from the current state.
83
+
84
+ ---
85
+
86
+ GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/jobs#software-engineer).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,32 @@
1
+ module Statesman
2
+ class EventTransitions
3
+ attr_reader :machine, :event_name
4
+
5
+ def initialize(machine, event_name, &block)
6
+ @machine = machine
7
+ @event_name = event_name
8
+ instance_eval(&block)
9
+ end
10
+
11
+ def transition(from: nil, to: nil)
12
+ from = to_s_or_nil(from)
13
+ to = array_to_s_or_nil(to)
14
+
15
+ machine.transition(from: from, to: to)
16
+
17
+ machine.events[event_name] ||= {}
18
+ machine.events[event_name][from] ||= []
19
+ machine.events[event_name][from] += to
20
+ end
21
+
22
+ private
23
+
24
+ def to_s_or_nil(input)
25
+ input.nil? ? input : input.to_s
26
+ end
27
+
28
+ def array_to_s_or_nil(input)
29
+ Array(input).map { |item| to_s_or_nil(item) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ require_relative "event_transitions"
2
+
3
+ # Adds support for events when `extend`ed into state machine classes
4
+ module Statesman
5
+ module Events
6
+ def self.included(base)
7
+ unless base.respond_to?(:states)
8
+ raise "Statesman::Events included before/without Statesman::Machine"
9
+ end
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def events
15
+ @events ||= {}
16
+ end
17
+
18
+ def event(name, &block)
19
+ EventTransitions.new(self, name, &block)
20
+ end
21
+ end
22
+
23
+ def trigger!(event_name, metadata = {})
24
+ transitions = self.class.events.fetch(event_name) do
25
+ raise Statesman::TransitionFailedError,
26
+ "Event #{event_name} not found"
27
+ end
28
+
29
+ new_state = transitions.fetch(current_state) do
30
+ raise Statesman::TransitionFailedError,
31
+ "State #{current_state} not found for Event #{event_name}"
32
+ end
33
+
34
+ transition_to!(new_state.first, metadata)
35
+ true
36
+ end
37
+
38
+ def trigger(event_name, metadata = {})
39
+ self.trigger!(event_name, metadata)
40
+ rescue Statesman::TransitionFailedError, Statesman::GuardFailedError
41
+ false
42
+ end
43
+
44
+ def available_events
45
+ state = current_state
46
+ self.class.events.select do |_, transitions|
47
+ transitions.key?(state)
48
+ end.map(&:first)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ module Statesman
2
+ module Events
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ require "statesman"
2
+ require "statesman/events"
3
+ require "rspec/its"
4
+
5
+ RSpec.configure do |config|
6
+ config.raise_errors_for_deprecations!
7
+ config.mock_with(:rspec) { |mocks| mocks.verify_partial_doubles = true }
8
+ config.order = "random"
9
+ end
@@ -0,0 +1,167 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesman::Events do
4
+ let(:machine) do
5
+ Class.new do
6
+ include Statesman::Machine
7
+ include Statesman::Events
8
+ end
9
+ end
10
+ let(:my_model) { Class.new { attr_accessor :current_state }.new }
11
+
12
+ describe "inclusion" do
13
+ context "after Statesman::Machine" do
14
+ let(:machine) do
15
+ Class.new do
16
+ include Statesman::Machine
17
+ include Statesman::Events
18
+ end
19
+ end
20
+
21
+ specify { expect { machine.events }.to_not raise_error }
22
+ end
23
+
24
+ context "without Statesman::Machine" do
25
+ let(:machine) { Class.new { include Statesman::Events } }
26
+
27
+ it "raises a descriptive error" do
28
+ expect { machine.events }.to raise_error(/without Statesman::Machine/)
29
+ end
30
+ end
31
+
32
+ context "before Statesman::Machine" do
33
+ let(:machine) do
34
+ Class.new do
35
+ include Statesman::Events
36
+ include Statesman::Machine
37
+ end
38
+ end
39
+
40
+ it "raises a descriptive error" do
41
+ expect { machine.events }.to raise_error(/without Statesman::Machine/)
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#event" do
47
+ before do
48
+ machine.class_eval do
49
+ state :x, initial: true
50
+ state :y
51
+ state :z
52
+
53
+ event :event_1 do
54
+ transition from: :x, to: :y
55
+ end
56
+
57
+ event :event_2 do
58
+ transition from: :y, to: :z
59
+ end
60
+ end
61
+ end
62
+
63
+ let(:instance) { machine.new(my_model) }
64
+
65
+ context "when the state cannot be transitioned to" do
66
+ it "raises an error" do
67
+ expect { instance.trigger!(:event_2) }.
68
+ to raise_error(Statesman::TransitionFailedError)
69
+ end
70
+ end
71
+
72
+ context "when the state can be transitioned to" do
73
+ it "changes state" do
74
+ instance.trigger!(:event_1)
75
+ expect(instance.current_state).to eq("y")
76
+ end
77
+
78
+ it "creates a new transition object" do
79
+ expect { instance.trigger!(:event_1) }.
80
+ to change(instance.history, :count).by(1)
81
+
82
+ expect(instance.history.first).
83
+ to be_a(Statesman::Adapters::MemoryTransition)
84
+ expect(instance.history.first.to_state).to eq("y")
85
+ end
86
+
87
+ it "sends metadata to the transition object" do
88
+ meta = { "my" => "hash" }
89
+ instance.trigger!(:event_1, meta)
90
+ expect(instance.history.first.metadata).to eq(meta)
91
+ end
92
+
93
+ it "sets an empty hash as the metadata if not specified" do
94
+ instance.trigger!(:event_1)
95
+ expect(instance.history.first.metadata).to eq({})
96
+ end
97
+
98
+ it "returns true" do
99
+ expect(instance.trigger!(:event_1)).to eq(true)
100
+ end
101
+
102
+ context "with a guard" do
103
+ let(:result) { true }
104
+ # rubocop:disable UnusedBlockArgument
105
+ let(:guard_cb) { ->(*args) { result } }
106
+ # rubocop:enable UnusedBlockArgument
107
+ before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
108
+
109
+ context "and an object to act on" do
110
+ let(:instance) { machine.new(my_model) }
111
+
112
+ it "passes the object to the guard" do
113
+ expect(guard_cb).to receive(:call).once.
114
+ with(my_model, instance.last_transition, {}).and_return(true)
115
+ instance.trigger!(:event_1)
116
+ end
117
+ end
118
+
119
+ context "which passes" do
120
+ it "changes state" do
121
+ expect { instance.trigger!(:event_1) }.
122
+ to change { instance.current_state }.to("y")
123
+ end
124
+ end
125
+
126
+ context "which fails" do
127
+ let(:result) { false }
128
+
129
+ it "raises an exception" do
130
+ expect { instance.trigger!(:event_1) }.
131
+ to raise_error(Statesman::GuardFailedError)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "#available_events" do
139
+ before do
140
+ machine.class_eval do
141
+ state :x, initial: true
142
+ state :y
143
+ state :z
144
+
145
+ event :event_1 do
146
+ transition from: :x, to: :y
147
+ end
148
+
149
+ event :event_2 do
150
+ transition from: :y, to: :z
151
+ end
152
+
153
+ event :event_3 do
154
+ transition from: :x, to: :y
155
+ transition from: :y, to: :x
156
+ end
157
+ end
158
+ end
159
+
160
+ let(:instance) { machine.new(my_model) }
161
+ it "should return list of available events for the current state" do
162
+ expect(instance.available_events).to eq([:event_1, :event_3])
163
+ instance.trigger!(:event_1)
164
+ expect(instance.available_events).to eq([:event_2, :event_3])
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,26 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'statesman/events/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "statesman-events"
7
+ spec.version = Statesman::Events::VERSION
8
+ spec.authors = ["Grey Baker"]
9
+ spec.email = ["developers@gocardless.com"]
10
+ spec.description = %q{Event support for Statesman}
11
+ spec.summary = spec.description
12
+ spec.homepage = "https://github.com/gocardless/statesman-events"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "statesman", ">= 1.3"
21
+
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", "~> 3.1"
24
+ spec.add_development_dependency "rspec-its", "~> 1.1"
25
+ spec.add_development_dependency "rubocop", "~> 0.30.0"
26
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statesman-events
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Grey Baker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: statesman
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-its
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 0.30.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.30.0
83
+ description: Event support for Statesman
84
+ email:
85
+ - developers@gocardless.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - .rubocop.yml
92
+ - .travis.yml
93
+ - CHANGELOG.md
94
+ - Gemfile
95
+ - Guardfile
96
+ - LICENSE.txt
97
+ - README.md
98
+ - Rakefile
99
+ - lib/statesman/event_transitions.rb
100
+ - lib/statesman/events.rb
101
+ - lib/statesman/events/version.rb
102
+ - spec/spec_helper.rb
103
+ - spec/statesman/events_spec.rb
104
+ - statesman-events.gemspec
105
+ homepage: https://github.com/gocardless/statesman-events
106
+ licenses:
107
+ - MIT
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.2.2
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Event support for Statesman
129
+ test_files:
130
+ - spec/spec_helper.rb
131
+ - spec/statesman/events_spec.rb