statum 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 52f0fb1eba87da4ce2d080b1256d3929628c2a05
4
- data.tar.gz: 10871eafd35c6dd767ec219d0f4fcdf6342d7cae
3
+ metadata.gz: 8faf2ca3f2b51f152887aa12e747d8089b3aef9c
4
+ data.tar.gz: b1312cb2bdf56cd71d32780faf14dc0699409327
5
5
  SHA512:
6
- metadata.gz: 4cfe42b84a4b55fad13213eb792f5b96a19cac90f7b2236892db35fa3eb3320a2dc3be897f8e7b5f9b50576d583309383206d123889e4239e729f284a79d8386
7
- data.tar.gz: d5442a1c6fb819574e3bf1ec13258868ea7f5dac6c836bfc902ce8fbbce1ecd64f6e65d59c440fe63f9b916b1b8d8096ce423599ce6c65656159b9ffef0fcb72
6
+ metadata.gz: f469b8e8ecad0b292ca837fdb4289d329b5b9dec87591ea8cfcb32c9f65f3eefb4baacaa423acb5734faec5a4fb04d1aa84da74525183336f7e7b5c2dc18301f
7
+ data.tar.gz: bfe730ae1c8ad58cc939464139480278c06ac84f9b1379a23793cf8eed5ad4e7182053618ccac0f16730edd211b801d5e1536b455a3d68ffd78cf63f57fa88ee
@@ -0,0 +1,2 @@
1
+ ruby:
2
+ config_file: .rubocop.yml
@@ -35,6 +35,9 @@ Metrics/AbcSize:
35
35
  Enabled: false
36
36
 
37
37
  # Style
38
+ Style/MixinUsage:
39
+ Enabled: false
40
+
38
41
  Style/StringLiterals:
39
42
  Enabled: false
40
43
 
@@ -56,3 +59,7 @@ Lint/AmbiguousOperator:
56
59
 
57
60
  Lint/AmbiguousBlockAssociation:
58
61
  Enabled: false
62
+
63
+ # Layout
64
+ Layout/SpaceInLambdaLiteral:
65
+ EnforcedStyle: require_space
@@ -5,4 +5,10 @@ cache: bundler
5
5
  before_install: gem install bundler
6
6
  script: bundle exec rspec
7
7
  rvm:
8
+ - ruby-head
9
+ - 2.4.2
8
10
  - 2.4.0
