circulator 2.1.3 → 2.1.4

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: bdd2e4e17046d72b53d25ea469b8f037316abf74cb7a34fe010091ff4f8b5bfa
4
- data.tar.gz: 879a93ab33bb08c07fb6239ce490c5d900272cbd3cd03a65c66516f95439499e
3
+ metadata.gz: 9126192a7ce18956bc8c27842e52765dac25e9436e322ad259100e5138a5b97f
4
+ data.tar.gz: f1e15524d5806a3f0b0455bd006e8ba5e3914b13e046ab0d2acd1dbcb897ea27
5
5
  SHA512:
6
- metadata.gz: 910cd9755d9b9c02996d9d38329082733d019019151c19bc839663893e3523a6c64be9149485317bc7a0979f10223d15c4e0bb34ae0a9942f30d1c473acf8378
7
- data.tar.gz: a98cad092d4da49cf93c38db6462489730061c38439f9825e2d2378ef58def38d3db243784c570f50697704442d271d8ac3aa0b1eddd5147e7f4a65b2a346c8a
6
+ metadata.gz: 03ad68c5d43c45fddf1b7d5c3f84dd845363b2bf8cc1a881463efbd1b1e43d233e196e965dae157726b2ac0b90066d9ec05087f12339667ffd1e9effe0213e54
7
+ data.tar.gz: 7590c58b27a608f4d49f6041c9fed8a4fd5a05526cd5c36eba40a12346ce5a63cd6f9f0a6253c48ec6aa7405e9e4b5ed8121fae276404a1a32f4587118453c2f
data/CHANGELOG.md CHANGED
@@ -5,12 +5,24 @@ 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.3] - 2025-10-27
8
+ ## [2.1.4] - 2025-11-03
9
9
 
10
- ### Fixed
10
+ ### Added
11
11
 
12
- - Ignore nil states in predicate methods (73c74c2)
12
+ - Symbol-based allow_if support (a96b794)
13
+ - Documentation for symbol-based allow_if (d3befc1)
14
+ - available_flows method with guard and argument support (122e2ab)
15
+ - available_flow? predicate method (122e2ab)
16
+ - Documentation for query methods (a75507e)
13
17
 
14
- ### Added
18
+ ### Changed
19
+
20
+ - Validate symbol allow_if at definition time (392eac5)
21
+
22
+ ### Removed
23
+
24
+ - Redundant respond_to? check (40ff669)
25
+
26
+ ### Fixed
15
27
 
16
- - Attribute predicate methods and documentation (8f9cc33)
28
+ - DOT syntax error for states ending with ? (65b503e)
data/README.md CHANGED
@@ -103,10 +103,36 @@ order.status_pending? # => false
103
103
 
104
104
  These predicate methods work with both symbol and string values, automatically converting strings to symbols for comparison.
105
105
 
106
+ #### Query Available Actions
107
+
108
+ Circulator provides methods to query which actions are available from the current state:
109
+
110
+ ```ruby
111
+ order.status = :pending
112
+
113
+ # Get all available actions
114
+ order.available_flows(:status) # => [:approve, :reject]
115
+
116
+ # Check if a specific action is available
117
+ order.available_flow?(:status, :approve) # => true
118
+ order.available_flow?(:status, :ship) # => false
119
+ ```
120
+
121
+ These methods respect all `allow_if` conditions and can accept arguments to pass through to guard conditions:
122
+
123
+ ```ruby
124
+ # With conditional guards
125
+ order.available_flow?(:status, :approve, level: 5) # => true
126
+ ```
127
+
106
128
  ### Advanced Features
107
129
 
108
130
  #### Conditional Transitions with Guards
109
131
 
132
+ You can control when transitions are allowed using the `allow_if` option. Circulator supports three types of guards:
133
+
134
+ **Proc-based guards** evaluate a block of code:
135
+
110
136
  ```ruby
111
137
  class Document
112
138
  extend Circulator
@@ -125,7 +151,29 @@ class Document
125
151
  end
126
152
  ```
