circulator 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 +7 -0
- data/CHANGELOG.md +12 -0
- data/README.md +227 -0
- data/Rakefile +18 -0
- data/lib/circulator/diverter.rb +242 -0
- data/lib/circulator/flow.rb +73 -0
- data/lib/circulator/version.rb +5 -0
- data/lib/circulator.rb +3 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 34d38ad852dfc0dea3071103c368fa9d77224c7a390b7fc10c6a39fde2e2b507
|
4
|
+
data.tar.gz: c67068423ece22963cdf64391f0424251a8c23caebf08280a6bb977b1a18bd4b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dc4264165cdb3fdf72ec246dcabc02c0bacff9d20f24a167984e6521517550cc131fd0711f289892c8263fea6ac6c07dcc1d39c0ed77165dbf74d2a3d044f331
|
7
|
+
data.tar.gz: d85440b2e40cc2aa2ff1f7848f5b4dc63b94c2217ebdd7918488967489badb439edcb1c5f39c0b762634f1739ce38405ad19e841a7f4b84c2bb8671262418ec3
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [1.0.0] - 2025-09-11
|
9
|
+
|
10
|
+
###
|
11
|
+
|
12
|
+
- Initial release.
|
data/README.md
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# Circulator
|
2
|
+
|
3
|
+
A lightweight and flexible state machine implementation for Ruby that allows you to define and manage state transitions with an simple DSL. Circulator provides a simple yet powerful way to add state machine functionality to your Ruby classes without the complexity of larger frameworks.
|
4
|
+
|
5
|
+
## Key Features
|
6
|
+
|
7
|
+
- **Lightweight**: Minimal dependencies and simple implementation
|
8
|
+
- **Flexible DSL**: Intuitive syntax for defining states and transitions
|
9
|
+
- **Dynamic Method Generation**: Automatically creates helper methods for state transitions
|
10
|
+
- **Conditional Transitions**: Support for guards and conditional logic
|
11
|
+
- **Transition Callbacks**: Execute code before, during, or after transitions
|
12
|
+
- **Multiple State Machines**: Define multiple independent state machines per class
|
13
|
+
- **Framework Agnostic**: Works with plain Ruby objects, no Rails or ActiveRecord required
|
14
|
+
- **100% Test Coverage**: Thoroughly tested with comprehensive test suite
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'circulator'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
gem install circulator
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Basic Example
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class Order
|
42
|
+
extend Circulator::Diverter
|
43
|
+
|
44
|
+
attr_accessor :status
|
45
|
+
|
46
|
+
circulator :status do
|
47
|
+
state :pending do
|
48
|
+
action :process, to: :processing
|
49
|
+
action :cancel, to: :cancelled
|
50
|
+
end
|
51
|
+
|
52
|
+
state :processing do
|
53
|
+
action :ship, to: :shipped
|
54
|
+
action :cancel, to: :cancelled
|
55
|
+
end
|
56
|
+
|
57
|
+
state :shipped do
|
58
|
+
action :deliver, to: :delivered
|
59
|
+
end
|
60
|
+
|
61
|
+
state :delivered
|
62
|
+
state :cancelled
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
order = Order.new
|
67
|
+
order.status = :pending
|
68
|
+
|
69
|
+
order.status_process # => :processing
|
70
|
+
order.status_ship # => :shipped
|
71
|
+
order.status_deliver # => :delivered
|
72
|
+
```
|
73
|
+
|
74
|
+
### Advanced Features
|
75
|
+
|
76
|
+
#### Conditional Transitions with Guards
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class Document
|
80
|
+
extend Circulator::Diverter
|
81
|
+
|
82
|
+
attr_accessor :state, :reviewed_by
|
83
|
+
|
84
|
+
circulator :state do
|
85
|
+
state :draft do
|
86
|
+
action :submit, to: :review
|
87
|
+
|
88
|
+
action :publish, to: :published, allow_if: -> { reviewed_by.present? } do
|
89
|
+
puts "Publishing document reviewed by #{reviewed_by}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
#### Dynamic Destination States
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class Task
|
100
|
+
extend Circulator::Diverter
|
101
|
+
|
102
|
+
attr_accessor :priority, :urgency_level
|
103
|
+
|
104
|
+
circulator :priority do
|
105
|
+
state :normal do
|
106
|
+
# Destination determined at runtime
|
107
|
+
action :escalate, to: -> { urgency_level > 5 ? :critical : :high }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
#### Multiple State Machines
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class Server
|
117
|
+
extend Circulator::Diverter
|
118
|
+
|
119
|
+
attr_accessor :power_state, :network_state
|
120
|
+
|
121
|
+
# First state machine for power management
|
122
|
+
circulator :power_state do
|
123
|
+
state :off do
|
124
|
+
action :boot, to: :booting
|
125
|
+
end
|
126
|
+
state :booting do
|
127
|
+
action :ready, to: :on
|
128
|
+
end
|
129
|
+
state :on do
|
130
|
+
action :shutdown, to: :off
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Second state machine for network status
|
135
|
+
circulator :network_state do
|
136
|
+
state :disconnected do
|
137
|
+
action :connect, to: :connected
|
138
|
+
end
|
139
|
+
state :connected do
|
140
|
+
action :disconnect, to: :disconnected
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
#### Transition Callbacks
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class Payment
|
150
|
+
extend Circulator::Diverter
|
151
|
+
|
152
|
+
attr_accessor :status, :processed_at
|
153
|
+
|
154
|
+
circulator :status do
|
155
|
+
state :pending do
|
156
|
+
action :process, to: :completed do
|
157
|
+
self.processed_at = Time.now
|
158
|
+
send_confirmation_email
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def send_confirmation_email
|
166
|
+
# Send email logic here
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
## Why Circulator?
|
172
|
+
|
173
|
+
Circulator distinguishes itself from other Ruby state machine libraries through its simplicity and flexibility:
|
174
|
+
|
175
|
+
### Advantages Over Other Libraries
|
176
|
+
|
177
|
+
- **Minimal Magic**: Unlike AASM and state_machines, Circulator uses straightforward Ruby metaprogramming without complex DSL magic
|
178
|
+
- **No Dependencies**: Works with plain Ruby objects without requiring Rails, ActiveRecord, or other frameworks
|
179
|
+
- **Lightweight**: Smaller footprint compared to feature-heavy alternatives
|
180
|
+
- **Clear Method Names**: Generated methods follow predictable naming patterns (`status_approve`, `priority_escalate`)
|
181
|
+
- **Flexible Architecture**: Easy to extend and customize for specific needs
|
182
|
+
|
183
|
+
### When to Use Circulator
|
184
|
+
|
185
|
+
Choose Circulator when you need:
|
186
|
+
- A simple, lightweight state machine without framework dependencies
|
187
|
+
- Clear, predictable method naming conventions
|
188
|
+
- Multiple independent state machines on the same object
|
189
|
+
- Easy-to-understand code without DSL complexity
|
190
|
+
- Full control over state transition logic
|
191
|
+
|
192
|
+
## Related Projects
|
193
|
+
|
194
|
+
If Circulator doesn't meet your needs, consider these alternatives:
|
195
|
+
|
196
|
+
- **[AASM](https://github.com/aasm/aasm)** - Full-featured state machine with ActiveRecord integration and extensive callbacks
|
197
|
+
- **[state_machines](https://github.com/state-machines/state_machines)** - Comprehensive state machine library with GraphViz visualization support
|
198
|
+
- **[workflow](https://github.com/geekq/workflow)** - Workflow-focused state machine with emphasis on business processes
|
199
|
+
- **[statesman](https://github.com/gocardless/statesman)** - Database-backed state machines with transition history
|
200
|
+
- **[finite_machine](https://github.com/piotrmurach/finite_machine)** - Minimal finite state machine with a simple DSL
|
201
|
+
|
202
|
+
Each library has its strengths:
|
203
|
+
- Use **AASM** for Rails applications needing ActiveRecord integration
|
204
|
+
- Use **state_machines** for complex state logic with visualization needs
|
205
|
+
- Use **workflow** for business process modeling
|
206
|
+
- Use **statesman** when audit trails and transition history are critical
|
207
|
+
- Use **finite_machine** for thread-safe state machines
|
208
|
+
- Use **Circulator** for lightweight, flexible state management without dependencies
|
209
|
+
|
210
|
+
## Development
|
211
|
+
|
212
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
213
|
+
|
214
|
+
To install this gem onto your local machine, run bundle exec rake install.
|
215
|
+
|
216
|
+
This project is managed with [Reissue](https://github.com/SOFware/reissue).
|
217
|
+
|
218
|
+
To release a new version, make your changes and be sure to update the CHANGELOG.md.
|
219
|
+
|
220
|
+
To release a new version:
|
221
|
+
|
222
|
+
bundle exec rake build:checksum
|
223
|
+
bundle exec rake release
|
224
|
+
|
225
|
+
## Contributing
|
226
|
+
|
227
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/SOFware/circulator.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "minitest/test_task"
|
5
|
+
|
6
|
+
Minitest::TestTask.create
|
7
|
+
|
8
|
+
require "standard/rake"
|
9
|
+
|
10
|
+
task default: %i[test standard]
|
11
|
+
|
12
|
+
require "reissue/gem"
|
13
|
+
|
14
|
+
Reissue::Task.create :reissue do |task|
|
15
|
+
task.version_file = "lib/circulator/version.rb"
|
16
|
+
task.changelog_file = "CHANGELOG.md"
|
17
|
+
task.version_limit = 1
|
18
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Circulator
|
4
|
+
module Diverter
|
5
|
+
# Declare a flow for an attribute.
|
6
|
+
#
|
7
|
+
# Specify the attribute to be used for states and actions.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# flow(:status) do
|
12
|
+
# state :pending do
|
13
|
+
# action :approve, to: :approved
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# The above declares a flow for the `status` attribute. When in the `pending`
|
18
|
+
# state, the `approve` action will transition the `status` to `approved`.
|
19
|
+
#
|
20
|
+
# This creates a `status_approve` method which will change the state in memory.
|
21
|
+
#
|
22
|
+
# You will also have a instance method `flow` which will allow you to specify
|
23
|
+
# the action to take on the attribute.
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
#
|
27
|
+
# test_object.status_approve
|
28
|
+
# # OR
|
29
|
+
# test_object.flow(:approve, :status)
|
30
|
+
#
|
31
|
+
# You can also provide a block to receive arguments
|
32
|
+
#
|
33
|
+
# Example:
|
34
|
+
#
|
35
|
+
# flow(:status) do
|
36
|
+
# state :pending do
|
37
|
+
# action :approve, to: :approved do |*args, **kwargs|
|
38
|
+
# @args_received = {args: args, kwargs: kwargs}
|
39
|
+
# end
|
40
|
+
# action_allowed(:approve) { true } # Optional. Check some value on self
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# The block will be evalutaed on the instance of the class declaring the flow.
|
45
|
+
# So `self` inside that action block will be the instance of the class.
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
#
|
49
|
+
# test_object.status_approve("arg1", "arg2", key: "value")
|
50
|
+
# # @args_received will be {args: ["arg1", "arg2"], kwargs: {key: "value"}}
|
51
|
+
#
|
52
|
+
# If the action is not allowed, the transition will not be executed.
|
53
|
+
#
|
54
|
+
# Example:
|
55
|
+
#
|
56
|
+
# flow(:status) do
|
57
|
+
# state :pending do
|
58
|
+
# action :approve, to: :approved do
|
59
|
+
# @args_received = {args: args, kwargs: kwargs}
|
60
|
+
# end
|
61
|
+
# action_allowed(:approve) { false }
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# test_object.status_approve
|
66
|
+
# # Will not transition because the action is not allowed
|
67
|
+
#
|
68
|
+
# You may also specify the `allow_if` option to check a condition before
|
69
|
+
# the action is allowed. The callable will be evaluated on the instance of
|
70
|
+
# the class declaring the flow. So `self` inside that block will be the
|
71
|
+
# instance of the class.
|
72
|
+
#
|
73
|
+
# Example:
|
74
|
+
#
|
75
|
+
# flow(:status) do
|
76
|
+
# state :pending do
|
77
|
+
# action :approve, to: :approved, allow_if: -> { true }
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# test_object.status_approve
|
82
|
+
# # Will transition to :approved if the condition is true
|
83
|
+
#
|
84
|
+
# If you declare states separately, for example in an enum, you can use the
|
85
|
+
# `action` method to declare the action on the attribute.
|
86
|
+
#
|
87
|
+
# Example:
|
88
|
+
#
|
89
|
+
# enum :status, {pending: 0, approved: 1, rejected: 2}
|
90
|
+
# flow(:status) do
|
91
|
+
# action :approve, to: :approved, from: :pending
|
92
|
+
# action :reject, to: :rejected, from: :approved do |rejected_at|
|
93
|
+
# self.rejected_at = rejected_at
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# test_object.status_approve
|
98
|
+
# # Will transition to :approved
|
99
|
+
# test_object.status_reject
|
100
|
+
# # Will transition to :rejected and set the rejected_at attribute
|
101
|
+
#
|
102
|
+
# By default, if there is no transition for the current state, the flow will
|
103
|
+
# raise an error. You can specify a no_action block to handle this case.
|
104
|
+
#
|
105
|
+
# Example:
|
106
|
+
#
|
107
|
+
# flow(:status) do
|
108
|
+
# no_action { |attribute_name, action| raise "Nope!" }
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# test_object.status_approve
|
112
|
+
# # Will raise an error
|
113
|
+
#
|
114
|
+
# You can also provide a custom action for other behavior for a set of states and
|
115
|
+
# use the `to` option as a callable to set the attribute.
|
116
|
+
#
|
117
|
+
# Example:
|
118
|
+
#
|
119
|
+
# flow(:status) do
|
120
|
+
# action :unknown, to: -> { status }, from: [:enforcing, :monitoring, :ignoring] do |signal|
|
121
|
+
# raise UnhandledSignalError, signal
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# test_object.flow(:unknown, :status, "signal")
|
126
|
+
# # Will raise an UnhandledSignalError
|
127
|
+
#
|
128
|
+
def flow(attribute_name, model: to_s, &block)
|
129
|
+
@flows ||= {}
|
130
|
+
model_key = Circulator::Diverter.model_key(model)
|
131
|
+
@flows[model_key] ||= {}
|
132
|
+
@flows[model_key][attribute_name] = Circulator::Flow.new(model, attribute_name, &block)
|
133
|
+
|
134
|
+
flow_module = ancestors.find { |ancestor|
|
135
|
+
ancestor.name.to_s =~ /FlowMethods/
|
136
|
+
} || Module.new.tap do |mod|
|
137
|
+
include mod
|
138
|
+
|
139
|
+
const_set(:FlowMethods, mod)
|
140
|
+
end
|
141
|
+
|
142
|
+
object = if model == to_s
|
143
|
+
nil
|
144
|
+
else
|
145
|
+
Circulator::Diverter.methodize_name(model)
|
146
|
+
end
|
147
|
+
|
148
|
+
@flows.dig(model_key, attribute_name).transition_map.each do |action, transitions|
|
149
|
+
define_flow_method(attribute_name:, action:, transitions:, object:, owner: flow_module)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
alias_method :circulator, :flow
|
153
|
+
|
154
|
+
def define_flow_method(attribute_name:, action:, transitions:, object:, owner:)
|
155
|
+
object_attribute_method = [object, attribute_name, action].compact.join("_")
|
156
|
+
raise ArgumentError, "Method already defined: #{object_attribute_method}" if owner.method_defined?(object_attribute_method)
|
157
|
+
|
158
|
+
owner.define_method(object_attribute_method) do |*args, flow_target: self, **kwargs, &block|
|
159
|
+
current_value = flow_target.send(attribute_name)
|
160
|
+
|
161
|
+
transition = if current_value.respond_to?(:to_sym)
|
162
|
+
transitions[current_value.to_sym]
|
163
|
+
else
|
164
|
+
transitions[current_value]
|
165
|
+
end
|
166
|
+
|
167
|
+
unless transition
|
168
|
+
flow_target.instance_exec(attribute_name, action, &flows.dig(Circulator::Diverter.model_key(flow_target), attribute_name).no_action)
|
169
|
+
return
|
170
|
+
end
|
171
|
+
|
172
|
+
if transition[:allow_if]
|
173
|
+
return unless flow_target.instance_exec(*args, **kwargs, &transition[:allow_if])
|
174
|
+
end
|
175
|
+
|
176
|
+
if transition[:block]
|
177
|
+
flow_target.instance_exec(*args, **kwargs, &transition[:block])
|
178
|
+
end
|
179
|
+
|
180
|
+
if transition[:to].respond_to?(:call)
|
181
|
+
flow_target.send("#{attribute_name}=", flow_target.instance_exec(*args, **kwargs, &transition[:to]))
|
182
|
+
else
|
183
|
+
flow_target.send("#{attribute_name}=", transition[:to])
|
184
|
+
end.tap do
|
185
|
+
if block
|
186
|
+
flow_target.instance_exec(*args, **kwargs, &block)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
module_function def model_key(object)
|
193
|
+
if object.is_a?(String)
|
194
|
+
if object.start_with?("#<Class:")
|
195
|
+
"anonymous_#{object.split("0x")[1]}".sub(">", "")
|
196
|
+
else
|
197
|
+
object
|
198
|
+
end
|
199
|
+
else
|
200
|
+
model_key(object.class.name || object.class.to_s)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
module_function def methodize_name(name)
|
205
|
+
name.split("::").map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2') }.join("_").downcase
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.extended(base)
|
209
|
+
base.include(InstanceMethods)
|
210
|
+
base.singleton_class.attr_reader :flows
|
211
|
+
end
|
212
|
+
|
213
|
+
module InstanceMethods
|
214
|
+
# Use this method to call an action on the attribute.
|
215
|
+
#
|
216
|
+
# Example:
|
217
|
+
#
|
218
|
+
# test_object.flow(:approve, :status)
|
219
|
+
# test_object.flow(:approve, :status, "arg1", "arg2", key: "value")
|
220
|
+
def flow(action, attribute, *args, flow_target: self, **kwargs, &block)
|
221
|
+
target_name = if flow_target != self
|
222
|
+
Circulator::Diverter.methodize_name(Circulator::Diverter.model_key(flow_target))
|
223
|
+
end
|
224
|
+
external_attribute_name = [target_name, attribute].compact.join("_")
|
225
|
+
method_name = "#{external_attribute_name}_#{action}"
|
226
|
+
if respond_to?(method_name)
|
227
|
+
send(method_name, *args, flow_target:, **kwargs, &block)
|
228
|
+
elsif flow_target.respond_to?(method_name)
|
229
|
+
flow_target.send(method_name, *args, **kwargs, &block)
|
230
|
+
else
|
231
|
+
raise "Invalid action for the current state of #{attribute} (#{flow_target.send(attribute).inspect}): #{action}"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def flows
|
238
|
+
self.class.flows
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "diverter"
|
4
|
+
|
5
|
+
module Circulator
|
6
|
+
class Flow
|
7
|
+
def initialize(klass, attribute_name, states = Set.new, &block)
|
8
|
+
@klass = klass
|
9
|
+
@attribute_name = attribute_name
|
10
|
+
@states = states
|
11
|
+
@no_action = ->(attribute_name, action) { raise "No action found for the current state of #{attribute_name} (#{send(attribute_name)}): #{action}" }
|
12
|
+
@transition_map = {}
|
13
|
+
instance_eval(&block)
|
14
|
+
end
|
15
|
+
attr_reader :transition_map
|
16
|
+
|
17
|
+
def state(name, &block)
|
18
|
+
name = name.to_sym if name.respond_to?(:to_sym)
|
19
|
+
@states.add(name)
|
20
|
+
@current_state = name
|
21
|
+
instance_eval(&block) if block
|
22
|
+
remove_instance_variable(:@current_state)
|
23
|
+
end
|
24
|
+
|
25
|
+
def action(name, to:, from: :__not_specified__, allow_if: nil, &block)
|
26
|
+
raise "You must be in a state block or have a `from` option to declare an action" unless defined?(@current_state) || from != :__not_specified__
|
27
|
+
|
28
|
+
@transition_map[name] ||= {}
|
29
|
+
selected_state = (from == :__not_specified__) ? @current_state : from
|
30
|
+
|
31
|
+
# Handle nil case specially - convert to [nil] instead of []
|
32
|
+
states_to_process = if selected_state.nil?
|
33
|
+
[nil]
|
34
|
+
else
|
35
|
+
Array(selected_state)
|
36
|
+
end
|
37
|
+
|
38
|
+
states_to_process.each do |from_state|
|
39
|
+
from_state = from_state.to_sym if from_state.respond_to?(:to_sym)
|
40
|
+
@states.add(from_state)
|
41
|
+
@transition_map[name][from_state] = {to:, block:}
|
42
|
+
@transition_map[name][from_state][:allow_if] = allow_if if allow_if
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def action_allowed(name, from: :__not_specified__, &block)
|
47
|
+
raise "You must be in a state block or have a `from` option to declare an action" unless defined?(@current_state) || from != :__not_specified__
|
48
|
+
|
49
|
+
selected_state = (from == :__not_specified__) ? @current_state : from
|
50
|
+
|
51
|
+
# Handle nil case specially - convert to [nil] instead of []
|
52
|
+
states_to_process = if selected_state.nil?
|
53
|
+
[nil]
|
54
|
+
else
|
55
|
+
Array(selected_state)
|
56
|
+
end
|
57
|
+
|
58
|
+
states_to_process.each do |from_state|
|
59
|
+
from_state = from_state.to_sym if from_state.respond_to?(:to_sym)
|
60
|
+
@states.add(from_state)
|
61
|
+
@transition_map[name][from_state][:allow_if] = block
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def no_action(&block)
|
66
|
+
if block_given?
|
67
|
+
@no_action = block
|
68
|
+
else
|
69
|
+
@no_action
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/circulator.rb
ADDED
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: circulator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jim Gay
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: Simple declarative state machine
|
13
|
+
email:
|
14
|
+
- jim@saturnflyer.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- CHANGELOG.md
|
20
|
+
- README.md
|
21
|
+
- Rakefile
|
22
|
+
- lib/circulator.rb
|
23
|
+
- lib/circulator/diverter.rb
|
24
|
+
- lib/circulator/flow.rb
|
25
|
+
- lib/circulator/version.rb
|
26
|
+
homepage: https://github.com/SOFware/circulator
|
27
|
+
licenses: []
|
28
|
+
metadata:
|
29
|
+
homepage_uri: https://github.com/SOFware/circulator
|
30
|
+
source_code_uri: https://github.com/SOFware/circulator
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 3.2.0
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.6.7
|
46
|
+
specification_version: 4
|
47
|
+
summary: Simple state machine
|
48
|
+
test_files: []
|