11
+ - 2.3.5
12
+ - 2.3.1
13
+ - 2.2.5
14
+ - 2.2.0
data/README.md CHANGED
@@ -4,6 +4,16 @@
4
4
  [![Coverage Status](https://coveralls.io/repos/github/nulldef/statum/badge.svg?branch=master)](https://coveralls.io/github/nulldef/statum?branch=master&v=1)
5
5
 
6
6
  Finite state machine for your objects
7
+
8
+ Tested on Ruby 2.2.0 +
9
+
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Basic usage](#basic-usage)
13
+ - [Multiple state machines](#multiple-state-machines)
14
+ - [Hooks](#hooks)
15
+ - [Contributing](#contributing)
16
+ - [License](#license)
7
17
 
8
18
  ## Installation
9
19
 
@@ -49,6 +59,72 @@ car.riding? # => false
49
59
  car.ride! # changes idle state to riding
50
60
  ```
51
61
 
62
+ You can define an array of states which will be able to fire event:
63
+ ```ruby
64
+ class Car
65
+ include Statum
66
+
67
+ attr_accessor :state
68
+
69
+ statum :state, initial: :idle do
70
+ state :idle
71
+ state :parked
72
+ state :riding
73
+
74
+ event :ride, %i[idle parked] => :riding
75
+ end
76
+ end
77
+ ```
78
+
79
+ Also you can use `any_state` helper to say, that event can be fired from any of defined states
80
+ ```ruby
81
+ class Car
82
+ include Statum
83
+
84
+ attr_accessor :state
85
+
86
+ statum :state, initial: :idle do
87
+ state :idle
88
+ state :parked
89
+ state :riding
90
+
91
+ event :ride, any_state => :riding
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Multiple state machines
97
+ You can define more than one state machine on your object.
98
+
99
+ **IMPORTANT** use unique fields to work with two or more states
100
+ ```ruby
101
+ class Car
102
+ include Statum
103
+
104
+ attr_accessor :state, :engine
105
+
106
+ statum :state do
107
+ state :riding
108
+ state :idle
109
+
110
+ event :ride, idle: :riding
111
+ end
112
+
113
+ statum :engine do
114
+ state :stopped
115
+ state :started
116
+
117
+ event :start, stopped: :started
118
+ end
119
+ end
120
+ ```
121
+
122
+ ```ruby
123
+ car = Car.new
124
+ car.start! # changes engine to started
125
+ car.ride! # changes state to riding
126
+ ```
127
+
52
128
  ### Hooks
53
129
  You can be able to execute some procs before and after event will be fired
54
130
 
@@ -76,6 +152,14 @@ end
76
152
  And then before state changes will be executed `before` proc, and after
77
153
  changing - `after` proc (in instance context).
78
154
 
155
+ If you will wait for argument in hook - the instance will be passed.
156
+ ```ruby
157
+ ...
158
+ event :ride, idle: :riding,
159
+ before: -> (instance) { instance.started = true}
160
+ ...
161
+ ```
162
+
79
163
  ## Contributing
80
164
 
81
165
  Bug reports and pull requests are welcome on GitHub at https://github.com/nulldef/statum.
@@ -1,11 +1,17 @@
1
1
  require "statum/version"
2
- require "statum/state_definer"
3
2
  require "statum/machine"
3
+ require "statum/hook"
4
4
  require "statum/event"
5
+ require "statum/state_definer"
5
6
 
6
7
  module Statum
7
8
  UnknownEventError = Class.new(ArgumentError)
8
9
  ErrorTransitionError = Class.new(StandardError)
10
+ ExistingMachineError = Class.new(ArgumentError)
11
+
12
+ STATE_MACHINES_VARIABLE = '@__statum_machines'.freeze
13
+
14
+ ANY_STATE_NAME = :__statum_any_state
9
15
 
10
16
  class << self
11
17
  def included(base)
@@ -18,20 +24,29 @@ module Statum
18
24
  def statum(field, options = {}, &block)
19
25
  definer = Statum::StateDefiner.new(self, field, options)
20
26
  definer.instance_eval(&block) if block_given?
21
- instance_variable_set('@__statum_machine', definer.state_machine)
27
+ add_machine(definer.state_machine)
22
28
  end
23
29
 
24
- def state_machine
25
- instance_variable_get('@__statum_machine')
30
+ def state_machines
31
+ instance_variable_get(STATE_MACHINES_VARIABLE) || []
32
+ end
33
+
34
+ private
35
+
36
+ def add_machine(machine)
37
+ if state_machines.any? { |m| m.name == machine.name }
38
+ raise ExistingMachineError, "State machine for #{machine.name} already exists"
39
+ end
40
+ instance_variable_set(STATE_MACHINES_VARIABLE, state_machines + [machine])
26
41
  end
27
42
  end
28
43
 
29
44
  module InstanceMethods
30
45
  def method_missing(meth, *args)
31
- if meth.to_s.end_with?('?') && state_machine.state?(meth[0...-1])
32
- state_machine.current(self) == meth[0...-1].to_sym
33
- elsif meth.to_s.end_with?('!') && state_machine.event?(meth[0...-1])
34
- state_machine.fire!(self, meth[0...-1])
46
+ if meth.to_s.end_with?('?') && (machine = find_machine_by_state(meth[0...-1]))
47
+ machine.current(self) == meth[0...-1].to_sym
48
+ elsif meth.to_s.end_with?('!') && (machine = find_machine_by_event(meth[0...-1]))
49
+ machine.fire!(self, meth[0...-1])
35
50
  else
36
51
  super
37
52
  end
@@ -39,16 +54,26 @@ module Statum
39
54
 
40
55
  def respond_to_missing?(meth, *args)
41
56
  if meth.to_s.end_with?('?')
42
- state_machine.state?(meth[0...-1])
57
+ !find_machine_by_state(meth[0...-1]).nil?
43
58
  elsif meth.to_s.end_with?('!')
44
- state_machine.event?(meth[0...-1])
59
+ !find_machine_by_event(meth[0...-1]).nil?
45
60
  else
46
61
  super
47
62
  end
48
63
  end
49
64
 
50
- def state_machine
51
- self.class.state_machine
65
+ private
66
+
67
+ def find_machine_by_event(name)
68
+ state_machines.select { |machine| machine.event?(name) }.first
69
+ end
70
+
71
+ def find_machine_by_state(name)
72
+ state_machines.select { |machine| machine.state?(name) }.first
73
+ end
74
+
75
+ def state_machines
76
+ self.class.state_machines
52
77
  end
53
78
  end
54
79
  end
@@ -1,30 +1,38 @@
1
1
  module Statum
2
2
  # Class for storing event info
3
+ #
4
+ # @attr [Statum::Hook] before Before hook object
5
+ # @attr [Statum::Hook] after After hook object
6
+ # @attr [Symbol|Array] from From state name (or names)
7
+ # @attr [Symbol] to To state name
3
8
  class Event
4
9
  attr_reader :from, :to, :before, :after
5
10
 
6
11
  # Creates an event class
7
12
  #
8
- # @param [String|Symbol] from From state name
13
+ # @param [String|Symbol|Array] from From state name
9
14
  # @param [String|Symbol] to To state name
10
15
  # @param [Hash] options Options for event
11
16
  def initialize(from, to, options = {})
12
- @from = from
13
- @to = to
14
- @before = options.fetch(:before, nil)&.to_proc
15
- @after = options.fetch(:after, nil)&.to_proc
17
+ @from = from.is_a?(Array) ? from : from.to_sym
18
+ @to = to.to_sym
19
+ @before = Statum::Hook.new(options.fetch(:before, nil))
20
+ @after = Statum::Hook.new(options.fetch(:after, nil))
16
21
  end
17
22
 
18
- # Check if before hook exists
19
- # @return [boolean]
20
- def before?
21
- !before.nil?
22
- end
23
-
24
- # Checks if after hook present
25
- # @return [boolean]
26
- def after?
27
- !after.nil?
23
+ # Returns true if event can be fired from current state
24
+ #
25
+ # @param [String|Symbol] current_state Current state
26
+ #
27
+ # @return [Boolean]
28
+ def can_fire?(current_state)
29
+ if from.is_a?(Array)
30
+ from.include?(current_state.to_sym)
31
+ elsif from == Statum::ANY_STATE_NAME
32
+ true
33
+ else
34
+ from == current_state.to_sym
35
+ end
28
36
  end
29
37
  end
30
38
  end
@@ -0,0 +1,30 @@
1
+ module Statum
2
+ # Hook wrapper for Statum::Event
3
+ class Hook
4
+ # Creates new Hook instance
5
+ #
6
+ # @param [Symbol|Proc|Lambda] hook Callable object or symbol that represents instance method
7
+ def initialize(hook)
8
+ @hook = hook
9
+ end
10
+
11
+ # Execute hook on instane
12
+ #
13
+ # @param [Object] instance Class instance
14
+ def evaluate(instance)
15
+ return if @hook.nil?
16
+ hook = find_hook(instance)
17
+ if hook.arity.zero?
18
+ instance.instance_exec(&hook)
19
+ else
20
+ instance.instance_exec(instance, &hook)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def find_hook(instance)
27
+ @hook.respond_to?(:call) ? @hook : instance.method(@hook)
28
+ end
29
+ end
30
+ end
@@ -3,6 +3,9 @@ module Statum
3
3
  class Machine
4
4
  attr_reader :events, :states, :field
5
5
 
6
+ # Use state field for name of machine
7
+ alias name field
8
+
6
9
  # Creates machine instance
7
10
  #
8
11
  # @param [Hash] options options hash
@@ -41,13 +44,13 @@ module Statum
41
44
  current_state = current(instance)
42
45
  event = events[name.to_sym]
43
46
 
44
- if event.from != current_state
47
+ unless event.can_fire?(current_state)
45
48
  raise Statum::ErrorTransitionError, "Cannot transition from #{current_state} to #{event.to}"
46
49
  end
47
50
 
48
- instance.instance_eval(&event.before) if event.before?
51
+ event.before.evaluate(instance)
49
52
  instance.send("#{field}=", event.to)
50
- instance.instance_eval(&event.after) if event.after?
53
+ event.after.evaluate(instance)
51
54
  end
52
55
 
53
56
  # Returns current state of instance
@@ -56,7 +59,8 @@ module Statum
56
59
  #
57
60
  # @return [Symbol] Current instance's state
58
61
  def current(instance)
59
- instance.send(field)&.to_sym || @initial
62
+ value = instance.send(field)
63
+ value.nil? ? @initial : value.to_sym
60
64
  end
61
65
  end
62
66
  end
@@ -35,6 +35,13 @@ module Statum
35
35
  @states << name.to_sym unless @states.include?(name.to_sym)
36
36
  end
37
37
 
38
+ # Returns any state identifier
39
+ #
40
+ # @return [Symbol]
41
+ def any_state
42
+ Statum::ANY_STATE_NAME
43
+ end
44
+
38
45
  # Define a new event
39
46
  #
40
47
  # @param [String|Symbol] name Event name
@@ -1,3 +1,3 @@
1
1
  module Statum
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
@@ -13,7 +13,6 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/nulldef/statum"
14
14
  spec.license = "MIT"
15
15
 
16
-
17
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
17
  f.match(%r{^(test|spec|features)/})
19
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Bespalov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-12-02 00:00:00.000000000 Z
11
+ date: 2017-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -88,6 +88,7 @@ extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
90
  - ".gitignore"
91
+ - ".hound.yml"
91
92
  - ".rspec"
92
93
  - ".rubocop.yml"
93
94
  - ".travis.yml"
@@ -99,6 +100,7 @@ files:
99
100
  - bin/setup
100
101
  - lib/statum.rb
101
102
  - lib/statum/event.rb
103
+ - lib/statum/hook.rb
102
104
  - lib/statum/machine.rb
103
105
  - lib/statum/state_definer.rb
104
106
  - lib/statum/version.rb
@@ -123,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
125
  version: '0'
124
126
  requirements: []
125
127
  rubyforge_project:
126
- rubygems_version: 2.6.8
128
+ rubygems_version: 2.5.1
127
129
  signing_key:
128
130
  specification_version: 4
129
131
  summary: Ruby gem to control your states