127
153
 
128
- #### Nested State Dependencies
154
+ **Symbol-based guards** call a method on the object:
155
+
156
+ ```ruby
157
+ class Document
158
+ extend Circulator
159
+
160
+ attr_accessor :state, :reviewed_by
161
+
162
+ circulator :state do
163
+ state :draft do
164
+ action :publish, to: :published, allow_if: :ready_to_publish?
165
+ end
166
+ end
167
+
168
+ def ready_to_publish?
169
+ reviewed_by.present?
170
+ end
171
+ end
172
+ ```
173
+
174
+ This is equivalent to the proc-based approach but cleaner when you have a dedicated method for the condition.
175
+
176
+ **Hash-based guards** check the state of another attribute:
129
177
 
130
178
  You can make one state machine depend on another using hash-based `allow_if`:
131
179
 
@@ -6,6 +6,14 @@ module Circulator
6
6
  class Dot < Diagram
7
7
  private
8
8
 
9
+ def quote_node_name(name)
10
+ if name.end_with?("?")
11
+ "\"#{name}\""
12
+ else
13
+ name
14
+ end
15
+ end
16
+
9
17
  def flows_output(flows_data, output)
10
18
  if flows_data.size == 1
11
19
  # Single flow: no grouping needed
@@ -27,7 +35,8 @@ module Circulator
27
35
  state_label = state.nil? ? "nil" : state.to_s
28
36
  # Prefix state names with attribute to avoid conflicts
29
37
  prefixed_name = "#{flow[:attribute_name]}_#{state_label}"
30
- output << " #{prefixed_name} [label=\"#{state_label}\", shape=circle];"
38
+ quoted_name = quote_node_name(prefixed_name)
39
+ output << " #{quoted_name} [label=\"#{state_label}\", shape=circle];"
31
40
  end
32
41
 
33
42
  output << " }"
@@ -43,7 +52,9 @@ module Circulator
43
52
  # Use prefixed names
44
53
  prefixed_from = "#{flow[:attribute_name]}_#{from_label}"
45
54
  prefixed_to = "#{flow[:attribute_name]}_#{to_label}"
46
- output << " #{prefixed_from} -> #{prefixed_to} [label=\"#{transition[:label]}\"];"
55
+ quoted_from = quote_node_name(prefixed_from)
56
+ quoted_to = quote_node_name(prefixed_to)
57
+ output << " #{quoted_from} -> #{quoted_to} [label=\"#{transition[:label]}\"];"
47
58
  end
48
59
  end
49
60
  end
@@ -67,7 +78,8 @@ module Circulator
67
78
  output << " // States"
68
79
  states.sort_by { |s| s.to_s }.each do |state|
69
80
  state_label = state.nil? ? "nil" : state.to_s
70
- output << " #{state_label} [shape=circle];"
81
+ quoted_name = quote_node_name(state_label)
82
+ output << " #{quoted_name} [shape=circle];"
71
83
  end
72
84
  end
73
85
 
@@ -77,7 +89,9 @@ module Circulator
77
89
  transitions.sort_by { |t| [t[:from].to_s, t[:to].to_s, t[:label]] }.each do |transition|
78
90
  from_label = transition[:from].nil? ? "nil" : transition[:from].to_s
79
91
  to_label = transition[:to].nil? ? "nil" : transition[:to].to_s
80
- output << " #{from_label} -> #{to_label} [label=\"#{transition[:label]}\"];"
92
+ quoted_from = quote_node_name(from_label)
93
+ quoted_to = quote_node_name(to_label)
94
+ output << " #{quoted_from} -> #{quoted_to} [label=\"#{transition[:label]}\"];"
81
95
  end
82
96
  end
83
97
 
@@ -83,14 +83,21 @@ module Circulator
83
83
  private
84
84
 
85
85
  def validate_allow_if(allow_if)
