circulator 2.1.11 → 2.1.12
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 +8 -8
- data/lib/circulator/version.rb +1 -1
- data/lib/circulator.rb +70 -8
- 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: d7dbf52b54ebf9536c2d5cac336376e6fa1fadbdd614794bd7322349c714bbc0
|
|
4
|
+
data.tar.gz: c81382ec182f2d2b2b4f13d73c8228675efed7f5f5ee80886a58d8b77820db76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c996bdd7a378930fbf14cdfabad9b83b099db386932ea6fab946c38bcd5d649dce25bbc2730b87747b5f46937b55727a06964e8930154b4da23745cf27cdb757
|
|
7
|
+
data.tar.gz: 24cd2abfe4e710269d4a3a39ed9606bb158b395fa66fa83c5892e9cc060850131c9d9704693a214762eed2efe97ec39a64e4b97378f2e6bda98a1f52ef5adcdf
|
data/CHANGELOG.md
CHANGED
|
@@ -5,17 +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.
|
|
8
|
+
## [2.1.12] - 2026-02-17
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Named test sampler classes for reuse across test files (b6bcb4e)
|
|
13
|
+
- bang method variants for flow actions (673d8a0)
|
|
14
|
+
- Circulator::InvalidTransition exception class (673d8a0)
|
|
15
|
+
- host-namespaced InvalidTransition subclass per class with flows (673d8a0)
|
|
16
|
+
- flow! instance method and variant: keyword on flow (673d8a0)
|
|
17
|
+
- FLOW_VARIANTS registry that drives method definition (673d8a0)
|
|
13
18
|
|
|
14
19
|
### Changed
|
|
15
20
|
|
|
16
|
-
-
|
|
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)
|
|
21
|
+
- Flow attribute writers are private after flow definition Version: minor (35ea18f)
|
data/lib/circulator/version.rb
CHANGED
data/lib/circulator.rb
CHANGED
|
@@ -2,6 +2,8 @@ require "circulator/version"
|
|
|
2
2
|
require "circulator/flow"
|
|
3
3
|
|
|
4
4
|
module Circulator
|
|
5
|
+
class InvalidTransition < StandardError; end
|
|
6
|
+
|
|
5
7
|
# Global registry for extensions
|
|
6
8
|
@extensions = Hash.new { |h, k| h[k] = [] }
|
|
7
9
|
|
|
@@ -109,14 +111,17 @@ module Circulator
|
|
|
109
111
|
|
|
110
112
|
# Define or redefine methods for actions (need to redefine if transitions changed)
|
|
111
113
|
flow.transition_map.each do |action, transitions|
|
|
112
|
-
|
|
114
|
+
base_name = [object, attribute_name, action].compact.join("_")
|
|
113
115
|
|
|
114
|
-
# Remove existing
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
# Remove existing methods so they can be redefined with updated transitions
|
|
117
|
+
FLOW_VARIANTS.each_value do |suffix|
|
|
118
|
+
full_name = "#{base_name}#{suffix}"
|
|
119
|
+
flow_module.remove_method(full_name) if flow_module.method_defined?(full_name)
|
|
117
120
|
end
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
FLOW_VARIANTS.each_key do |variant|
|
|
123
|
+
klass.send(["define", variant, "flow_method"].compact.join("_"), attribute_name:, action:, transitions:, object:, owner: flow_module)
|
|
124
|
+
end
|
|
120
125
|
end
|
|
121
126
|
|
|
122
127
|
# Define predicate methods for any new states
|
|
@@ -129,6 +134,7 @@ module Circulator
|
|
|
129
134
|
end
|
|
130
135
|
|
|
131
136
|
FLOW_MODULE_NAME = "FlowMethods"
|
|
137
|
+
FLOW_VARIANTS = {:bang => "!", nil => ""}.freeze
|
|
132
138
|
|
|
133
139
|
# Declare a flow for an attribute.
|
|
134
140
|
#
|
|
@@ -275,6 +281,11 @@ module Circulator
|
|
|
275
281
|
# Pass the flows_proc to Flow so it can create transition_maps of the same type
|
|
276
282
|
@flows[model_key][attribute_name] = Flow.new(self, attribute_name, flows_proc:, &block)
|
|
277
283
|
|
|
284
|
+
# Define InvalidTransition exception on the host class if not already defined
|
|
285
|
+
unless const_defined?(:InvalidTransition, false)
|
|
286
|
+
const_set(:InvalidTransition, Class.new(Circulator::InvalidTransition))
|
|
287
|
+
end
|
|
288
|
+
|
|
278
289
|
flow_module = ancestors.find { |ancestor|
|
|
279
290
|
ancestor.name.to_s =~ /#{FLOW_MODULE_NAME}/o
|
|
280
291
|
} || Module.new.tap do |mod|
|
|
@@ -298,7 +309,9 @@ module Circulator
|
|
|
298
309
|
states.add(transition_data[:to])
|
|
299
310
|
end
|
|
300
311
|
end
|
|
301
|
-
|
|
312
|
+
FLOW_VARIANTS.each_key do |variant|
|
|
313
|
+
send(["define", variant, "flow_method"].compact.join("_"), attribute_name:, action:, transitions:, object:, owner: flow_module)
|
|
314
|
+
end
|
|
302
315
|
end
|
|
303
316
|
|
|
304
317
|
# Define predicate methods for each state (skip nil)
|
|
@@ -306,6 +319,15 @@ module Circulator
|
|
|
306
319
|
next if state.nil?
|
|
307
320
|
define_state_method(attribute_name:, state:, object:, owner: flow_module)
|
|
308
321
|
end
|
|
322
|
+
|
|
323
|
+
# Make the attribute writer private so that state changes must go through
|
|
324
|
+
# the generated flow methods. This prevents bypassing guards and transition
|
|
325
|
+
# logic by directly assigning the attribute. The writer is only made private
|
|
326
|
+
# for self-managed flows (not model-based flows where the attribute belongs
|
|
327
|
+
# to a different class).
|
|
328
|
+
if object.nil? && method_defined?("#{attribute_name}=")
|
|
329
|
+
private "#{attribute_name}="
|
|
330
|
+
end
|
|
309
331
|
end
|
|
310
332
|
alias_method :circulator, :flow
|
|
311
333
|
|
|
@@ -363,6 +385,30 @@ module Circulator
|
|
|
363
385
|
end
|
|
364
386
|
end
|
|
365
387
|
|
|
388
|
+
def define_bang_flow_method(attribute_name:, action:, transitions:, object:, owner:)
|
|
389
|
+
bang_method = [object, attribute_name, action].compact.join("_") << "!"
|
|
390
|
+
return if owner.method_defined?(bang_method)
|
|
391
|
+
|
|
392
|
+
non_bang_method = [object, attribute_name, action].compact.join("_")
|
|
393
|
+
owner.define_method(bang_method) do |*args, flow_target: self, **kwargs, &block|
|
|
394
|
+
current_value = flow_target.send(attribute_name)
|
|
395
|
+
current_state = current_value.respond_to?(:to_sym) ? current_value.to_sym : current_value
|
|
396
|
+
|
|
397
|
+
transition = transitions[current_state]
|
|
398
|
+
unless transition
|
|
399
|
+
raise self.class.const_get(:InvalidTransition),
|
|
400
|
+
"No transition #{action} on #{attribute_name} from #{current_state.inspect}"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
if transition[:allow_if] && !Circulator.evaluate_guard(flow_target, transition[:allow_if], *args, **kwargs)
|
|
404
|
+
raise self.class.const_get(:InvalidTransition),
|
|
405
|
+
"Guard prevented #{action} on #{attribute_name} from #{current_state.inspect}"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
send(non_bang_method, *args, flow_target:, **kwargs, &block)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
366
412
|
module_function def evaluate_guard(target, allow_if, *args, **kwargs)
|
|
367
413
|
case allow_if
|
|
368
414
|
when Array
|
|
@@ -409,16 +455,21 @@ module Circulator
|
|
|
409
455
|
module InstanceMethods
|
|
410
456
|
# Use this method to call an action on the attribute.
|
|
411
457
|
#
|
|
458
|
+
# Accepts an optional +variant:+ keyword to select a method variant.
|
|
459
|
+
# See +FLOW_VARIANTS+ for the available variants and their suffixes.
|
|
460
|
+
#
|
|
412
461
|
# Example:
|
|
413
462
|
#
|
|
414
463
|
# test_object.flow(:approve, :status)
|
|
415
464
|
# test_object.flow(:approve, :status, "arg1", "arg2", key: "value")
|
|
416
|
-
|
|
465
|
+
# test_object.flow(:approve, :status, variant: :bang)
|
|
466
|
+
def flow(action, attribute, *args, flow_target: self, variant: nil, **kwargs, &block)
|
|
417
467
|
target_name = if flow_target != self
|
|
418
468
|
Circulator.methodize_name(Circulator.model_key(flow_target))
|
|
419
469
|
end
|
|
420
470
|
external_attribute_name = [target_name, attribute].compact.join("_")
|
|
421
|
-
|
|
471
|
+
suffix = Circulator::FLOW_VARIANTS.fetch(variant)
|
|
472
|
+
method_name = "#{external_attribute_name}_#{action}#{suffix}"
|
|
422
473
|
if respond_to?(method_name)
|
|
423
474
|
send(method_name, *args, flow_target:, **kwargs, &block)
|
|
424
475
|
elsif flow_target.respond_to?(method_name)
|
|
@@ -426,6 +477,17 @@ module Circulator
|
|
|
426
477
|
else
|
|
427
478
|
raise "Invalid action for the current state of #{attribute} (#{flow_target.send(attribute).inspect}): #{action}"
|
|
428
479
|
end
|
|
480
|
+
rescue KeyError
|
|
481
|
+
raise ArgumentError, "Invalid variant: #{variant.inspect}"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Bang variant of flow that raises InvalidTransition on failure.
|
|
485
|
+
#
|
|
486
|
+
# Example:
|
|
487
|
+
#
|
|
488
|
+
# test_object.flow!(:approve, :status)
|
|
489
|
+
def flow!(action, attribute, *args, **kwargs, &block)
|
|
490
|
+
flow(action, attribute, *args, variant: :bang, **kwargs, &block)
|
|
429
491
|
end
|
|
430
492
|
|
|
431
493
|
# Get available actions for an attribute based on current state
|