interstate_machine 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ffa91c86831fa5b11b22c705b710b42a33a0e8b2
4
+ data.tar.gz: bc7349ab2a864157ee0c03ff7f5ac8fb4a4cf3a2
5
+ SHA512:
6
+ metadata.gz: e656466c3918cf7ab0cba559dccb8238ebf03d864970666532511c70a2c2dcadd1fd07b62f0616a6bd8d111e9b8aefba533b421db23d1060ca31e627566a875a
7
+ data.tar.gz: b9c00fdebd2851d3fbfffe5eca3fc64770a46e75836505e8c18e679b0c77d4aea6f91753fbb6a999843598f9cff2499235471fc0102813fffbb2010637f80b4e
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /examples/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+ .byebug_history
15
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.0
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in interstate_machine.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 SamuelMartini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # InterstateMachine
2
+ When state machine meets interactor. InterstateMachine is a simple state machine which use interactors to trigger transitions. Long story short, an object receives an event which is a interactor and you can do fantastic things with interactors.
3
+ What is an interactor?
4
+ [*"An interactor is a simple, single-purpose object."*](https://github.com/collectiveidea/interactor)
5
+
6
+ ## Installation
7
+ ```ruby
8
+ gem install interstate_machine
9
+ ```
10
+
11
+ Gemfile
12
+ ```ruby
13
+ gem 'interstate_machine', '~> 1.0.0'
14
+ ```
15
+
16
+ ## Usage
17
+ ```ruby
18
+ class TrafficLight < ActiveRecord::Base
19
+ include InterstateMachine
20
+
21
+ initial_state :stop
22
+
23
+ transition_table :stop, :proceed, :caution, :tilt, :broken do
24
+ on event: :cycle do |event|
25
+ allow event: event, transition_to: [:proceed], from: [:stop]
26
+ allow event: event, transition_to: [:caution], from: [:proceed]
27
+ allow event: event, transition_to: [:stop], from: [:caution]
28
+ end
29
+ on event: :tilt, transition_to: [:broken], from: [:proceed, :caution, :stop]
30
+ on event: :repair, transition_to: [:stop], from: [:broken]
31
+ end
32
+ end
33
+ ```
34
+ If you want to use `InterstateMachine` in plain ruby, add `attr_accessor :state` to store the state.
35
+ `transition_table` is where the state machine rules and states are defined.
36
+ Each event represent an `Interactor` that is called to process the transition.
37
+
38
+ `on` can take a block which defines different transition(rules) for the same event or a single transition
39
+
40
+ In addition to the class where you define the state machine, you also need to create interactors for each event.
41
+
42
+ In this case we have an event `cycle` that trigger many transitions so we define three interactors for the `cycle` event and two for the remaining.
43
+ `CycleProceed`, `CycleCaution`, `CycleStop` and then `Tilt` and `Repair`. Yes, when an event triggers a transition to a single state and it's not a block, you have to name the class like the event name.
44
+
45
+ ```ruby
46
+ class CycleProceed
47
+ include Interactor
48
+
49
+ before :validate_transition
50
+
51
+ def call
52
+ # do stuff needed when this state happen
53
+ 'yeah'
54
+ end
55
+
56
+ private
57
+
58
+ def validate_transition
59
+ context.fail!(error: 'there is no power') if context.object.power == 0
60
+ end
61
+ end
62
+ ```
63
+ Note: You can use all the magics like [hooks](https://github.com/collectiveidea/interactor). Whoop!
64
+
65
+ You can access the class where you have included InterstateMachine by `context.object`
66
+
67
+ When transition is allowed:
68
+ ```ruby
69
+ t = TrafficLight.new
70
+ t.cycle
71
+ #=> :proceed
72
+ ```
73
+
74
+ When transition can't happen because something wrong executing the event
75
+
76
+ ```ruby
77
+ t.power = 0
78
+ t.cycle
79
+ #=> 'there is no power'
80
+ t.state
81
+ #=> :stop
82
+ ```
83
+
84
+ When transition is no allowed
85
+ ```ruby
86
+ t.state
87
+ #=> :stop
88
+ t.repair
89
+ #=> RuntimeError Exception:
90
+ ```
91
+ ## Contributing
92
+ Feel free to play around, fork, add, pull request, and get a hug. If you decide to pull request please add tests
data/Rakefile ADDED
@@ -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,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'interstate_machine/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "interstate_machine"
8
+ spec.version = InterstateMachine::VERSION
9
+ spec.authors = ["SamuelMartini"]
10
+ spec.email = ["samueljmartini@gmail.com"]
11
+
12
+ spec.summary = 'a state machine with interactor implementation'
13
+ spec.description = 'InterstateMachine is a simple state machine which use interactors to trigger transitions'
14
+ spec.homepage = 'https://github.com/SamuelMartini/interstate_machine'
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "interactor", "~> 3.1"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.14"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "rspec", "~> 3.6"
29
+ spec.add_development_dependency 'byebug', "~> 9.1"
30
+ spec.add_development_dependency 'activerecord', "~> 5.1"
31
+ spec.add_development_dependency "sqlite3", "~> 1.3"
32
+ spec.add_development_dependency 'simplecov', "~> 0.15"
33
+ end
@@ -0,0 +1,11 @@
1
+ module InterstateMachine
2
+ module ActiveRecordClass
3
+ module InstanceMethods
4
+ def self.included(base)
5
+ base.after_initialize do |_record|
6
+ @state_machine = StateMachine.new(self)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,51 @@
1
+ module InterstateMachine
2
+ module Base
3
+ module InstanceMethods
4
+
5
+ def states
6
+ state_machine.states
7
+ end
8
+
9
+ private
10
+
11
+ def evaluate_transition(event, transition_to, from)
12
+ if event_with_multiple_state_transition?(event, transition_to, from)
13
+ send("#{event}_#{state}")
14
+ elsif event_with_single_state_transition?(event, transition_to, from)
15
+ ensure_can_transit(event, transition_to, from)
16
+ else
17
+ raise "cannot transition via #{event} from #{state}"
18
+ end
19
+ end
20
+
21
+ def ensure_can_transit(event, transition_to, from, multiple: false)
22
+ @state_machine.evaluate_transition_by!(
23
+ Interactor::Context.new(
24
+ event: event, transition_to: transition_to, from: from,
25
+ multiple: multiple, object: self
26
+ )
27
+ )
28
+ end
29
+
30
+ def event_with_single_state_transition?(event, transition_to, from)
31
+ respond_to?(event) && transition_to && from
32
+ end
33
+
34
+ def event_with_multiple_state_transition?(event, _transition_to, _from)
35
+ respond_to?("#{event}_#{state}")
36
+ end
37
+
38
+ def constantize(event)
39
+ Object.const_get(event.to_s.split('_').collect(&:capitalize).join)
40
+ end
41
+
42
+ def interactor_name(event)
43
+ if state_machine.context.multiple
44
+ "#{event}_#{state_machine.next_state}"
45
+ else
46
+ event
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ module InterstateMachine
2
+ class Environment
3
+
4
+ def self.define(base)
5
+ if active_record?(base)
6
+ base.send(:include, ActiveRecordClass::InstanceMethods)
7
+ else
8
+ base.send(:prepend, PlainRuby::InstanceMethods)
9
+ end
10
+ end
11
+
12
+ def self.active_record?(base)
13
+ base.ancestors.include?(ActiveRecord::Base) rescue false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ module PlainRuby
2
+ module InstanceMethods
3
+ attr_accessor :state_machine
4
+
5
+ def initialize
6
+ super
7
+ @state_machine = InterstateMachine::StateMachine.new(self)
8
+ end
9
+
10
+ def save
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ module InterstateMachine
2
+ class StateMachine
3
+ attr_reader :states, :context
4
+ attr_accessor :state
5
+
6
+ def initialize(object)
7
+ @states = object.class.machine_states
8
+ @state = defined_state(object)
9
+ end
10
+
11
+ def evaluate_transition_by!(rule)
12
+ @context = rule
13
+ raise failed_transition unless context.from.include? context.object.state.to_sym
14
+ end
15
+
16
+ def next_state
17
+ context.transition_to.first
18
+ end
19
+
20
+ def next
21
+ context.object.state = context.transition_to.first
22
+ context.object.save
23
+ end
24
+
25
+ private
26
+
27
+ def failed_transition
28
+ "can not transit to #{context.transition_to} from #{state} via
29
+ #{context.event}"
30
+ end
31
+
32
+ def defined_state(object)
33
+ # TODO quite ugly, can be improved
34
+ if object.state.nil?
35
+ object.state = object.class.initialized_state.to_sym
36
+ object.save if object.respond_to?(:persisted?) && object.persisted?
37
+ object.state
38
+ else
39
+ object.state.to_sym
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module InterstateMachine
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,65 @@
1
+ require 'interstate_machine/version'
2
+ require 'interstate_machine/state_machine'
3
+ require 'interstate_machine/environment'
4
+ require 'interstate_machine/base/instance_methods'
5
+ require 'interstate_machine/plain_ruby/instance_methods'
6
+
7
+ require 'interstate_machine/active_record_class/instance_methods'
8
+ require 'interactor'
9
+
10
+ module InterstateMachine
11
+ def self.included(base)
12
+ base.class_eval do
13
+ Environment.define(base)
14
+ include Base::InstanceMethods
15
+ extend ClassMethods
16
+
17
+ private
18
+ attr_reader :state_machine
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def initial_state(state)
24
+ @initialized_state = state
25
+ end
26
+
27
+ def initialized_state
28
+ @initialized_state ||= []
29
+ end
30
+
31
+ def machine_states
32
+ @machine_states ||= []
33
+ end
34
+
35
+ def transition_table(*states)
36
+ @machine_states = states
37
+ yield
38
+ end
39
+
40
+ def on(event:, transition_to: nil, from: nil)
41
+ yield(event) if block_given?
42
+ perform_transition_by(
43
+ event: event,
44
+ transition_to: transition_to,
45
+ from: from
46
+ )
47
+ end
48
+
49
+ def allow(event: nil, transition_to: nil, from: nil)
50
+ define_method "#{event}_#{from.first}" do
51
+ ensure_can_transit(event, transition_to, from, multiple: true)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def perform_transition_by(event: nil, transition_to: nil, from: nil)
58
+ define_method event do
59
+ evaluate_transition(event, transition_to, from)
60
+ action = constantize(interactor_name(event)).call(object: self)
61
+ action.success? ? @state_machine.next : action.error
62
+ end
63
+ end
64
+ end
65
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interstate_machine
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - SamuelMartini
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-01-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: interactor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '9.1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '9.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '5.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.15'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.15'
125
+ description: InterstateMachine is a simple state machine which use interactors to
126
+ trigger transitions
127
+ email:
128
+ - samueljmartini@gmail.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".ruby-version"
136
+ - ".travis.yml"
137
+ - Gemfile
138
+ - LICENSE.txt
139
+ - README.md
140
+ - Rakefile
141
+ - interstate_machine.gemspec
142
+ - lib/interstate_machine.rb
143
+ - lib/interstate_machine/active_record_class/instance_methods.rb
144
+ - lib/interstate_machine/base/instance_methods.rb
145
+ - lib/interstate_machine/environment.rb
146
+ - lib/interstate_machine/plain_ruby/instance_methods.rb
147
+ - lib/interstate_machine/state_machine.rb
148
+ - lib/interstate_machine/version.rb
149
+ homepage: https://github.com/SamuelMartini/interstate_machine
150
+ licenses:
151
+ - MIT
152
+ metadata: {}
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubyforge_project:
169
+ rubygems_version: 2.6.13
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: a state machine with interactor implementation
173
+ test_files: []