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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad89c887b2b9cb0b3fc2a12bb0a8da92480828f4d8bb594700233f4b1c0ee9fd
4
- data.tar.gz: 4400d66545332b1bd4bc09028611efe7ce04a06076520e1f8a860f3089bb8d49
3
+ metadata.gz: 7d069df555d7f1ac9aa1865cacef2c3bda5373a287c899f23068abe56cf3c379
4
+ data.tar.gz: c79dbba353dbe721587f3361599385a280a93f9c9655b4fcae01de5359ec8f1e
5
5
  SHA512:
6
- metadata.gz: 31d7f74c6167dd0c0e51180d8cb585a5da27a9cab543bc32b0ec2da1c819ccd0cff3920446b94496baa36cf2b4005610f535054ce863a08f2ac691ca6ee70ff2
7
- data.tar.gz: 4fcf8a370b45aa9d883c5ce4b10f4b714b66c5d5f4f5cdf638281ee01f5636687505c1be94297e5abb072f1de5db8b082edd500cd0b9ccaa769a08a280de743e
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] - 2026-01-07
8
+ ## [2.1.9] - 2026-01-08
9
9
 
10
10
  ### Added
11
11
 
12
- - Detailed conditional information display in DOT and PlantUML diagrams (dce5b69)
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 must be registered before the class definition (typically in initializers).
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
 
@@ -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 by creating a new Flow and merging its transition_map
203
+ # Apply each extension using merge
176
204
  extensions.each do |extension_block|
177
- extension_flow = Flow.new(@klass, @attribute_name, @states, extension: true, flows_proc: @flows_proc, &extension_block)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Circulator
4
- VERSION = "2.1.8"
4
+ VERSION = "2.1.9"
5
5
  end
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 =~ /FlowMethods/
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(:FlowMethods, mod)
268
+ const_set(FLOW_MODULE_NAME.to_sym, mod)
211
269
  end
212
270
 
213
271
  object = if model == to_s
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.8
4
+ version: 2.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay