circulator 2.1.10 → 2.1.11

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
  SHA256:
3
- metadata.gz: 8c4d63bee9efd4c305309b4e77b502e378ad2a8ec7f550b85a25d9f9b1963f9f
4
- data.tar.gz: 281ebeb9de99be8dac7b6925ae7d3d51727c54916bca3f9df7c86b6aa5014966
3
+ metadata.gz: 9232620710fbfe075e26c0ae3bcf1dea0794cdca188b0a9a0ff8529362573ede
4
+ data.tar.gz: c45d7ac1bdf89fe21107905811d36d0ef4dca37370ea89686d5433cef1aeaaf5
5
5
  SHA512:
6
- metadata.gz: b9d5d21fb96c45580fd3051b084403aba315d058eee4edb56dfdb4c7b10cc49105b277e60d96c895d4ae0014e0a0298ff79878f4f3a1b61f35b8c480543f491a
7
- data.tar.gz: fd20a62b795f3817877c48b8ca810dde022615df11cd100506265436958e4e7326791d8a2dda6b13f898327e5c295f076800d541bc900986524b000ebdc812d9
6
+ metadata.gz: bfbc391566c29b7dd18005c56c37c21c9d976eeebc2705f92ea726074b93807daf47528f34e07bb449be3a38bca797067742abc64e1d862162cc18924049b47d
7
+ data.tar.gz: 9edd59a4985b266ad4c590d3e4d05bbe41f381e85a149b84de08adeabf9e1ff141d0a2b5d5dee759e1e1ba8d4dd740c8e33036331694614951994fddab6d4448
data/CHANGELOG.md CHANGED
@@ -5,8 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [2.1.10] - 2026-02-06
8
+ ## [2.1.11] - 2026-02-09
9
9
 
10
10
  ### Added
11
11
 
12
- - `around` block in flow DSL to wrap transition logic (state read, guards, state change, callbacks) (184527b)
12
+ - action_missing alias for no_action to clarify intent (c576a7c)
13
+
14
+ ### Changed
15
+
16
+ - Improved inline documentation for no_action and around methods (c576a7c)
17
+ - Added method documentation for Flow DSL methods (2546989)
18
+
19
+ ### Removed
20
+
21
+ - @no_action internal instance_variable and switched to @action_missing Version: minor (c576a7c)
data/README.md CHANGED
@@ -294,6 +294,39 @@ class Payment
294
294
  end
295
295
  ```
296
296
 
297
+ #### Handling Missing Transitions with `action_missing`
298
+
299
+ By default, if you call an action on an object whose current state doesn't define a transition for that action, Circulator raises an error. Use `action_missing` (aliased as `no_action`) to customize this behavior.
300
+
301
+ This is triggered when, for example, you call `order.status_ship` but the object is in `:pending` and `:ship` only has a transition defined from `:processing`.
302
+
303
+ ```ruby
304
+ class Order
305
+ extend Circulator
306
+
307
+ attr_accessor :status
308
+
309
+ flow :status do
310
+ action_missing do |attribute_name, action|
311
+ Rails.logger.warn "No #{action} transition from #{send(attribute_name)} for #{attribute_name}"
312
+ end
313
+
314
+ state :pending do
315
+ action :process, to: :processing
316
+ end
317
+
318
+ state :processing do
319
+ action :ship, to: :shipped
320
+ end
321
+ end
322
+ end
323
+
324
+ order = Order.new
325
+ order.status = :pending
326
+
327
+ order.status_ship # => logs warning instead of raising
328
+ ```
329
+
297
330
  #### Wrapping Transitions with `around`
298
331
 
299
332
  Use the `around` block to wrap all transitions in a flow with shared logic. The block receives a `transition` proc that you must call for the transition to execute:
@@ -6,7 +6,7 @@ module Circulator
6
6
  @klass = klass
7
7
  @attribute_name = attribute_name
8
8
  @states = states
9
- @no_action = ->(attribute_name, action) { raise "No action found for the current state of #{attribute_name} (#{send(attribute_name)}): #{action}" }
9
+ @action_missing = ->(attribute_name, action) { raise "No action found for the current state of #{attribute_name} (#{send(attribute_name)}): #{action}" }
10
10
  @flows_proc = flows_proc
11
11
  @transition_map = flows_proc.call
12
12
 
@@ -18,6 +18,18 @@ module Circulator
18
18
  end
19
19
  attr_reader :transition_map
20
20
 
21
+ # Declares a state in the flow. The block is evaluated in the context
22
+ # of the flow, allowing you to define actions that transition from
23
+ # this state. A state with no block acts as a terminal state.
24
+ #
25
+ # flow(:status) do
26
+ # state :pending do
27
+ # action :approve, to: :approved
28
+ # end
29
+ #
30
+ # state :approved # terminal state
31
+ # end
32
+ #
21
33
  def state(name, &block)
22
34
  name = name.to_sym if name.respond_to?(:to_sym)
23
35
  @states.add(name)
@@ -26,6 +38,34 @@ module Circulator
26
38
  remove_instance_variable(:@current_state)
27
39
  end
28
40
 
41
+ # Declares a transition from one or more states to a destination state.
42
+ # Must be called inside a +state+ block, or with an explicit +from:+ option.
43
+ #
44
+ # Generates a method named +<attribute>_<action>+ on the class that
45
+ # performs the transition when called.
46
+ #
47
+ # ==== Options
48
+ #
49
+ # +to+:: The destination state (Symbol) or a callable that returns
50
+ # the destination state at runtime.
51
+ # +from+:: One or more source states. Defaults to the enclosing
52
+ # +state+ block. Pass an Array to define the same action
53
+ # from multiple states.
54
+ # +allow_if+:: A guard condition that must be truthy for the transition
55
+ # to proceed. Accepts a Proc, Symbol (method name), Hash
56
+ # (checks another flow's state), or Array of those.
57
+ # +&block+:: A block executed on the instance during the transition,
58
+ # before the state is changed.
59
+ #
60
+ # state :pending do
61
+ # action :approve, to: :approved, allow_if: :reviewer? do
62
+ # self.approved_at = Time.now
63
+ # end
64
+ # end
65
+ #
66
+ # # Or with an explicit from:
67
+ # action :cancel, from: [:pending, :processing], to: :cancelled
68
+ #
29
69
  def action(name, to:, from: :__not_specified__, allow_if: nil, &block)
30
70
  raise "You must be in a state block or have a `from` option to declare an action" unless defined?(@current_state) || from != :__not_specified__
31
71
 
@@ -61,6 +101,22 @@ module Circulator
61
101
  end
62
102
  end
63
103
 
104
+ # Attaches an +allow_if+ guard to an existing action after it has been
105
+ # defined. This is useful when the guard logic needs to be defined
106
+ # separately from the action itself, such as in an extension.
107
+ #
108
+ # Must be called inside a +state+ block, or with an explicit +from:+ option.
109
+ # The action must already exist in the transition map.
110
+ #
111
+ # Example:
112
+ #
113
+ # flow(:status) do
114
+ # state :pending do
115
+ # action :approve, to: :approved
116
+ # action_allowed(:approve) { current_user.admin? }
117
+ # end
118
+ # end
119
+ #
64
120
  def action_allowed(name, from: :__not_specified__, &block)
65
121
  raise "You must be in a state block or have a `from` option to declare an action" unless defined?(@current_state) || from != :__not_specified__
66
122
 
@@ -80,14 +136,53 @@ module Circulator
80
136
  end
81
137
  end
82
138
 
83
- def no_action(&block)
139
+ # Called when an action is invoked but no transition is defined for the
140
+ # current state. For example, if an object is in :approved and you call
141
+ # an action that only has a transition from :pending, this block runs.
142
+ #
143
+ # By default, raises an error. Override to silently ignore, log, or
144
+ # handle the missing transition however you like.
145
+ #
146
+ # The block receives two arguments: the attribute name and the action name.
147
+ #
148
+ # Example:
149
+ #
150
+ # flow(:status) do
151
+ # action_missing do |attribute_name, action|
152
+ # Rails.logger.warn("No transition for #{attribute_name} in #{action}")
153
+ # end
154
+ # end
155
+ def action_missing(&block)
84
156
  if block_given?
85
- @no_action = block
157
+ @action_missing = block
86
158
  else
87
- @no_action
159
+ @action_missing
88
160
  end
89
161
  end
162
+ alias_method :no_action, :action_missing
90
163
 
164
+ # Wraps every transition in this flow. The block receives a lambda that
165
+ # executes the transition logic (guard check, state change, and any
166
+ # transition block). You must call +transition.call+ for the transition
167
+ # to actually happen — omitting it prevents the state change entirely.
168
+ #
169
+ # Useful for wrapping transitions in database transactions, logging,
170
+ # instrumentation, or any before/after behavior.
171
+ #
172
+ # Example:
173
+ #
174
+ # flow(:status) do
175
+ # around do |transition|
176
+ # ActiveRecord::Base.transaction do
177
+ # transition.call
178
+ # end
179
+ # end
180
+ #
181
+ # state :pending do
182
+ # action :approve, to: :approved
183
+ # end
184
+ # end
185
+ #
91
186
  def around(&block)
92
187
  if block_given?
93
188
  @around = block
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Circulator
4
- VERSION = "2.1.10"
4
+ VERSION = "2.1.11"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: circulator
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.10
4
+ version: 2.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -46,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  requirements: []
49
- rubygems_version: 4.0.3
49
+ rubygems_version: 3.6.9
50
50
  specification_version: 4
51
51
  summary: Simple state machine
52
52
  test_files: []