circulator 2.1.8 → 2.1.9
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 +4 -4
- data/CHANGELOG.md +6 -2
- data/README.md +1 -1
- data/lib/circulator/flow.rb +30 -14
- data/lib/circulator/version.rb +1 -1
- data/lib/circulator.rb +60 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d069df555d7f1ac9aa1865cacef2c3bda5373a287c899f23068abe56cf3c379
|
|
4
|
+
data.tar.gz: c79dbba353dbe721587f3361599385a280a93f9c9655b4fcae01de5359ec8f1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca22e7ae65647826445f17464a74427524bda9fa4d3fa8cb34a1ff47bc8f6c036c5f35a624de6c6853334677add37decd5f27fcbe19bfcb5e4c9edd54606d8f9
|
|
7
|
+
data.tar.gz: d7c0c520cda68eee6e470a66fa8be95c3f332a3b36bb1314964a2c55f51a763667a2de745c0ea65a319e83eaf5b475cf1303a5961e67877fdbadb4154596ed37
|
data/CHANGELOG.md
CHANGED
|
@@ -5,8 +5,12 @@ 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.
|
|
8
|
+
## [2.1.9] - 2026-01-08
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Flow#merge to merge existing flows with extensions (d1eb47d)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Circulator.extension applies immediately to existing flows (d1eb47d)
|
data/README.md
CHANGED
|
@@ -338,7 +338,7 @@ doc.status_revise # => :draft (from extension)
|
|
|
338
338
|
|
|
339
339
|
**How Extensions Work:**
|
|
340
340
|
|
|
341
|
-
Extensions are registered globally using `Circulator.extension(class_name, attribute)` and are automatically applied when the class defines its flow. Multiple extensions can be registered for the same class/attribute and are applied in registration order. Extensions
|
|
341
|
+
Extensions are registered globally using `Circulator.extension(class_name, attribute)` and are automatically applied when the class defines its flow. Multiple extensions can be registered for the same class/attribute and are applied in registration order. Extensions can be registered before or after the class definition—if registered after, they are applied immediately to the existing flow.
|
|
342
342
|
|
|
343
343
|
By default, when an extension defines the same action from the same state as the base flow, the extension completely replaces the base definition (last-defined wins). To implement intelligent composition where extensions add their conditions/blocks additively, your application can configure a custom `flows_proc` that uses a Hash-like object with merge logic. Circulator remains dependency-free and supports any compatible Hash implementation.
|
|
344
344
|
|
data/lib/circulator/flow.rb
CHANGED
|
@@ -88,6 +88,34 @@ module Circulator
|
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Merge an extension block into this flow
|
|
92
|
+
#
|
|
93
|
+
# Creates an extension flow from the block and merges its transitions
|
|
94
|
+
# and states into this flow. Returns self for convenience.
|
|
95
|
+
#
|
|
96
|
+
# Example:
|
|
97
|
+
#
|
|
98
|
+
# existing_flow.merge do
|
|
99
|
+
# state :pending do
|
|
100
|
+
# action :send_to_legal, to: :legal_review
|
|
101
|
+
# end
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
def merge(&block)
|
|
105
|
+
extension_flow = Flow.new(@klass, @attribute_name, @states, extension: true, flows_proc: @flows_proc, &block)
|
|
106
|
+
|
|
107
|
+
# Merge transition map
|
|
108
|
+
extension_flow.transition_map.each do |action, transitions|
|
|
109
|
+
@transition_map[action] = if @transition_map[action]
|
|
110
|
+
@transition_map[action].merge(transitions)
|
|
111
|
+
else
|
|
112
|
+
transitions
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
91
119
|
private
|
|
92
120
|
|
|
93
121
|
def validate_allow_if(allow_if)
|
|
@@ -172,21 +200,9 @@ module Circulator
|
|
|
172
200
|
key = "#{class_name}:#{@attribute_name}"
|
|
173
201
|
extensions = Circulator.extensions[key]
|
|
174
202
|
|
|
175
|
-
# Apply each extension
|
|
203
|
+
# Apply each extension using merge
|
|
176
204
|
extensions.each do |extension_block|
|
|
177
|
-
|
|
178
|
-
extension_flow.transition_map.each do |action, transitions|
|
|
179
|
-
@transition_map[action] = if @transition_map[action]
|
|
180
|
-
@transition_map[action].merge(transitions)
|
|
181
|
-
else
|
|
182
|
-
transitions
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Merge states from extension
|
|
187
|
-
extension_flow.instance_variable_get(:@states).each do |state|
|
|
188
|
-
@states.add(state)
|
|
189
|
-
end
|
|
205
|
+
merge(&extension_block)
|
|
190
206
|
end
|
|
191
207
|
end
|
|
192
208
|
end
|
data/lib/circulator/version.rb
CHANGED
data/lib/circulator.rb
CHANGED
|
@@ -70,8 +70,66 @@ module Circulator
|
|
|
70
70
|
|
|
71
71
|
key = "#{class_name}:#{attribute_name}"
|
|
72
72
|
@extensions[key] << block
|
|
73
|
+
|
|
74
|
+
# If the class already exists and has flows defined, apply the extension immediately
|
|
75
|
+
apply_extension_to_existing_flow(class_name, attribute_name, block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def apply_extension_to_existing_flow(class_name, attribute_name, block)
|
|
81
|
+
# Try to get the class constant
|
|
82
|
+
klass = begin
|
|
83
|
+
Object.const_get(class_name.to_s)
|
|
84
|
+
rescue NameError
|
|
85
|
+
return # Class doesn't exist yet, extension will be applied when flow is defined
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if the class has flows and the specific attribute flow
|
|
89
|
+
return unless klass.respond_to?(:flows) && klass.flows
|
|
90
|
+
|
|
91
|
+
model_key = Circulator.model_key(klass.to_s)
|
|
92
|
+
existing_flow = klass.flows.dig(model_key, attribute_name.to_sym)
|
|
93
|
+
return unless existing_flow
|
|
94
|
+
|
|
95
|
+
# Merge the extension into the existing flow
|
|
96
|
+
existing_flow.merge(&block)
|
|
97
|
+
|
|
98
|
+
# Re-define flow methods for any new actions/states
|
|
99
|
+
redefine_flow_methods(klass, attribute_name, existing_flow)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def redefine_flow_methods(klass, attribute_name, flow)
|
|
103
|
+
flow_module = klass.ancestors.find { |ancestor|
|
|
104
|
+
ancestor.name.to_s =~ /#{FLOW_MODULE_NAME}/o
|
|
105
|
+
}
|
|
106
|
+
return unless flow_module
|
|
107
|
+
|
|
108
|
+
object = nil # Extensions only work on the same class model
|
|
109
|
+
|
|
110
|
+
# Define or redefine methods for actions (need to redefine if transitions changed)
|
|
111
|
+
flow.transition_map.each do |action, transitions|
|
|
112
|
+
method_name = [object, attribute_name, action].compact.join("_")
|
|
113
|
+
|
|
114
|
+
# Remove existing method so it can be redefined with updated transitions
|
|
115
|
+
if flow_module.method_defined?(method_name)
|
|
116
|
+
flow_module.remove_method(method_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
klass.send(:define_flow_method, attribute_name: attribute_name, action: action, transitions: transitions, object: object, owner: flow_module)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Define predicate methods for any new states
|
|
123
|
+
states = flow.instance_variable_get(:@states)
|
|
124
|
+
states.each do |state|
|
|
125
|
+
next if state.nil?
|
|
126
|
+
klass.send(:define_state_method, attribute_name: attribute_name, state: state, object: object, owner: flow_module)
|
|
127
|
+
end
|
|
73
128
|
end
|
|
74
129
|
end
|
|
130
|
+
|
|
131
|
+
FLOW_MODULE_NAME = "FlowMethods"
|
|
132
|
+
|
|
75
133
|
# Declare a flow for an attribute.
|
|
76
134
|
#
|
|
77
135
|
# Specify the attribute to be used for states and actions.
|
|
@@ -203,11 +261,11 @@ module Circulator
|
|
|
203
261
|
@flows[model_key][attribute_name] = Flow.new(self, attribute_name, flows_proc:, &block)
|
|
204
262
|
|
|
205
263
|
flow_module = ancestors.find { |ancestor|
|
|
206
|
-
ancestor.name.to_s =~ /
|
|
264
|
+
ancestor.name.to_s =~ /#{FLOW_MODULE_NAME}/o
|
|
207
265
|
} || Module.new.tap do |mod|
|
|
208
266
|
include mod
|
|
209
267
|
|
|
210
|
-
const_set(
|
|
268
|
+
const_set(FLOW_MODULE_NAME.to_sym, mod)
|
|
211
269
|
end
|
|
212
270
|
|
|
213
271
|
object = if model == to_s
|