statum 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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