philiprehberger-state_machine 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +148 -0
- data/lib/philiprehberger/state_machine/callbacks.rb +61 -0
- data/lib/philiprehberger/state_machine/definition.rb +58 -0
- data/lib/philiprehberger/state_machine/instance_methods.rb +124 -0
- data/lib/philiprehberger/state_machine/transition.rb +49 -0
- data/lib/philiprehberger/state_machine/version.rb +7 -0
- data/lib/philiprehberger/state_machine.rb +38 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b03b538804454b46d59bbc7a123e9ba9574cb1f2ad222a7ac8242dfd79d69db5
|
|
4
|
+
data.tar.gz: 2fa1caa0ebf454e23bd8f4282ddbd4f496de8f6111fb4c56e3e147dcd6cdc79f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: db65dd12012f2db78a47946f2977b0122e417413233e4c458aae79f2de7d500f2a7b90f4b3804deb9dc33a74b7c85b1db32ed8ce4244a1fe7a8d0d0023ddfea5
|
|
7
|
+
data.tar.gz: ffb15acff6cf3a832c4a02ff5445b2c92ce8b0c665294fb6a652d349fad324baec3a65dded1daf45049173c304a1c8d1427d348d25f60088d6730b224a912d60
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- State machine DSL with `state_machine initial: :state` block syntax
|
|
15
|
+
- Event definitions with `event :name` and `transition from:, to:` DSL
|
|
16
|
+
- Guard conditions via `guard:` lambda on transitions
|
|
17
|
+
- Before and after transition callbacks with `to:` and `from:` filters
|
|
18
|
+
- State predicate methods (`state?`)
|
|
19
|
+
- Transition check methods (`can_event?`)
|
|
20
|
+
- Safe and bang event methods (`event` returns boolean, `event!` raises)
|
|
21
|
+
- `allowed_transitions` introspection method
|
|
22
|
+
- Works with any Ruby class, no framework dependency required
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# philiprehberger-state_machine
|
|
2
|
+
|
|
3
|
+
[](https://github.com/philiprehberger/rb-state-machine/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-state_machine)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Lightweight state machine DSL with transitions, guards, and callbacks
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby >= 3.1
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "philiprehberger-state_machine"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install philiprehberger-state_machine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "philiprehberger/state_machine"
|
|
31
|
+
|
|
32
|
+
class Order
|
|
33
|
+
include Philiprehberger::StateMachine
|
|
34
|
+
|
|
35
|
+
state_machine initial: :pending do
|
|
36
|
+
event :pay do
|
|
37
|
+
transition from: :pending, to: :paid
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
event :ship do
|
|
41
|
+
transition from: :paid, to: :shipped
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
event :deliver do
|
|
45
|
+
transition from: :shipped, to: :delivered
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
order = Order.new
|
|
51
|
+
order.current_state # => :pending
|
|
52
|
+
order.pay!
|
|
53
|
+
order.current_state # => :paid
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Guards
|
|
57
|
+
|
|
58
|
+
Attach a guard lambda to conditionally block transitions:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class Order
|
|
62
|
+
include Philiprehberger::StateMachine
|
|
63
|
+
|
|
64
|
+
attr_accessor :tracking_number
|
|
65
|
+
|
|
66
|
+
state_machine initial: :pending do
|
|
67
|
+
event :pay do
|
|
68
|
+
transition from: :pending, to: :paid
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
event :ship do
|
|
72
|
+
transition from: :paid, to: :shipped, guard: -> { !tracking_number.nil? }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
order = Order.new
|
|
78
|
+
order.pay!
|
|
79
|
+
order.ship # => false (no tracking number)
|
|
80
|
+
order.tracking_number = "TRACK123"
|
|
81
|
+
order.ship! # => transitions to :shipped
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Callbacks
|
|
85
|
+
|
|
86
|
+
Register before and after callbacks with optional state filters:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class Order
|
|
90
|
+
include Philiprehberger::StateMachine
|
|
91
|
+
|
|
92
|
+
state_machine initial: :pending do
|
|
93
|
+
event :pay do
|
|
94
|
+
transition from: :pending, to: :paid
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
before_transition to: :paid do |order|
|
|
98
|
+
puts "About to mark order as paid"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
after_transition to: :paid do |order|
|
|
102
|
+
puts "Order is now paid"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Introspection
|
|
109
|
+
|
|
110
|
+
Query the state machine at runtime:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
order = Order.new
|
|
114
|
+
|
|
115
|
+
order.pending? # => true
|
|
116
|
+
order.paid? # => false
|
|
117
|
+
order.can_pay? # => true
|
|
118
|
+
order.can_ship? # => false
|
|
119
|
+
order.allowed_transitions # => [:pay]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
| Method | Description |
|
|
125
|
+
|--------|-------------|
|
|
126
|
+
| `state_machine(initial:, &block)` | Define a state machine on the class with an initial state |
|
|
127
|
+
| `event(name, &block)` | Define an event inside the state machine block |
|
|
128
|
+
| `transition(from:, to:, guard: nil)` | Define a transition inside an event block |
|
|
129
|
+
| `before_transition(to: nil, from: nil, &block)` | Register a callback that fires before a transition |
|
|
130
|
+
| `after_transition(to: nil, from: nil, &block)` | Register a callback that fires after a transition |
|
|
131
|
+
| `#current_state` | Returns the current state as a symbol |
|
|
132
|
+
| `#can_X?` | Returns true if event X can fire from the current state (including guards) |
|
|
133
|
+
| `#allowed_transitions` | Returns an array of event names that can fire from the current state |
|
|
134
|
+
| `#X!` | Fire event X or raise `InvalidTransition` |
|
|
135
|
+
| `#X` | Fire event X, returns true on success, false on failure |
|
|
136
|
+
| `#X?` | Returns true if current state is X |
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bundle install
|
|
142
|
+
bundle exec rspec
|
|
143
|
+
bundle exec rubocop
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module StateMachine
|
|
5
|
+
# Represents a single callback with optional state filters.
|
|
6
|
+
#
|
|
7
|
+
# @attr_reader type [:before, :after] callback timing
|
|
8
|
+
# @attr_reader block [Proc] the callback to execute
|
|
9
|
+
# @attr_reader conditions [Hash] optional :from and :to filters
|
|
10
|
+
Callback = Struct.new(:type, :block, :conditions, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
# Stores and filters callbacks for state transitions.
|
|
13
|
+
class CallbackSet
|
|
14
|
+
def initialize
|
|
15
|
+
@callbacks = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Register a callback.
|
|
19
|
+
#
|
|
20
|
+
# @param type [:before, :after]
|
|
21
|
+
# @param conditions [Hash] optional :from and :to state filters
|
|
22
|
+
# @param block [Proc]
|
|
23
|
+
def add(type:, conditions: {}, &block)
|
|
24
|
+
@callbacks << Callback.new(type: type, block: block, conditions: conditions)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Execute all matching callbacks for the given transition.
|
|
28
|
+
#
|
|
29
|
+
# @param type [:before, :after]
|
|
30
|
+
# @param from [Symbol] source state
|
|
31
|
+
# @param to [Symbol] target state
|
|
32
|
+
# @param context [Object] the object to pass to the callback
|
|
33
|
+
def execute(type:, from:, to:, context:)
|
|
34
|
+
matching(type: type, from: from, to: to).each do |callback|
|
|
35
|
+
callback.block.call(context)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def matching(type:, from:, to:)
|
|
42
|
+
@callbacks.select do |cb|
|
|
43
|
+
next false unless cb.type == type
|
|
44
|
+
|
|
45
|
+
matches_condition?(cb.conditions[:from], from) &&
|
|
46
|
+
matches_condition?(cb.conditions[:to], to)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def matches_condition?(condition, state)
|
|
51
|
+
return true if condition.nil?
|
|
52
|
+
|
|
53
|
+
if condition.is_a?(Array)
|
|
54
|
+
condition.include?(state)
|
|
55
|
+
else
|
|
56
|
+
condition == state
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module StateMachine
|
|
5
|
+
# DSL class used inside the `state_machine` block to define events,
|
|
6
|
+
# transitions, and callbacks.
|
|
7
|
+
class Definition
|
|
8
|
+
attr_reader :initial, :events, :callback_set
|
|
9
|
+
|
|
10
|
+
# @param initial [Symbol] the initial state
|
|
11
|
+
def initialize(initial:)
|
|
12
|
+
@initial = initial
|
|
13
|
+
@events = {}
|
|
14
|
+
@callback_set = CallbackSet.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Define an event with transitions.
|
|
18
|
+
#
|
|
19
|
+
# @param name [Symbol] event name
|
|
20
|
+
# @yield block evaluated via TransitionBuilder
|
|
21
|
+
def event(name, &block)
|
|
22
|
+
builder = TransitionBuilder.new
|
|
23
|
+
builder.instance_eval(&block)
|
|
24
|
+
@events[name] = builder.transitions
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Register a before_transition callback.
|
|
28
|
+
#
|
|
29
|
+
# @param opts [Hash] optional :from and :to state filters
|
|
30
|
+
# @yield [Object] block receives the host object
|
|
31
|
+
def before_transition(opts = {}, &block)
|
|
32
|
+
@callback_set.add(type: :before, conditions: opts, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Register an after_transition callback.
|
|
36
|
+
#
|
|
37
|
+
# @param opts [Hash] optional :from and :to state filters
|
|
38
|
+
# @yield [Object] block receives the host object
|
|
39
|
+
def after_transition(opts = {}, &block)
|
|
40
|
+
@callback_set.add(type: :after, conditions: opts, &block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns all unique states referenced in the definition.
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<Symbol>]
|
|
46
|
+
def all_states
|
|
47
|
+
states = [initial]
|
|
48
|
+
events.each_value do |transitions|
|
|
49
|
+
transitions.each do |t|
|
|
50
|
+
states.concat(Array(t.from))
|
|
51
|
+
states << t.to
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
states.uniq
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module StateMachine
|
|
5
|
+
# Defines and mixes in instance methods on the host class.
|
|
6
|
+
module InstanceMethods
|
|
7
|
+
class << self
|
|
8
|
+
# Define all state machine methods on the host class.
|
|
9
|
+
#
|
|
10
|
+
# @param klass [Class] the host class
|
|
11
|
+
# @param definition [Definition] the state machine definition
|
|
12
|
+
def define_methods(klass, definition)
|
|
13
|
+
define_initializer(klass, definition)
|
|
14
|
+
define_state_accessors(klass)
|
|
15
|
+
define_event_methods(klass, definition)
|
|
16
|
+
define_state_predicates(klass, definition)
|
|
17
|
+
define_introspection(klass, definition)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def define_initializer(klass, definition)
|
|
23
|
+
initial = definition.initial
|
|
24
|
+
initializer = Module.new do
|
|
25
|
+
define_method(:initialize) do |*args, **kwargs, &block|
|
|
26
|
+
@_sm_state = initial
|
|
27
|
+
return unless method(:initialize).super_method
|
|
28
|
+
|
|
29
|
+
if kwargs.empty?
|
|
30
|
+
super(*args, &block)
|
|
31
|
+
else
|
|
32
|
+
super(*args, **kwargs, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
klass.prepend(initializer)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def define_state_accessors(klass)
|
|
40
|
+
klass.define_method(:current_state) { @_sm_state }
|
|
41
|
+
klass.send(:define_method, :_sm_set_state) { |state| @_sm_state = state }
|
|
42
|
+
klass.send(:private, :_sm_set_state)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def define_event_methods(klass, definition)
|
|
46
|
+
definition.events.each do |event_name, transitions|
|
|
47
|
+
define_bang_method(klass, event_name, transitions, definition)
|
|
48
|
+
define_safe_method(klass, event_name, transitions, definition)
|
|
49
|
+
define_can_method(klass, event_name, transitions)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def define_bang_method(klass, event_name, transitions, definition)
|
|
54
|
+
klass.define_method(:"#{event_name}!") do
|
|
55
|
+
transition = transitions.find { |t| t.matches?(current_state) }
|
|
56
|
+
unless transition
|
|
57
|
+
raise InvalidTransition,
|
|
58
|
+
"Cannot #{event_name} from #{current_state}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if transition.guard && !instance_exec(&transition.guard)
|
|
62
|
+
raise InvalidTransition,
|
|
63
|
+
"Guard condition failed for #{event_name} from #{current_state}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
from = current_state
|
|
67
|
+
to = transition.to
|
|
68
|
+
|
|
69
|
+
definition.callback_set.execute(type: :before, from: from, to: to, context: self)
|
|
70
|
+
_sm_set_state(to)
|
|
71
|
+
definition.callback_set.execute(type: :after, from: from, to: to, context: self)
|
|
72
|
+
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def define_safe_method(klass, event_name, transitions, definition)
|
|
78
|
+
klass.define_method(event_name) do
|
|
79
|
+
transition = transitions.find { |t| t.matches?(current_state) }
|
|
80
|
+
return false unless transition
|
|
81
|
+
return false if transition.guard && !instance_exec(&transition.guard)
|
|
82
|
+
|
|
83
|
+
from = current_state
|
|
84
|
+
to = transition.to
|
|
85
|
+
|
|
86
|
+
definition.callback_set.execute(type: :before, from: from, to: to, context: self)
|
|
87
|
+
_sm_set_state(to)
|
|
88
|
+
definition.callback_set.execute(type: :after, from: from, to: to, context: self)
|
|
89
|
+
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def define_can_method(klass, event_name, transitions)
|
|
95
|
+
klass.define_method(:"can_#{event_name}?") do
|
|
96
|
+
transition = transitions.find { |t| t.matches?(current_state) }
|
|
97
|
+
return false unless transition
|
|
98
|
+
return false if transition.guard && !instance_exec(&transition.guard)
|
|
99
|
+
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def define_state_predicates(klass, definition)
|
|
105
|
+
definition.all_states.each do |state|
|
|
106
|
+
klass.define_method(:"#{state}?") { current_state == state }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def define_introspection(klass, definition)
|
|
111
|
+
klass.define_method(:allowed_transitions) do
|
|
112
|
+
definition.events.select do |_name, transitions|
|
|
113
|
+
transition = transitions.find { |t| t.matches?(current_state) }
|
|
114
|
+
next false unless transition
|
|
115
|
+
next false if transition.guard && !instance_exec(&transition.guard)
|
|
116
|
+
|
|
117
|
+
true
|
|
118
|
+
end.keys
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module StateMachine
|
|
5
|
+
# Represents a single transition from one or more states to a target state.
|
|
6
|
+
#
|
|
7
|
+
# @attr_reader from [Symbol, Array<Symbol>] source state(s)
|
|
8
|
+
# @attr_reader to [Symbol] target state
|
|
9
|
+
# @attr_reader guard [Proc, nil] optional guard condition
|
|
10
|
+
Transition = Struct.new(:from, :to, :guard, keyword_init: true) do
|
|
11
|
+
# Check if this transition matches the given current state.
|
|
12
|
+
#
|
|
13
|
+
# @param current_state [Symbol]
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def matches?(current_state)
|
|
16
|
+
if from.is_a?(Array)
|
|
17
|
+
from.include?(current_state)
|
|
18
|
+
else
|
|
19
|
+
from == current_state
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns all source states as an array.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Symbol>]
|
|
26
|
+
def from_states
|
|
27
|
+
Array(from)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Collects transitions for a single event via the DSL.
|
|
32
|
+
class TransitionBuilder
|
|
33
|
+
attr_reader :transitions
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@transitions = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Define a transition within an event block.
|
|
40
|
+
#
|
|
41
|
+
# @param from [Symbol, Array<Symbol>] source state(s)
|
|
42
|
+
# @param to [Symbol] target state
|
|
43
|
+
# @param guard [Proc, nil] optional guard lambda
|
|
44
|
+
def transition(from:, to:, guard: nil)
|
|
45
|
+
@transitions << Transition.new(from: from, to: to, guard: guard)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'state_machine/version'
|
|
4
|
+
require_relative 'state_machine/transition'
|
|
5
|
+
require_relative 'state_machine/callbacks'
|
|
6
|
+
require_relative 'state_machine/definition'
|
|
7
|
+
require_relative 'state_machine/instance_methods'
|
|
8
|
+
|
|
9
|
+
module Philiprehberger
|
|
10
|
+
module StateMachine
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class InvalidTransition < Error; end
|
|
13
|
+
|
|
14
|
+
def self.included(base)
|
|
15
|
+
base.extend(ClassMethods)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
# Define a state machine on the class.
|
|
20
|
+
#
|
|
21
|
+
# @param initial [Symbol] the initial state
|
|
22
|
+
# @yield block evaluated via Definition DSL
|
|
23
|
+
def state_machine(initial:, &block)
|
|
24
|
+
definition = Definition.new(initial: initial)
|
|
25
|
+
definition.instance_eval(&block)
|
|
26
|
+
|
|
27
|
+
@_sm_definition = definition
|
|
28
|
+
|
|
29
|
+
InstanceMethods.define_methods(self, definition)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Definition] the state machine definition
|
|
33
|
+
def _sm_definition
|
|
34
|
+
@_sm_definition
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-state_machine
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A minimal state machine for Ruby objects. Define states, events, transitions,
|
|
14
|
+
guard conditions, and callbacks with a clean DSL. Works with any Ruby class — no
|
|
15
|
+
framework dependency required.
|
|
16
|
+
email:
|
|
17
|
+
- me@philiprehberger.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/philiprehberger/state_machine.rb
|
|
26
|
+
- lib/philiprehberger/state_machine/callbacks.rb
|
|
27
|
+
- lib/philiprehberger/state_machine/definition.rb
|
|
28
|
+
- lib/philiprehberger/state_machine/instance_methods.rb
|
|
29
|
+
- lib/philiprehberger/state_machine/transition.rb
|
|
30
|
+
- lib/philiprehberger/state_machine/version.rb
|
|
31
|
+
homepage: https://github.com/philiprehberger/rb-state-machine
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://github.com/philiprehberger/rb-state-machine
|
|
36
|
+
source_code_uri: https://github.com/philiprehberger/rb-state-machine
|
|
37
|
+
changelog_uri: https://github.com/philiprehberger/rb-state-machine/blob/main/CHANGELOG.md
|
|
38
|
+
bug_tracker_uri: https://github.com/philiprehberger/rb-state-machine/issues
|
|
39
|
+
rubygems_mfa_required: 'true'
|
|
40
|
+
post_install_message:
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 3.1.0
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.5.22
|
|
56
|
+
signing_key:
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Lightweight state machine DSL with transitions, guards, and callbacks
|
|
59
|
+
test_files: []
|