state_machines 0.20.0 → 0.30.0
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/README.md +124 -13
- data/lib/state_machines/branch.rb +12 -13
- data/lib/state_machines/callback.rb +11 -12
- data/lib/state_machines/core.rb +0 -1
- data/lib/state_machines/error.rb +5 -4
- data/lib/state_machines/eval_helpers.rb +83 -45
- data/lib/state_machines/event.rb +23 -26
- data/lib/state_machines/event_collection.rb +4 -5
- data/lib/state_machines/extensions.rb +5 -5
- data/lib/state_machines/helper_module.rb +1 -1
- data/lib/state_machines/integrations/base.rb +1 -1
- data/lib/state_machines/integrations.rb +11 -14
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +25 -11
- data/lib/state_machines/machine/configuration.rb +124 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +73 -617
- data/lib/state_machines/machine_collection.rb +18 -14
- data/lib/state_machines/macro_methods.rb +2 -2
- data/lib/state_machines/matcher.rb +6 -6
- data/lib/state_machines/matcher_helpers.rb +1 -1
- data/lib/state_machines/node_collection.rb +18 -17
- data/lib/state_machines/path.rb +2 -4
- data/lib/state_machines/path_collection.rb +2 -3
- data/lib/state_machines/state.rb +6 -5
- data/lib/state_machines/state_collection.rb +3 -3
- data/lib/state_machines/state_context.rb +6 -7
- data/lib/state_machines/stdio_renderer.rb +16 -16
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +290 -27
- data/lib/state_machines/transition.rb +43 -41
- data/lib/state_machines/transition_collection.rb +22 -25
- data/lib/state_machines/version.rb +1 -1
- metadata +23 -9
@@ -2,6 +2,19 @@
|
|
2
2
|
|
3
3
|
require_relative 'options_validator'
|
4
4
|
require_relative 'machine/class_methods'
|
5
|
+
require_relative 'machine/utilities'
|
6
|
+
require_relative 'machine/parsing'
|
7
|
+
require_relative 'machine/validation'
|
8
|
+
require_relative 'machine/helper_generators'
|
9
|
+
require_relative 'machine/action_hooks'
|
10
|
+
require_relative 'machine/scoping'
|
11
|
+
require_relative 'machine/configuration'
|
12
|
+
require_relative 'machine/state_methods'
|
13
|
+
require_relative 'machine/event_methods'
|
14
|
+
require_relative 'machine/callbacks'
|
15
|
+
require_relative 'machine/rendering'
|
16
|
+
require_relative 'machine/integration'
|
17
|
+
require_relative 'syntax_validator'
|
5
18
|
|
6
19
|
module StateMachines
|
7
20
|
# Represents a state machine for a particular attribute. State machines
|
@@ -406,9 +419,22 @@ module StateMachines
|
|
406
419
|
extend ClassMethods
|
407
420
|
include EvalHelpers
|
408
421
|
include MatcherHelpers
|
422
|
+
include Utilities
|
423
|
+
include Parsing
|
424
|
+
include Validation
|
425
|
+
include HelperGenerators
|
426
|
+
include ActionHooks
|
427
|
+
include Scoping
|
428
|
+
include Configuration
|
429
|
+
include StateMethods
|
430
|
+
include EventMethods
|
431
|
+
include Callbacks
|
432
|
+
include Rendering
|
433
|
+
include Integration
|
409
434
|
|
410
435
|
# Whether to ignore any conflicts that are detected for helper methods that
|
411
436
|
# get generated for a machine's owner class. Default is false.
|
437
|
+
# Thread-safe via atomic reference updates
|
412
438
|
@ignore_method_conflicts = false
|
413
439
|
|
414
440
|
# The class that the machine is defined in
|
@@ -450,114 +476,6 @@ module StateMachines
|
|
450
476
|
attr_reader :use_transactions
|
451
477
|
|
452
478
|
# Creates a new state machine for the given attribute
|
453
|
-
def initialize(owner_class, *args, &block)
|
454
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
455
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
456
|
-
|
457
|
-
# Find an integration that matches this machine's owner class
|
458
|
-
if options.include?(:integration)
|
459
|
-
@integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
|
460
|
-
else
|
461
|
-
@integration = StateMachines::Integrations.match(owner_class)
|
462
|
-
end
|
463
|
-
|
464
|
-
if @integration
|
465
|
-
extend @integration
|
466
|
-
options = (@integration.defaults || {}).merge(options)
|
467
|
-
end
|
468
|
-
|
469
|
-
# Add machine-wide defaults
|
470
|
-
options = {use_transactions: true, initialize: true}.merge(options)
|
471
|
-
|
472
|
-
# Set machine configuration
|
473
|
-
@name = args.first || :state
|
474
|
-
@attribute = options[:attribute] || @name
|
475
|
-
@events = EventCollection.new(self)
|
476
|
-
@states = StateCollection.new(self)
|
477
|
-
@callbacks = {before: [], after: [], failure: []}
|
478
|
-
@namespace = options[:namespace]
|
479
|
-
@messages = options[:messages] || {}
|
480
|
-
@action = options[:action]
|
481
|
-
@use_transactions = options[:use_transactions]
|
482
|
-
@initialize_state = options[:initialize]
|
483
|
-
@action_hook_defined = false
|
484
|
-
self.owner_class = owner_class
|
485
|
-
|
486
|
-
# Merge with sibling machine configurations
|
487
|
-
add_sibling_machine_configs
|
488
|
-
|
489
|
-
# Define class integration
|
490
|
-
define_helpers
|
491
|
-
define_scopes(options[:plural])
|
492
|
-
after_initialize
|
493
|
-
|
494
|
-
# Evaluate DSL
|
495
|
-
instance_eval(&block) if block_given?
|
496
|
-
self.initial_state = options[:initial] unless sibling_machines.any?
|
497
|
-
end
|
498
|
-
|
499
|
-
# Creates a copy of this machine in addition to copies of each associated
|
500
|
-
# event/states/callback, so that the modifications to those collections do
|
501
|
-
# not affect the original machine.
|
502
|
-
def initialize_copy(orig) #:nodoc:
|
503
|
-
super
|
504
|
-
|
505
|
-
@events = @events.dup
|
506
|
-
@events.machine = self
|
507
|
-
@states = @states.dup
|
508
|
-
@states.machine = self
|
509
|
-
@callbacks = {before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup}
|
510
|
-
end
|
511
|
-
|
512
|
-
# Sets the class which is the owner of this state machine. Any methods
|
513
|
-
# generated by states, events, or other parts of the machine will be defined
|
514
|
-
# on the given owner class.
|
515
|
-
def owner_class=(klass)
|
516
|
-
@owner_class = klass
|
517
|
-
|
518
|
-
# Create modules for extending the class with state/event-specific methods
|
519
|
-
@helper_modules = helper_modules = {instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class)}
|
520
|
-
owner_class.class_eval do
|
521
|
-
extend helper_modules[:class]
|
522
|
-
include helper_modules[:instance]
|
523
|
-
end
|
524
|
-
|
525
|
-
# Add class-/instance-level methods to the owner class for state initialization
|
526
|
-
unless owner_class < StateMachines::InstanceMethods
|
527
|
-
owner_class.class_eval do
|
528
|
-
extend StateMachines::ClassMethods
|
529
|
-
include StateMachines::InstanceMethods
|
530
|
-
end
|
531
|
-
|
532
|
-
define_state_initializer if @initialize_state
|
533
|
-
end
|
534
|
-
|
535
|
-
# Record this machine as matched to the name in the current owner class.
|
536
|
-
# This will override any machines mapped to the same name in any superclasses.
|
537
|
-
owner_class.state_machines[name] = self
|
538
|
-
end
|
539
|
-
|
540
|
-
# Sets the initial state of the machine. This can be either the static name
|
541
|
-
# of a state or a lambda block which determines the initial state at
|
542
|
-
# creation time.
|
543
|
-
def initial_state=(new_initial_state)
|
544
|
-
@initial_state = new_initial_state
|
545
|
-
add_states([@initial_state]) unless dynamic_initial_state?
|
546
|
-
|
547
|
-
# Update all states to reflect the new initial state
|
548
|
-
states.each { |state| state.initial = (state.name == @initial_state) }
|
549
|
-
|
550
|
-
# Output a warning if there are conflicting initial states for the machine's
|
551
|
-
# attribute
|
552
|
-
initial_state = states.detect { |state| state.initial }
|
553
|
-
if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
|
554
|
-
warn(
|
555
|
-
"Both #{owner_class.name} and its #{name.inspect} machine have defined "\
|
556
|
-
"a different default for \"#{attribute}\". Use only one or the other for "\
|
557
|
-
"defining defaults to avoid unexpected behaviors."
|
558
|
-
)
|
559
|
-
end
|
560
|
-
end
|
561
479
|
|
562
480
|
# Gets the initial state of the machine for the given object. If a dynamic
|
563
481
|
# initial state was configured for this machine, then the object will be
|
@@ -593,41 +511,6 @@ module StateMachines
|
|
593
511
|
#
|
594
512
|
# vehicle.force_idle = false
|
595
513
|
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
|
596
|
-
def initial_state(object)
|
597
|
-
states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
|
598
|
-
end
|
599
|
-
|
600
|
-
# Whether a dynamic initial state is being used in the machine
|
601
|
-
def dynamic_initial_state?
|
602
|
-
instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
|
603
|
-
end
|
604
|
-
|
605
|
-
# Initializes the state on the given object. Initial values are only set if
|
606
|
-
# the machine's attribute hasn't been previously initialized.
|
607
|
-
#
|
608
|
-
# Configuration options:
|
609
|
-
# * <tt>:force</tt> - Whether to initialize the state regardless of its
|
610
|
-
# current value
|
611
|
-
# * <tt>:to</tt> - A hash to set the initial value in instead of writing
|
612
|
-
# directly to the object
|
613
|
-
def initialize_state(object, options = {})
|
614
|
-
state = initial_state(object)
|
615
|
-
if state && (options[:force] || initialize_state?(object))
|
616
|
-
value = state.value
|
617
|
-
|
618
|
-
if (hash = options[:to])
|
619
|
-
hash[attribute.to_s] = value
|
620
|
-
else
|
621
|
-
write(object, :state, value)
|
622
|
-
end
|
623
|
-
end
|
624
|
-
end
|
625
|
-
|
626
|
-
# Gets the actual name of the attribute on the machine's owner class that
|
627
|
-
# stores data with the given name.
|
628
|
-
def attribute(name = :state)
|
629
|
-
name == :state ? @attribute : :"#{self.name}_#{name}"
|
630
|
-
end
|
631
514
|
|
632
515
|
# Defines a new helper method in an instance or class scope with the given
|
633
516
|
# name. If the method is already defined in the scope, then this will not
|
@@ -665,7 +548,7 @@ module StateMachines
|
|
665
548
|
# "State"
|
666
549
|
# end
|
667
550
|
# end_eval
|
668
|
-
def define_helper(scope, method,
|
551
|
+
def define_helper(scope, method, *, **, &block)
|
669
552
|
helper_module = @helper_modules.fetch(scope)
|
670
553
|
|
671
554
|
if block_given?
|
@@ -681,7 +564,9 @@ module StateMachines
|
|
681
564
|
end
|
682
565
|
end
|
683
566
|
else
|
684
|
-
|
567
|
+
# Validate string input before eval if method is a string
|
568
|
+
validate_eval_string(method) if method.is_a?(String)
|
569
|
+
helper_module.class_eval(method, __FILE__, __LINE__)
|
685
570
|
end
|
686
571
|
end
|
687
572
|
|
@@ -952,82 +837,6 @@ module StateMachines
|
|
952
837
|
#
|
953
838
|
# The minimum requirement is that the last argument in the method be an
|
954
839
|
# options hash which contains at least <tt>:if</tt> condition support.
|
955
|
-
def state(*names, &block)
|
956
|
-
options = names.last.is_a?(Hash) ? names.pop : {}
|
957
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
|
958
|
-
|
959
|
-
# Store the context so that it can be used for / matched against any state
|
960
|
-
# that gets added
|
961
|
-
@states.context(names, &block) if block_given?
|
962
|
-
|
963
|
-
if names.first.is_a?(Matcher)
|
964
|
-
# Add any states referenced in the matcher. When matchers are used,
|
965
|
-
# states are not allowed to be configured.
|
966
|
-
raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
|
967
|
-
|
968
|
-
states = add_states(names.first.values)
|
969
|
-
else
|
970
|
-
states = add_states(names)
|
971
|
-
|
972
|
-
# Update the configuration for the state(s)
|
973
|
-
states.each do |state|
|
974
|
-
if options.include?(:value)
|
975
|
-
state.value = options[:value]
|
976
|
-
self.states.update(state)
|
977
|
-
end
|
978
|
-
|
979
|
-
state.human_name = options[:human_name] if options.include?(:human_name)
|
980
|
-
state.cache = options[:cache] if options.include?(:cache)
|
981
|
-
state.matcher = options[:if] if options.include?(:if)
|
982
|
-
end
|
983
|
-
end
|
984
|
-
|
985
|
-
states.length == 1 ? states.first : states
|
986
|
-
end
|
987
|
-
|
988
|
-
alias_method :other_states, :state
|
989
|
-
|
990
|
-
# Gets the current value stored in the given object's attribute.
|
991
|
-
#
|
992
|
-
# For example,
|
993
|
-
#
|
994
|
-
# class Vehicle
|
995
|
-
# state_machine :initial => :parked do
|
996
|
-
# ...
|
997
|
-
# end
|
998
|
-
# end
|
999
|
-
#
|
1000
|
-
# vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
|
1001
|
-
# Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
|
1002
|
-
# Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
|
1003
|
-
def read(object, attribute, ivar = false)
|
1004
|
-
attribute = self.attribute(attribute)
|
1005
|
-
if ivar
|
1006
|
-
object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
|
1007
|
-
else
|
1008
|
-
object.send(attribute)
|
1009
|
-
end
|
1010
|
-
end
|
1011
|
-
|
1012
|
-
# Sets a new value in the given object's attribute.
|
1013
|
-
#
|
1014
|
-
# For example,
|
1015
|
-
#
|
1016
|
-
# class Vehicle
|
1017
|
-
# state_machine :initial => :parked do
|
1018
|
-
# ...
|
1019
|
-
# end
|
1020
|
-
# end
|
1021
|
-
#
|
1022
|
-
# vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
|
1023
|
-
# Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
|
1024
|
-
# Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
|
1025
|
-
# vehicle.state # => "idling"
|
1026
|
-
# vehicle.event # => "park"
|
1027
|
-
def write(object, attribute, value, ivar = false)
|
1028
|
-
attribute = self.attribute(attribute)
|
1029
|
-
ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
|
1030
|
-
end
|
1031
840
|
|
1032
841
|
# Defines one or more events for the machine and the transitions that can
|
1033
842
|
# be performed when those events are run.
|
@@ -1255,36 +1064,6 @@ module StateMachines
|
|
1255
1064
|
# end
|
1256
1065
|
# end
|
1257
1066
|
# end
|
1258
|
-
def event(*names, &block)
|
1259
|
-
options = names.last.is_a?(Hash) ? names.pop : {}
|
1260
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
1261
|
-
|
1262
|
-
# Store the context so that it can be used for / matched against any event
|
1263
|
-
# that gets added
|
1264
|
-
@events.context(names, &block) if block_given?
|
1265
|
-
|
1266
|
-
if names.first.is_a?(Matcher)
|
1267
|
-
# Add any events referenced in the matcher. When matchers are used,
|
1268
|
-
# events are not allowed to be configured.
|
1269
|
-
raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
|
1270
|
-
|
1271
|
-
events = add_events(names.first.values)
|
1272
|
-
else
|
1273
|
-
events = add_events(names)
|
1274
|
-
|
1275
|
-
# Update the configuration for the event(s)
|
1276
|
-
events.each do |event|
|
1277
|
-
event.human_name = options[:human_name] if options.include?(:human_name)
|
1278
|
-
|
1279
|
-
# Add any states that may have been referenced within the event
|
1280
|
-
add_states(event.known_states)
|
1281
|
-
end
|
1282
|
-
end
|
1283
|
-
|
1284
|
-
events.length == 1 ? events.first : events
|
1285
|
-
end
|
1286
|
-
|
1287
|
-
alias_method :on, :event
|
1288
1067
|
|
1289
1068
|
# Creates a new transition that determines what to change the current state
|
1290
1069
|
# to when an event fires.
|
@@ -1375,15 +1154,6 @@ module StateMachines
|
|
1375
1154
|
# Transitions are evaluated in the order in which they're defined. As a
|
1376
1155
|
# result, if more than one transition applies to a given object, then the
|
1377
1156
|
# first transition that matches will be performed.
|
1378
|
-
def transition(options)
|
1379
|
-
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
1380
|
-
|
1381
|
-
branches = []
|
1382
|
-
options = options.dup
|
1383
|
-
event(*Array(options.delete(:on))) { branches << transition(options) }
|
1384
|
-
|
1385
|
-
branches.length == 1 ? branches.first : branches
|
1386
|
-
end
|
1387
1157
|
|
1388
1158
|
# Creates a callback that will be invoked *before* a transition is
|
1389
1159
|
# performed so long as the given requirements match the transition.
|
@@ -1590,10 +1360,15 @@ module StateMachines
|
|
1590
1360
|
#
|
1591
1361
|
# As can be seen, any number of transitions can be created using various
|
1592
1362
|
# combinations of configuration options.
|
1593
|
-
def before_transition(*args, &
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1363
|
+
def before_transition(*args, **options, &)
|
1364
|
+
# Extract legacy positional arguments and merge with keyword options
|
1365
|
+
parsed_options = parse_callback_arguments(args, options)
|
1366
|
+
|
1367
|
+
# Only validate callback-specific options, not state transition requirements
|
1368
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
1369
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
1370
|
+
|
1371
|
+
add_callback(:before, parsed_options, &)
|
1597
1372
|
end
|
1598
1373
|
|
1599
1374
|
# Creates a callback that will be invoked *after* a transition is
|
@@ -1601,10 +1376,15 @@ module StateMachines
|
|
1601
1376
|
#
|
1602
1377
|
# See +before_transition+ for a description of the possible configurations
|
1603
1378
|
# for defining callbacks.
|
1604
|
-
def after_transition(*args, &
|
1605
|
-
|
1606
|
-
|
1607
|
-
|
1379
|
+
def after_transition(*args, **options, &)
|
1380
|
+
# Extract legacy positional arguments and merge with keyword options
|
1381
|
+
parsed_options = parse_callback_arguments(args, options)
|
1382
|
+
|
1383
|
+
# Only validate callback-specific options, not state transition requirements
|
1384
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
1385
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
1386
|
+
|
1387
|
+
add_callback(:after, parsed_options, &)
|
1608
1388
|
end
|
1609
1389
|
|
1610
1390
|
# Creates a callback that will be invoked *around* a transition so long as
|
@@ -1662,10 +1442,15 @@ module StateMachines
|
|
1662
1442
|
#
|
1663
1443
|
# See +before_transition+ for a description of the possible configurations
|
1664
1444
|
# for defining callbacks.
|
1665
|
-
def around_transition(*args, &
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
1445
|
+
def around_transition(*args, **options, &)
|
1446
|
+
# Extract legacy positional arguments and merge with keyword options
|
1447
|
+
parsed_options = parse_callback_arguments(args, options)
|
1448
|
+
|
1449
|
+
# Only validate callback-specific options, not state transition requirements
|
1450
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
1451
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
1452
|
+
|
1453
|
+
add_callback(:around, parsed_options, &)
|
1669
1454
|
end
|
1670
1455
|
|
1671
1456
|
# Creates a callback that will be invoked *after* a transition failures to
|
@@ -1696,12 +1481,12 @@ module StateMachines
|
|
1696
1481
|
# ...
|
1697
1482
|
# end
|
1698
1483
|
# end
|
1699
|
-
def after_failure(*args, &
|
1700
|
-
|
1701
|
-
|
1702
|
-
StateMachines::OptionsValidator.assert_valid_keys!(
|
1484
|
+
def after_failure(*args, **options, &)
|
1485
|
+
# Extract legacy positional arguments and merge with keyword options
|
1486
|
+
parsed_options = parse_callback_arguments(args, options)
|
1487
|
+
StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless)
|
1703
1488
|
|
1704
|
-
add_callback(:failure,
|
1489
|
+
add_callback(:failure, parsed_options, &)
|
1705
1490
|
end
|
1706
1491
|
|
1707
1492
|
# Generates a list of the possible transition sequences that can be run on
|
@@ -1773,15 +1558,11 @@ module StateMachines
|
|
1773
1558
|
#
|
1774
1559
|
# # Get the list of events that can be accessed from the current state
|
1775
1560
|
# vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
|
1776
|
-
def paths_for(object, requirements = {})
|
1777
|
-
PathCollection.new(object, self, requirements)
|
1778
|
-
end
|
1779
1561
|
|
1780
1562
|
# Marks the given object as invalid with the given message.
|
1781
1563
|
#
|
1782
1564
|
# By default, this is a no-op.
|
1783
|
-
def invalidate(_object, _attribute, _message, _values = [])
|
1784
|
-
end
|
1565
|
+
def invalidate(_object, _attribute, _message, _values = []); end
|
1785
1566
|
|
1786
1567
|
# Gets a description of the errors for the given object. This is used to
|
1787
1568
|
# provide more detailed information when an InvalidTransition exception is
|
@@ -1793,18 +1574,17 @@ module StateMachines
|
|
1793
1574
|
# Resets any errors previously added when invalidating the given object.
|
1794
1575
|
#
|
1795
1576
|
# By default, this is a no-op.
|
1796
|
-
def reset(_object)
|
1797
|
-
end
|
1577
|
+
def reset(_object); end
|
1798
1578
|
|
1799
1579
|
# Generates the message to use when invalidating the given object after
|
1800
1580
|
# failing to transition on a specific event
|
1801
1581
|
def generate_message(name, values = [])
|
1802
|
-
message =
|
1582
|
+
message = @messages[name] || self.class.default_messages[name]
|
1803
1583
|
|
1804
1584
|
# Check whether there are actually any values to interpolate to avoid
|
1805
1585
|
# any warnings
|
1806
1586
|
if message.scan(/%./).any? { |match| match != '%%' }
|
1807
|
-
message % values.map
|
1587
|
+
message % values.map(&:last)
|
1808
1588
|
else
|
1809
1589
|
message
|
1810
1590
|
end
|
@@ -1815,309 +1595,40 @@ module StateMachines
|
|
1815
1595
|
# This is only applicable to integrations that involve databases. By
|
1816
1596
|
# default, this will not run any transactions since the changes aren't
|
1817
1597
|
# taking place within the context of a database.
|
1818
|
-
def within_transaction(object)
|
1598
|
+
def within_transaction(object, &)
|
1819
1599
|
if use_transactions
|
1820
|
-
transaction(object)
|
1600
|
+
transaction(object, &)
|
1821
1601
|
else
|
1822
1602
|
yield
|
1823
1603
|
end
|
1824
1604
|
end
|
1825
1605
|
|
1826
|
-
|
1827
1606
|
def renderer
|
1828
1607
|
self.class.renderer
|
1829
1608
|
end
|
1830
1609
|
|
1831
|
-
def draw(**
|
1832
|
-
renderer.draw_machine(self, **
|
1610
|
+
def draw(**)
|
1611
|
+
renderer.draw_machine(self, **)
|
1833
1612
|
end
|
1834
1613
|
|
1835
1614
|
# Determines whether an action hook was defined for firing attribute-based
|
1836
1615
|
# event transitions when the configured action gets called.
|
1837
1616
|
def action_hook?(self_only = false)
|
1838
|
-
@action_hook_defined || !self_only && owner_class.state_machines.any? { |
|
1617
|
+
@action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) })
|
1839
1618
|
end
|
1840
1619
|
|
1841
1620
|
protected
|
1842
1621
|
|
1843
1622
|
# Runs additional initialization hooks. By default, this is a no-op.
|
1844
|
-
def after_initialize
|
1845
|
-
end
|
1846
|
-
|
1847
|
-
# Looks up other machines that have been defined in the owner class and
|
1848
|
-
# are targeting the same attribute as this machine. When accessing
|
1849
|
-
# sibling machines, they will be automatically copied for the current
|
1850
|
-
# class if they haven't been already. This ensures that any configuration
|
1851
|
-
# changes made to the sibling machines only affect this class and not any
|
1852
|
-
# base class that may have originally defined the machine.
|
1853
|
-
def sibling_machines
|
1854
|
-
owner_class.state_machines.inject([]) do |machines, (name, machine)|
|
1855
|
-
if machine.attribute == attribute && machine != self
|
1856
|
-
machines << (owner_class.state_machine(name) {})
|
1857
|
-
end
|
1858
|
-
machines
|
1859
|
-
end
|
1860
|
-
end
|
1861
|
-
|
1862
|
-
# Determines if the machine's attribute needs to be initialized. This
|
1863
|
-
# will only be true if the machine's attribute is blank.
|
1864
|
-
def initialize_state?(object)
|
1865
|
-
value = read(object, :state)
|
1866
|
-
(value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
|
1867
|
-
end
|
1868
|
-
|
1869
|
-
# Adds helper methods for interacting with the state machine, including
|
1870
|
-
# for states, events, and transitions
|
1871
|
-
def define_helpers
|
1872
|
-
define_state_accessor
|
1873
|
-
define_state_predicate
|
1874
|
-
define_event_helpers
|
1875
|
-
define_path_helpers
|
1876
|
-
define_action_helpers if define_action_helpers?
|
1877
|
-
define_name_helpers
|
1878
|
-
end
|
1879
|
-
|
1880
|
-
# Defines the initial values for state machine attributes. Static values
|
1881
|
-
# are set prior to the original initialize method and dynamic values are
|
1882
|
-
# set *after* the initialize method in case it is dependent on it.
|
1883
|
-
def define_state_initializer
|
1884
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1885
|
-
def initialize(*)
|
1886
|
-
self.class.state_machines.initialize_states(self) { super }
|
1887
|
-
end
|
1888
|
-
end_eval
|
1889
|
-
end
|
1890
|
-
|
1891
|
-
# Adds reader/writer methods for accessing the state attribute
|
1892
|
-
def define_state_accessor
|
1893
|
-
attribute = self.attribute
|
1894
|
-
|
1895
|
-
@helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
|
1896
|
-
@helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
|
1897
|
-
end
|
1898
|
-
|
1899
|
-
# Adds predicate method to the owner class for determining the name of the
|
1900
|
-
# current state
|
1901
|
-
def define_state_predicate
|
1902
|
-
call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
|
1903
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1904
|
-
def #{name}?(*args)
|
1905
|
-
args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
|
1906
|
-
end
|
1907
|
-
end_eval
|
1908
|
-
end
|
1909
|
-
|
1910
|
-
# Adds helper methods for getting information about this state machine's
|
1911
|
-
# events
|
1912
|
-
def define_event_helpers
|
1913
|
-
# Gets the events that are allowed to fire on the current object
|
1914
|
-
define_helper(:instance, attribute(:events)) do |machine, object, *args|
|
1915
|
-
machine.events.valid_for(object, *args).map { |event| event.name }
|
1916
|
-
end
|
1917
|
-
|
1918
|
-
# Gets the next possible transitions that can be run on the current
|
1919
|
-
# object
|
1920
|
-
define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
|
1921
|
-
machine.events.transitions_for(object, *args)
|
1922
|
-
end
|
1923
|
-
|
1924
|
-
# Fire an arbitrary event for this machine
|
1925
|
-
define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
|
1926
|
-
machine.events.fetch(event).fire(object, *args)
|
1927
|
-
end
|
1928
|
-
|
1929
|
-
# Add helpers for tracking the event / transition to invoke when the
|
1930
|
-
# action is called
|
1931
|
-
if action
|
1932
|
-
event_attribute = attribute(:event)
|
1933
|
-
define_helper(:instance, event_attribute) do |machine, object|
|
1934
|
-
# Interpret non-blank events as present
|
1935
|
-
event = machine.read(object, :event, true)
|
1936
|
-
event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
|
1937
|
-
end
|
1938
|
-
|
1939
|
-
# A roundabout way of writing the attribute is used here so that
|
1940
|
-
# integrations can hook into this modification
|
1941
|
-
define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
|
1942
|
-
machine.write(object, :event, value, true)
|
1943
|
-
end
|
1944
|
-
|
1945
|
-
event_transition_attribute = attribute(:event_transition)
|
1946
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1947
|
-
protected; attr_accessor #{event_transition_attribute.inspect}
|
1948
|
-
end_eval
|
1949
|
-
end
|
1950
|
-
end
|
1951
|
-
|
1952
|
-
# Adds helper methods for getting information about this state machine's
|
1953
|
-
# available transition paths
|
1954
|
-
def define_path_helpers
|
1955
|
-
# Gets the paths of transitions available to the current object
|
1956
|
-
define_helper(:instance, attribute(:paths)) do |machine, object, *args|
|
1957
|
-
machine.paths_for(object, *args)
|
1958
|
-
end
|
1959
|
-
end
|
1960
|
-
|
1961
|
-
# Determines whether action helpers should be defined for this machine.
|
1962
|
-
# This is only true if there is an action configured and no other machines
|
1963
|
-
# have process this same configuration already.
|
1964
|
-
def define_action_helpers?
|
1965
|
-
action && !owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self }
|
1966
|
-
end
|
1967
|
-
|
1968
|
-
# Adds helper methods for automatically firing events when an action
|
1969
|
-
# is invoked
|
1970
|
-
def define_action_helpers
|
1971
|
-
if action_hook
|
1972
|
-
@action_hook_defined = true
|
1973
|
-
define_action_hook
|
1974
|
-
end
|
1975
|
-
end
|
1976
|
-
|
1977
|
-
# Hooks directly into actions by defining the same method in an included
|
1978
|
-
# module. As a result, when the action gets invoked, any state events
|
1979
|
-
# defined for the object will get run. Method visibility is preserved.
|
1980
|
-
def define_action_hook
|
1981
|
-
action_hook = self.action_hook
|
1982
|
-
action = self.action
|
1983
|
-
private_action_hook = owner_class.private_method_defined?(action_hook)
|
1984
|
-
|
1985
|
-
# Only define helper if it hasn't
|
1986
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1987
|
-
def #{action_hook}(*)
|
1988
|
-
self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
|
1989
|
-
end
|
1990
|
-
|
1991
|
-
private #{action_hook.inspect} if #{private_action_hook}
|
1992
|
-
end_eval
|
1993
|
-
end
|
1994
|
-
|
1995
|
-
# The method to hook into for triggering transitions when invoked. By
|
1996
|
-
# default, this is the action configured for the machine.
|
1997
|
-
#
|
1998
|
-
# Since the default hook technique relies on module inheritance, the
|
1999
|
-
# action must be defined in an ancestor of the owner classs in order for
|
2000
|
-
# it to be the action hook.
|
2001
|
-
def action_hook
|
2002
|
-
action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
|
2003
|
-
end
|
1623
|
+
def after_initialize; end
|
2004
1624
|
|
2005
1625
|
# Determines whether there's already a helper method defined within the
|
2006
1626
|
# given scope. This is true only if one of the owner's ancestors defines
|
2007
1627
|
# the method and is further along in the ancestor chain than this
|
2008
1628
|
# machine's helper module.
|
2009
|
-
def owner_class_ancestor_has_method?(scope, method)
|
2010
|
-
return false unless owner_class_has_method?(scope, method)
|
2011
|
-
|
2012
|
-
superclasses = owner_class.ancestors.select { |ancestor| ancestor.is_a?(Class) }[1..-1]
|
2013
|
-
|
2014
|
-
if scope == :class
|
2015
|
-
current = owner_class.singleton_class
|
2016
|
-
superclass = superclasses.first
|
2017
|
-
else
|
2018
|
-
current = owner_class
|
2019
|
-
superclass = owner_class.superclass
|
2020
|
-
end
|
2021
|
-
|
2022
|
-
# Generate the list of modules that *only* occur in the owner class, but
|
2023
|
-
# were included *prior* to the helper modules, in addition to the
|
2024
|
-
# superclasses
|
2025
|
-
ancestors = current.ancestors - superclass.ancestors + superclasses
|
2026
|
-
ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
|
2027
|
-
|
2028
|
-
# Search for for the first ancestor that defined this method
|
2029
|
-
ancestors.detect do |ancestor|
|
2030
|
-
ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
|
2031
|
-
ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
|
2032
|
-
end
|
2033
|
-
end
|
2034
|
-
|
2035
|
-
def owner_class_has_method?(scope, method)
|
2036
|
-
target = scope == :class ? owner_class.singleton_class : owner_class
|
2037
|
-
target.method_defined?(method) || target.private_method_defined?(method)
|
2038
|
-
end
|
2039
|
-
|
2040
|
-
# Adds helper methods for accessing naming information about states and
|
2041
|
-
# events on the owner class
|
2042
|
-
def define_name_helpers
|
2043
|
-
# Gets the humanized version of a state
|
2044
|
-
define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
|
2045
|
-
machine.states.fetch(state).human_name(klass)
|
2046
|
-
end
|
2047
|
-
|
2048
|
-
# Gets the humanized version of an event
|
2049
|
-
define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
|
2050
|
-
machine.events.fetch(event).human_name(klass)
|
2051
|
-
end
|
2052
|
-
|
2053
|
-
# Gets the state name for the current value
|
2054
|
-
define_helper(:instance, attribute(:name)) do |machine, object|
|
2055
|
-
machine.states.match!(object).name
|
2056
|
-
end
|
2057
|
-
|
2058
|
-
# Gets the human state name for the current value
|
2059
|
-
define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
|
2060
|
-
machine.states.match!(object).human_name(object.class)
|
2061
|
-
end
|
2062
|
-
end
|
2063
|
-
|
2064
|
-
# Defines the with/without scope helpers for this attribute. Both the
|
2065
|
-
# singular and plural versions of the attribute are defined for each
|
2066
|
-
# scope helper. A custom plural can be specified if it cannot be
|
2067
|
-
# automatically determined by either calling +pluralize+ on the attribute
|
2068
|
-
# name or adding an "s" to the end of the name.
|
2069
|
-
def define_scopes(custom_plural = nil)
|
2070
|
-
plural = custom_plural || pluralize(name)
|
2071
|
-
|
2072
|
-
[:with, :without].each do |kind|
|
2073
|
-
[name, plural].map { |s| s.to_s }.uniq.each do |suffix|
|
2074
|
-
method = "#{kind}_#{suffix}"
|
2075
|
-
|
2076
|
-
if (scope = send("create_#{kind}_scope", method))
|
2077
|
-
# Converts state names to their corresponding values so that they
|
2078
|
-
# can be looked up properly
|
2079
|
-
define_helper(:class, method) do |machine, klass, *states|
|
2080
|
-
run_scope(scope, machine, klass, states)
|
2081
|
-
end
|
2082
|
-
end
|
2083
|
-
end
|
2084
|
-
end
|
2085
|
-
end
|
2086
|
-
|
2087
|
-
# Generates the results for the given scope based on one or more states to
|
2088
|
-
# filter by
|
2089
|
-
def run_scope(scope, machine, klass, states)
|
2090
|
-
values = states.flatten.compact.map { |state| machine.states.fetch(state).value }
|
2091
|
-
scope.call(klass, values)
|
2092
|
-
end
|
2093
|
-
|
2094
|
-
# Pluralizes the given word using #pluralize (if available) or simply
|
2095
|
-
# adding an "s" to the end of the word
|
2096
|
-
def pluralize(word)
|
2097
|
-
word = word.to_s
|
2098
|
-
if word.respond_to?(:pluralize)
|
2099
|
-
word.pluralize
|
2100
|
-
else
|
2101
|
-
"#{name}s"
|
2102
|
-
end
|
2103
|
-
end
|
2104
|
-
|
2105
|
-
# Creates a scope for finding objects *with* a particular value or values
|
2106
|
-
# for the attribute.
|
2107
|
-
#
|
2108
|
-
# By default, this is a no-op.
|
2109
|
-
def create_with_scope(name)
|
2110
|
-
end
|
2111
|
-
|
2112
|
-
# Creates a scope for finding objects *without* a particular value or
|
2113
|
-
# values for the attribute.
|
2114
|
-
#
|
2115
|
-
# By default, this is a no-op.
|
2116
|
-
def create_without_scope(name)
|
2117
|
-
end
|
2118
1629
|
|
2119
1630
|
# Always yields
|
2120
|
-
def transaction(
|
1631
|
+
def transaction(_object)
|
2121
1632
|
yield
|
2122
1633
|
end
|
2123
1634
|
|
@@ -2132,60 +1643,5 @@ module StateMachines
|
|
2132
1643
|
def owner_class_attribute_default_matches?(state)
|
2133
1644
|
state.matches?(owner_class_attribute_default)
|
2134
1645
|
end
|
2135
|
-
|
2136
|
-
# Updates this machine based on the configuration of other machines in the
|
2137
|
-
# owner class that share the same target attribute.
|
2138
|
-
def add_sibling_machine_configs
|
2139
|
-
# Add existing states
|
2140
|
-
sibling_machines.each do |machine|
|
2141
|
-
machine.states.each { |state| states << state unless states[state.name] }
|
2142
|
-
end
|
2143
|
-
end
|
2144
|
-
|
2145
|
-
# Adds a new transition callback of the given type.
|
2146
|
-
def add_callback(type, options, &block)
|
2147
|
-
callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
|
2148
|
-
add_states(callback.known_states)
|
2149
|
-
callback
|
2150
|
-
end
|
2151
|
-
|
2152
|
-
# Tracks the given set of states in the list of all known states for
|
2153
|
-
# this machine
|
2154
|
-
def add_states(new_states)
|
2155
|
-
new_states.map do |new_state|
|
2156
|
-
# Check for other states that use a different class type for their name.
|
2157
|
-
# This typically prevents string / symbol misuse.
|
2158
|
-
if new_state && (conflict = states.detect { |state| state.name && state.name.class != new_state.class })
|
2159
|
-
raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
|
2160
|
-
end
|
2161
|
-
|
2162
|
-
unless (state = states[new_state])
|
2163
|
-
states << state = State.new(self, new_state)
|
2164
|
-
|
2165
|
-
# Copy states over to sibling machines
|
2166
|
-
sibling_machines.each { |machine| machine.states << state }
|
2167
|
-
end
|
2168
|
-
|
2169
|
-
state
|
2170
|
-
end
|
2171
|
-
end
|
2172
|
-
|
2173
|
-
# Tracks the given set of events in the list of all known events for
|
2174
|
-
# this machine
|
2175
|
-
def add_events(new_events)
|
2176
|
-
new_events.map do |new_event|
|
2177
|
-
# Check for other states that use a different class type for their name.
|
2178
|
-
# This typically prevents string / symbol misuse.
|
2179
|
-
if (conflict = events.detect { |event| event.name.class != new_event.class })
|
2180
|
-
raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
|
2181
|
-
end
|
2182
|
-
|
2183
|
-
unless (event = events[new_event])
|
2184
|
-
events << event = Event.new(self, new_event)
|
2185
|
-
end
|
2186
|
-
|
2187
|
-
event
|
2188
|
-
end
|
2189
|
-
end
|
2190
1646
|
end
|
2191
1647
|
end
|