86
- # Must be either a Proc or a Hash
87
- unless allow_if.is_a?(Proc) || allow_if.is_a?(Hash)
88
- raise ArgumentError, "allow_if must be a Proc or Hash, got: #{allow_if.class}"
86
+ case allow_if
87
+ in Proc
88
+ # Valid, no additional validation needed
89
+ in Symbol
90
+ validate_symbol_allow_if(allow_if)
91
+ in Hash
92
+ validate_hash_allow_if(allow_if)
93
+ else
94
+ raise ArgumentError, "allow_if must be a Proc, Hash, or Symbol, got: #{allow_if.class}"
89
95
  end
96
+ end
90
97
 
91
- # If it's a Hash, validate the structure
92
- if allow_if.is_a?(Hash)
93
- validate_hash_allow_if(allow_if)
98
+ def validate_symbol_allow_if(method_name)
99
+ unless @klass.method_defined?(method_name)
100
+ raise ArgumentError, "allow_if references undefined method :#{method_name} on #{@klass}"
94
101
  end
95
102
  end
96
103
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Circulator
4
- VERSION = "2.1.3"
4
+ VERSION = "2.1.4"
5
5
  end
data/lib/circulator.rb CHANGED
@@ -209,6 +209,9 @@ module Circulator
209
209
 
210
210
  # Return early if current state is not in the valid states
211
211
  return unless valid_states_array.include?(current_state)
212
+ elsif transition[:allow_if].is_a?(Symbol)
213
+ # Handle symbol-based allow_if (method name)
214
+ return unless flow_target.send(transition[:allow_if])
212
215
  else
213
216
  # Handle proc-based allow_if (original behavior)
214
217
  return unless flow_target.instance_exec(*args, **kwargs, &transition[:allow_if])
@@ -274,10 +277,62 @@ module Circulator
274
277
  end
275
278
  end
276
279
 
280
+ # Get available actions for an attribute based on current state
281
+ #
282
+ # Example:
283
+ #
284
+ # test_object.available_flows(:status)
285
+ # # => [:approve, :reject]
286
+ def available_flows(attribute, *args, **kwargs)
287
+ model_key = Circulator.model_key(self)
288
+ flow = flows.dig(model_key, attribute)
289
+ return [] unless flow
290
+
291
+ current_value = send(attribute)
292
+ current_state = current_value.respond_to?(:to_sym) ? current_value.to_sym : current_value
293
+
294
+ flow.transition_map.select do |action, transitions|
295
+ transition = transitions[current_state]
296
+ next false unless transition
297
+
298
+ # Check allow_if condition if present
299
+ if transition[:allow_if]
300
+ check_allow_if(transition[:allow_if], *args, **kwargs)
301
+ else
302
+ true
303
+ end
304
+ end.keys
305
+ end
306
+
307
+ # Check if a specific action is available for an attribute
308
+ #
309
+ # Example:
310
+ #
311
+ # test_object.available_flow?(:status, :approve)
312
+ # # => true
313
+ def available_flow?(attribute, action, *args, **kwargs)
314
+ available_flows(attribute, *args, **kwargs).include?(action)
315
+ end
316
+
277
317
  private
278
318
 
279
319
  def flows
280
320
  self.class.flows
281
321
  end
322
+
323
+ def check_allow_if(allow_if, *args, **kwargs)
324
+ case allow_if
325
+ when Hash
326
+ attribute_name, valid_states = allow_if.first
327
+ current_state = send(attribute_name)
328
+ current_state = current_state.to_sym if current_state.respond_to?(:to_sym)
329
+ valid_states_array = Array(valid_states).map { |s| s.respond_to?(:to_sym) ? s.to_sym : s }
330
+ valid_states_array.include?(current_state)
331
+ when Symbol
332
+ send(allow_if, *args, **kwargs)
333
+ else # Proc
334
+ instance_exec(*args, **kwargs, &allow_if)
335
+ end
336
+ end
282
337
  end
283
338
  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.3
4
+ version: 2.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay