state_machines 0.6.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +205 -14
  3. data/lib/state_machines/branch.rb +20 -17
  4. data/lib/state_machines/callback.rb +13 -12
  5. data/lib/state_machines/core.rb +3 -3
  6. data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
  7. data/lib/state_machines/core_ext.rb +2 -0
  8. data/lib/state_machines/error.rb +7 -4
  9. data/lib/state_machines/eval_helpers.rb +93 -26
  10. data/lib/state_machines/event.rb +41 -29
  11. data/lib/state_machines/event_collection.rb +6 -5
  12. data/lib/state_machines/extensions.rb +7 -5
  13. data/lib/state_machines/helper_module.rb +3 -1
  14. data/lib/state_machines/integrations/base.rb +3 -1
  15. data/lib/state_machines/integrations.rb +13 -14
  16. data/lib/state_machines/machine/action_hooks.rb +53 -0
  17. data/lib/state_machines/machine/callbacks.rb +59 -0
  18. data/lib/state_machines/machine/class_methods.rb +93 -0
  19. data/lib/state_machines/machine/configuration.rb +124 -0
  20. data/lib/state_machines/machine/event_methods.rb +59 -0
  21. data/lib/state_machines/machine/helper_generators.rb +125 -0
  22. data/lib/state_machines/machine/integration.rb +70 -0
  23. data/lib/state_machines/machine/parsing.rb +77 -0
  24. data/lib/state_machines/machine/rendering.rb +17 -0
  25. data/lib/state_machines/machine/scoping.rb +44 -0
  26. data/lib/state_machines/machine/state_methods.rb +101 -0
  27. data/lib/state_machines/machine/utilities.rb +85 -0
  28. data/lib/state_machines/machine/validation.rb +39 -0
  29. data/lib/state_machines/machine.rb +83 -673
  30. data/lib/state_machines/machine_collection.rb +23 -15
  31. data/lib/state_machines/macro_methods.rb +4 -2
  32. data/lib/state_machines/matcher.rb +8 -5
  33. data/lib/state_machines/matcher_helpers.rb +3 -1
  34. data/lib/state_machines/node_collection.rb +23 -18
  35. data/lib/state_machines/options_validator.rb +72 -0
  36. data/lib/state_machines/path.rb +7 -5
  37. data/lib/state_machines/path_collection.rb +7 -4
  38. data/lib/state_machines/state.rb +76 -47
  39. data/lib/state_machines/state_collection.rb +5 -3
  40. data/lib/state_machines/state_context.rb +11 -8
  41. data/lib/state_machines/stdio_renderer.rb +74 -0
  42. data/lib/state_machines/syntax_validator.rb +57 -0
  43. data/lib/state_machines/test_helper.rb +568 -0
  44. data/lib/state_machines/transition.rb +45 -41
  45. data/lib/state_machines/transition_collection.rb +27 -26
  46. data/lib/state_machines/version.rb +3 -1
  47. data/lib/state_machines.rb +4 -1
  48. metadata +32 -16
  49. data/lib/state_machines/assertions.rb +0 -40
@@ -1,3 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
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'
18
+
1
19
  module StateMachines
2
20
  # Represents a state machine for a particular attribute. State machines
3
21
  # consist of states, events and a set of transitions that define how the
@@ -398,67 +416,25 @@ module StateMachines
398
416
  # machine's behavior, refer to all constants defined under the
399
417
  # StateMachines::Integrations namespace.
400
418
  class Machine
401
-
419
+ extend ClassMethods
402
420
  include EvalHelpers
403
421
  include MatcherHelpers
404
-
405
- class << self
406
- # Attempts to find or create a state machine for the given class. For
407
- # example,
408
- #
409
- # StateMachines::Machine.find_or_create(Vehicle)
410
- # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
411
- # StateMachines::Machine.find_or_create(Vehicle, :status)
412
- # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
413
- #
414
- # If a machine of the given name already exists in one of the class's
415
- # superclasses, then a copy of that machine will be created and stored
416
- # in the new owner class (the original will remain unchanged).
417
- def find_or_create(owner_class, *args, &block)
418
- options = args.last.is_a?(Hash) ? args.pop : {}
419
- name = args.first || :state
420
-
421
- # Find an existing machine
422
- machine = owner_class.respond_to?(:state_machines) &&
423
- (args.first && owner_class.state_machines[name] || !args.first &&
424
- owner_class.state_machines.values.first) || nil
425
-
426
- if machine
427
- # Only create a new copy if changes are being made to the machine in
428
- # a subclass
429
- if machine.owner_class != owner_class && (options.any? || block_given?)
430
- machine = machine.clone
431
- machine.initial_state = options[:initial] if options.include?(:initial)
432
- machine.owner_class = owner_class
433
- end
434
-
435
- # Evaluate DSL
436
- machine.instance_eval(&block) if block_given?
437
- else
438
- # No existing machine: create a new one
439
- machine = new(owner_class, name, options, &block)
440
- end
441
-
442
- machine
443
- end
444
-
445
-
446
- def draw(*)
447
- fail NotImplementedError
448
- end
449
-
450
- # Default messages to use for validation errors in ORM integrations
451
- attr_accessor :default_messages
452
- attr_accessor :ignore_method_conflicts
453
- end
454
- @default_messages = {
455
- invalid: 'is invalid',
456
- invalid_event: 'cannot transition when %s',
457
- invalid_transition: 'cannot transition via "%1$s"'
458
- }
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
459
434
 
460
435
  # Whether to ignore any conflicts that are detected for helper methods that
461
436
  # get generated for a machine's owner class. Default is false.
437
+ # Thread-safe via atomic reference updates
462
438
  @ignore_method_conflicts = false
463
439
 
464
440
  # The class that the machine is defined in
@@ -500,114 +476,6 @@ module StateMachines
500
476
  attr_reader :use_transactions
501
477
 
502
478
  # Creates a new state machine for the given attribute
503
- def initialize(owner_class, *args, &block)
504
- options = args.last.is_a?(Hash) ? args.pop : {}
505
- options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
506
-
507
- # Find an integration that matches this machine's owner class
508
- if options.include?(:integration)
509
- @integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
510
- else
511
- @integration = StateMachines::Integrations.match(owner_class)
512
- end
513
-
514
- if @integration
515
- extend @integration
516
- options = (@integration.defaults || {}).merge(options)
517
- end
518
-
519
- # Add machine-wide defaults
520
- options = {use_transactions: true, initialize: true}.merge(options)
521
-
522
- # Set machine configuration
523
- @name = args.first || :state
524
- @attribute = options[:attribute] || @name
525
- @events = EventCollection.new(self)
526
- @states = StateCollection.new(self)
527
- @callbacks = {before: [], after: [], failure: []}
528
- @namespace = options[:namespace]
529
- @messages = options[:messages] || {}
530
- @action = options[:action]
531
- @use_transactions = options[:use_transactions]
532
- @initialize_state = options[:initialize]
533
- @action_hook_defined = false
534
- self.owner_class = owner_class
535
-
536
- # Merge with sibling machine configurations
537
- add_sibling_machine_configs
538
-
539
- # Define class integration
540
- define_helpers
541
- define_scopes(options[:plural])
542
- after_initialize
543
-
544
- # Evaluate DSL
545
- instance_eval(&block) if block_given?
546
- self.initial_state = options[:initial] unless sibling_machines.any?
547
- end
548
-
549
- # Creates a copy of this machine in addition to copies of each associated
550
- # event/states/callback, so that the modifications to those collections do
551
- # not affect the original machine.
552
- def initialize_copy(orig) #:nodoc:
553
- super
554
-
555
- @events = @events.dup
556
- @events.machine = self
557
- @states = @states.dup
558
- @states.machine = self
559
- @callbacks = {before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup}
560
- end
561
-
562
- # Sets the class which is the owner of this state machine. Any methods
563
- # generated by states, events, or other parts of the machine will be defined
564
- # on the given owner class.
565
- def owner_class=(klass)
566
- @owner_class = klass
567
-
568
- # Create modules for extending the class with state/event-specific methods
569
- @helper_modules = helper_modules = {instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class)}
570
- owner_class.class_eval do
571
- extend helper_modules[:class]
572
- include helper_modules[:instance]
573
- end
574
-
575
- # Add class-/instance-level methods to the owner class for state initialization
576
- unless owner_class < StateMachines::InstanceMethods
577
- owner_class.class_eval do
578
- extend StateMachines::ClassMethods
579
- include StateMachines::InstanceMethods
580
- end
581
-
582
- define_state_initializer if @initialize_state
583
- end
584
-
585
- # Record this machine as matched to the name in the current owner class.
586
- # This will override any machines mapped to the same name in any superclasses.
587
- owner_class.state_machines[name] = self
588
- end
589
-
590
- # Sets the initial state of the machine. This can be either the static name
591
- # of a state or a lambda block which determines the initial state at
592
- # creation time.
593
- def initial_state=(new_initial_state)
594
- @initial_state = new_initial_state
595
- add_states([@initial_state]) unless dynamic_initial_state?
596
-
597
- # Update all states to reflect the new initial state
598
- states.each { |state| state.initial = (state.name == @initial_state) }
599
-
600
- # Output a warning if there are conflicting initial states for the machine's
601
- # attribute
602
- initial_state = states.detect { |state| state.initial }
603
- if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
604
- warn(
605
- "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
606
- "a different default for \"#{attribute}\". Use only one or the other for "\
607
- "defining defaults to avoid unexpected behaviors."
608
- )
609
- end
610
- end
611
479
 
612
480
  # Gets the initial state of the machine for the given object. If a dynamic
613
481
  # initial state was configured for this machine, then the object will be
@@ -643,41 +511,6 @@ module StateMachines
643
511
  #
644
512
  # vehicle.force_idle = false
645
513
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
646
- def initial_state(object)
647
- states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
648
- end
649
-
650
- # Whether a dynamic initial state is being used in the machine
651
- def dynamic_initial_state?
652
- instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
653
- end
654
-
655
- # Initializes the state on the given object. Initial values are only set if
656
- # the machine's attribute hasn't been previously initialized.
657
- #
658
- # Configuration options:
659
- # * <tt>:force</tt> - Whether to initialize the state regardless of its
660
- # current value
661
- # * <tt>:to</tt> - A hash to set the initial value in instead of writing
662
- # directly to the object
663
- def initialize_state(object, options = {})
664
- state = initial_state(object)
665
- if state && (options[:force] || initialize_state?(object))
666
- value = state.value
667
-
668
- if (hash = options[:to])
669
- hash[attribute.to_s] = value
670
- else
671
- write(object, :state, value)
672
- end
673
- end
674
- end
675
-
676
- # Gets the actual name of the attribute on the machine's owner class that
677
- # stores data with the given name.
678
- def attribute(name = :state)
679
- name == :state ? @attribute : :"#{self.name}_#{name}"
680
- end
681
514
 
682
515
  # Defines a new helper method in an instance or class scope with the given
683
516
  # name. If the method is already defined in the scope, then this will not
@@ -715,7 +548,7 @@ module StateMachines
715
548
  # "State"
716
549
  # end
717
550
  # end_eval
718
- def define_helper(scope, method, *args, **kwargs, &block)
551
+ def define_helper(scope, method, *, **, &block)
719
552
  helper_module = @helper_modules.fetch(scope)
720
553
 
721
554
  if block_given?
@@ -731,7 +564,9 @@ module StateMachines
731
564
  end
732
565
  end
733
566
  else
734
- helper_module.class_eval(method, *args, **kwargs)
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__)
735
570
  end
736
571
  end
737
572
 
@@ -1002,82 +837,6 @@ module StateMachines
1002
837
  #
1003
838
  # The minimum requirement is that the last argument in the method be an
1004
839
  # options hash which contains at least <tt>:if</tt> condition support.
1005
- def state(*names, &block)
1006
- options = names.last.is_a?(Hash) ? names.pop : {}
1007
- options.assert_valid_keys(:value, :cache, :if, :human_name)
1008
-
1009
- # Store the context so that it can be used for / matched against any state
1010
- # that gets added
1011
- @states.context(names, &block) if block_given?
1012
-
1013
- if names.first.is_a?(Matcher)
1014
- # Add any states referenced in the matcher. When matchers are used,
1015
- # states are not allowed to be configured.
1016
- raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1017
-
1018
- states = add_states(names.first.values)
1019
- else
1020
- states = add_states(names)
1021
-
1022
- # Update the configuration for the state(s)
1023
- states.each do |state|
1024
- if options.include?(:value)
1025
- state.value = options[:value]
1026
- self.states.update(state)
1027
- end
1028
-
1029
- state.human_name = options[:human_name] if options.include?(:human_name)
1030
- state.cache = options[:cache] if options.include?(:cache)
1031
- state.matcher = options[:if] if options.include?(:if)
1032
- end
1033
- end
1034
-
1035
- states.length == 1 ? states.first : states
1036
- end
1037
-
1038
- alias_method :other_states, :state
1039
-
1040
- # Gets the current value stored in the given object's attribute.
1041
- #
1042
- # For example,
1043
- #
1044
- # class Vehicle
1045
- # state_machine :initial => :parked do
1046
- # ...
1047
- # end
1048
- # end
1049
- #
1050
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1051
- # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
1052
- # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1053
- def read(object, attribute, ivar = false)
1054
- attribute = self.attribute(attribute)
1055
- if ivar
1056
- object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1057
- else
1058
- object.send(attribute)
1059
- end
1060
- end
1061
-
1062
- # Sets a new value in the given object's attribute.
1063
- #
1064
- # For example,
1065
- #
1066
- # class Vehicle
1067
- # state_machine :initial => :parked do
1068
- # ...
1069
- # end
1070
- # end
1071
- #
1072
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1073
- # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
1074
- # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
1075
- # vehicle.state # => "idling"
1076
- # vehicle.event # => "park"
1077
- def write(object, attribute, value, ivar = false)
1078
- attribute = self.attribute(attribute)
1079
- ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1080
- end
1081
840
 
1082
841
  # Defines one or more events for the machine and the transitions that can
1083
842
  # be performed when those events are run.
@@ -1305,36 +1064,6 @@ module StateMachines
1305
1064
  # end
1306
1065
  # end
1307
1066
  # end
1308
- def event(*names, &block)
1309
- options = names.last.is_a?(Hash) ? names.pop : {}
1310
- options.assert_valid_keys(:human_name)
1311
-
1312
- # Store the context so that it can be used for / matched against any event
1313
- # that gets added
1314
- @events.context(names, &block) if block_given?
1315
-
1316
- if names.first.is_a?(Matcher)
1317
- # Add any events referenced in the matcher. When matchers are used,
1318
- # events are not allowed to be configured.
1319
- raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1320
-
1321
- events = add_events(names.first.values)
1322
- else
1323
- events = add_events(names)
1324
-
1325
- # Update the configuration for the event(s)
1326
- events.each do |event|
1327
- event.human_name = options[:human_name] if options.include?(:human_name)
1328
-
1329
- # Add any states that may have been referenced within the event
1330
- add_states(event.known_states)
1331
- end
1332
- end
1333
-
1334
- events.length == 1 ? events.first : events
1335
- end
1336
-
1337
- alias_method :on, :event
1338
1067
 
1339
1068
  # Creates a new transition that determines what to change the current state
1340
1069
  # to when an event fires.
@@ -1425,15 +1154,6 @@ module StateMachines
1425
1154
  # Transitions are evaluated in the order in which they're defined. As a
1426
1155
  # result, if more than one transition applies to a given object, then the
1427
1156
  # first transition that matches will be performed.
1428
- def transition(options)
1429
- raise ArgumentError, 'Must specify :on event' unless options[:on]
1430
-
1431
- branches = []
1432
- options = options.dup
1433
- event(*Array(options.delete(:on))) { branches << transition(options) }
1434
-
1435
- branches.length == 1 ? branches.first : branches
1436
- end
1437
1157
 
1438
1158
  # Creates a callback that will be invoked *before* a transition is
1439
1159
  # performed so long as the given requirements match the transition.
@@ -1640,10 +1360,15 @@ module StateMachines
1640
1360
  #
1641
1361
  # As can be seen, any number of transitions can be created using various
1642
1362
  # combinations of configuration options.
1643
- def before_transition(*args, &block)
1644
- options = (args.last.is_a?(Hash) ? args.pop : {})
1645
- options[:do] = args if args.any?
1646
- add_callback(:before, options, &block)
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, &)
1647
1372
  end
1648
1373
 
1649
1374
  # Creates a callback that will be invoked *after* a transition is
@@ -1651,10 +1376,15 @@ module StateMachines
1651
1376
  #
1652
1377
  # See +before_transition+ for a description of the possible configurations
1653
1378
  # for defining callbacks.
1654
- def after_transition(*args, &block)
1655
- options = (args.last.is_a?(Hash) ? args.pop : {})
1656
- options[:do] = args if args.any?
1657
- add_callback(:after, options, &block)
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, &)
1658
1388
  end
1659
1389
 
1660
1390
  # Creates a callback that will be invoked *around* a transition so long as
@@ -1712,10 +1442,15 @@ module StateMachines
1712
1442
  #
1713
1443
  # See +before_transition+ for a description of the possible configurations
1714
1444
  # for defining callbacks.
1715
- def around_transition(*args, &block)
1716
- options = (args.last.is_a?(Hash) ? args.pop : {})
1717
- options[:do] = args if args.any?
1718
- add_callback(:around, options, &block)
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, &)
1719
1454
  end
1720
1455
 
1721
1456
  # Creates a callback that will be invoked *after* a transition failures to
@@ -1746,12 +1481,12 @@ module StateMachines
1746
1481
  # ...
1747
1482
  # end
1748
1483
  # end
1749
- def after_failure(*args, &block)
1750
- options = (args.last.is_a?(Hash) ? args.pop : {})
1751
- options[:do] = args if args.any?
1752
- options.assert_valid_keys(:on, :do, :if, :unless)
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)
1753
1488
 
1754
- add_callback(:failure, options, &block)
1489
+ add_callback(:failure, parsed_options, &)
1755
1490
  end
1756
1491
 
1757
1492
  # Generates a list of the possible transition sequences that can be run on
@@ -1823,15 +1558,11 @@ module StateMachines
1823
1558
  #
1824
1559
  # # Get the list of events that can be accessed from the current state
1825
1560
  # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
1826
- def paths_for(object, requirements = {})
1827
- PathCollection.new(object, self, requirements)
1828
- end
1829
1561
 
1830
1562
  # Marks the given object as invalid with the given message.
1831
1563
  #
1832
1564
  # By default, this is a no-op.
1833
- def invalidate(_object, _attribute, _message, _values = [])
1834
- end
1565
+ def invalidate(_object, _attribute, _message, _values = []); end
1835
1566
 
1836
1567
  # Gets a description of the errors for the given object. This is used to
1837
1568
  # provide more detailed information when an InvalidTransition exception is
@@ -1843,18 +1574,17 @@ module StateMachines
1843
1574
  # Resets any errors previously added when invalidating the given object.
1844
1575
  #
1845
1576
  # By default, this is a no-op.
1846
- def reset(_object)
1847
- end
1577
+ def reset(_object); end
1848
1578
 
1849
1579
  # Generates the message to use when invalidating the given object after
1850
1580
  # failing to transition on a specific event
1851
1581
  def generate_message(name, values = [])
1852
- message = (@messages[name] || self.class.default_messages[name])
1582
+ message = @messages[name] || self.class.default_messages[name]
1853
1583
 
1854
1584
  # Check whether there are actually any values to interpolate to avoid
1855
1585
  # any warnings
1856
1586
  if message.scan(/%./).any? { |match| match != '%%' }
1857
- message % values.map { |value| value.last }
1587
+ message % values.map(&:last)
1858
1588
  else
1859
1589
  message
1860
1590
  end
@@ -1865,305 +1595,40 @@ module StateMachines
1865
1595
  # This is only applicable to integrations that involve databases. By
1866
1596
  # default, this will not run any transactions since the changes aren't
1867
1597
  # taking place within the context of a database.
1868
- def within_transaction(object)
1598
+ def within_transaction(object, &)
1869
1599
  if use_transactions
1870
- transaction(object) { yield }
1600
+ transaction(object, &)
1871
1601
  else
1872
1602
  yield
1873
1603
  end
1874
1604
  end
1875
1605
 
1606
+ def renderer
1607
+ self.class.renderer
1608
+ end
1876
1609
 
1877
- def draw(*)
1878
- fail NotImplementedError
1610
+ def draw(**)
1611
+ renderer.draw_machine(self, **)
1879
1612
  end
1880
1613
 
1881
1614
  # Determines whether an action hook was defined for firing attribute-based
1882
1615
  # event transitions when the configured action gets called.
1883
1616
  def action_hook?(self_only = false)
1884
- @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
1617
+ @action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) })
1885
1618
  end
1886
1619
 
1887
- protected
1620
+ protected
1888
1621
 
1889
1622
  # Runs additional initialization hooks. By default, this is a no-op.
1890
- def after_initialize
1891
- end
1892
-
1893
- # Looks up other machines that have been defined in the owner class and
1894
- # are targeting the same attribute as this machine. When accessing
1895
- # sibling machines, they will be automatically copied for the current
1896
- # class if they haven't been already. This ensures that any configuration
1897
- # changes made to the sibling machines only affect this class and not any
1898
- # base class that may have originally defined the machine.
1899
- def sibling_machines
1900
- owner_class.state_machines.inject([]) do |machines, (name, machine)|
1901
- if machine.attribute == attribute && machine != self
1902
- machines << (owner_class.state_machine(name) {})
1903
- end
1904
- machines
1905
- end
1906
- end
1907
-
1908
- # Determines if the machine's attribute needs to be initialized. This
1909
- # will only be true if the machine's attribute is blank.
1910
- def initialize_state?(object)
1911
- value = read(object, :state)
1912
- (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
1913
- end
1914
-
1915
- # Adds helper methods for interacting with the state machine, including
1916
- # for states, events, and transitions
1917
- def define_helpers
1918
- define_state_accessor
1919
- define_state_predicate
1920
- define_event_helpers
1921
- define_path_helpers
1922
- define_action_helpers if define_action_helpers?
1923
- define_name_helpers
1924
- end
1925
-
1926
- # Defines the initial values for state machine attributes. Static values
1927
- # are set prior to the original initialize method and dynamic values are
1928
- # set *after* the initialize method in case it is dependent on it.
1929
- def define_state_initializer
1930
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1931
- def initialize(*)
1932
- self.class.state_machines.initialize_states(self) { super }
1933
- end
1934
- end_eval
1935
- end
1936
-
1937
- # Adds reader/writer methods for accessing the state attribute
1938
- def define_state_accessor
1939
- attribute = self.attribute
1940
-
1941
- @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
1942
- @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
1943
- end
1944
-
1945
- # Adds predicate method to the owner class for determining the name of the
1946
- # current state
1947
- def define_state_predicate
1948
- call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
1949
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1950
- def #{name}?(*args)
1951
- args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
1952
- end
1953
- end_eval
1954
- end
1955
-
1956
- # Adds helper methods for getting information about this state machine's
1957
- # events
1958
- def define_event_helpers
1959
- # Gets the events that are allowed to fire on the current object
1960
- define_helper(:instance, attribute(:events)) do |machine, object, *args|
1961
- machine.events.valid_for(object, *args).map { |event| event.name }
1962
- end
1963
-
1964
- # Gets the next possible transitions that can be run on the current
1965
- # object
1966
- define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
1967
- machine.events.transitions_for(object, *args)
1968
- end
1969
-
1970
- # Fire an arbitrary event for this machine
1971
- define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
1972
- machine.events.fetch(event).fire(object, *args)
1973
- end
1974
-
1975
- # Add helpers for tracking the event / transition to invoke when the
1976
- # action is called
1977
- if action
1978
- event_attribute = attribute(:event)
1979
- define_helper(:instance, event_attribute) do |machine, object|
1980
- # Interpret non-blank events as present
1981
- event = machine.read(object, :event, true)
1982
- event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1983
- end
1984
-
1985
- # A roundabout way of writing the attribute is used here so that
1986
- # integrations can hook into this modification
1987
- define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
1988
- machine.write(object, :event, value, true)
1989
- end
1990
-
1991
- event_transition_attribute = attribute(:event_transition)
1992
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1993
- protected; attr_accessor #{event_transition_attribute.inspect}
1994
- end_eval
1995
- end
1996
- end
1997
-
1998
- # Adds helper methods for getting information about this state machine's
1999
- # available transition paths
2000
- def define_path_helpers
2001
- # Gets the paths of transitions available to the current object
2002
- define_helper(:instance, attribute(:paths)) do |machine, object, *args|
2003
- machine.paths_for(object, *args)
2004
- end
2005
- end
2006
-
2007
- # Determines whether action helpers should be defined for this machine.
2008
- # This is only true if there is an action configured and no other machines
2009
- # have process this same configuration already.
2010
- def define_action_helpers?
2011
- action && !owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self }
2012
- end
2013
-
2014
- # Adds helper methods for automatically firing events when an action
2015
- # is invoked
2016
- def define_action_helpers
2017
- if action_hook
2018
- @action_hook_defined = true
2019
- define_action_hook
2020
- end
2021
- end
2022
-
2023
- # Hooks directly into actions by defining the same method in an included
2024
- # module. As a result, when the action gets invoked, any state events
2025
- # defined for the object will get run. Method visibility is preserved.
2026
- def define_action_hook
2027
- action_hook = self.action_hook
2028
- action = self.action
2029
- private_action_hook = owner_class.private_method_defined?(action_hook)
2030
-
2031
- # Only define helper if it hasn't
2032
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2033
- def #{action_hook}(*)
2034
- self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
2035
- end
2036
-
2037
- private #{action_hook.inspect} if #{private_action_hook}
2038
- end_eval
2039
- end
2040
-
2041
- # The method to hook into for triggering transitions when invoked. By
2042
- # default, this is the action configured for the machine.
2043
- #
2044
- # Since the default hook technique relies on module inheritance, the
2045
- # action must be defined in an ancestor of the owner classs in order for
2046
- # it to be the action hook.
2047
- def action_hook
2048
- action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
2049
- end
1623
+ def after_initialize; end
2050
1624
 
2051
1625
  # Determines whether there's already a helper method defined within the
2052
1626
  # given scope. This is true only if one of the owner's ancestors defines
2053
1627
  # the method and is further along in the ancestor chain than this
2054
1628
  # machine's helper module.
2055
- def owner_class_ancestor_has_method?(scope, method)
2056
- return false unless owner_class_has_method?(scope, method)
2057
-
2058
- superclasses = owner_class.ancestors.select { |ancestor| ancestor.is_a?(Class) }[1..-1]
2059
-
2060
- if scope == :class
2061
- current = owner_class.singleton_class
2062
- superclass = superclasses.first
2063
- else
2064
- current = owner_class
2065
- superclass = owner_class.superclass
2066
- end
2067
-
2068
- # Generate the list of modules that *only* occur in the owner class, but
2069
- # were included *prior* to the helper modules, in addition to the
2070
- # superclasses
2071
- ancestors = current.ancestors - superclass.ancestors + superclasses
2072
- ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
2073
-
2074
- # Search for for the first ancestor that defined this method
2075
- ancestors.detect do |ancestor|
2076
- ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
2077
- ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2078
- end
2079
- end
2080
-
2081
- def owner_class_has_method?(scope, method)
2082
- target = scope == :class ? owner_class.singleton_class : owner_class
2083
- target.method_defined?(method) || target.private_method_defined?(method)
2084
- end
2085
-
2086
- # Adds helper methods for accessing naming information about states and
2087
- # events on the owner class
2088
- def define_name_helpers
2089
- # Gets the humanized version of a state
2090
- define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
2091
- machine.states.fetch(state).human_name(klass)
2092
- end
2093
-
2094
- # Gets the humanized version of an event
2095
- define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
2096
- machine.events.fetch(event).human_name(klass)
2097
- end
2098
-
2099
- # Gets the state name for the current value
2100
- define_helper(:instance, attribute(:name)) do |machine, object|
2101
- machine.states.match!(object).name
2102
- end
2103
-
2104
- # Gets the human state name for the current value
2105
- define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
2106
- machine.states.match!(object).human_name(object.class)
2107
- end
2108
- end
2109
-
2110
- # Defines the with/without scope helpers for this attribute. Both the
2111
- # singular and plural versions of the attribute are defined for each
2112
- # scope helper. A custom plural can be specified if it cannot be
2113
- # automatically determined by either calling +pluralize+ on the attribute
2114
- # name or adding an "s" to the end of the name.
2115
- def define_scopes(custom_plural = nil)
2116
- plural = custom_plural || pluralize(name)
2117
-
2118
- [:with, :without].each do |kind|
2119
- [name, plural].map { |s| s.to_s }.uniq.each do |suffix|
2120
- method = "#{kind}_#{suffix}"
2121
-
2122
- if (scope = send("create_#{kind}_scope", method))
2123
- # Converts state names to their corresponding values so that they
2124
- # can be looked up properly
2125
- define_helper(:class, method) do |machine, klass, *states|
2126
- run_scope(scope, machine, klass, states)
2127
- end
2128
- end
2129
- end
2130
- end
2131
- end
2132
-
2133
- # Generates the results for the given scope based on one or more states to
2134
- # filter by
2135
- def run_scope(scope, machine, klass, states)
2136
- values = states.flatten.compact.map { |state| machine.states.fetch(state).value }
2137
- scope.call(klass, values)
2138
- end
2139
-
2140
- # Pluralizes the given word using #pluralize (if available) or simply
2141
- # adding an "s" to the end of the word
2142
- def pluralize(word)
2143
- word = word.to_s
2144
- if word.respond_to?(:pluralize)
2145
- word.pluralize
2146
- else
2147
- "#{name}s"
2148
- end
2149
- end
2150
-
2151
- # Creates a scope for finding objects *with* a particular value or values
2152
- # for the attribute.
2153
- #
2154
- # By default, this is a no-op.
2155
- def create_with_scope(name)
2156
- end
2157
-
2158
- # Creates a scope for finding objects *without* a particular value or
2159
- # values for the attribute.
2160
- #
2161
- # By default, this is a no-op.
2162
- def create_without_scope(name)
2163
- end
2164
1629
 
2165
1630
  # Always yields
2166
- def transaction(object)
1631
+ def transaction(_object)
2167
1632
  yield
2168
1633
  end
2169
1634
 
@@ -2178,60 +1643,5 @@ module StateMachines
2178
1643
  def owner_class_attribute_default_matches?(state)
2179
1644
  state.matches?(owner_class_attribute_default)
2180
1645
  end
2181
-
2182
- # Updates this machine based on the configuration of other machines in the
2183
- # owner class that share the same target attribute.
2184
- def add_sibling_machine_configs
2185
- # Add existing states
2186
- sibling_machines.each do |machine|
2187
- machine.states.each { |state| states << state unless states[state.name] }
2188
- end
2189
- end
2190
-
2191
- # Adds a new transition callback of the given type.
2192
- def add_callback(type, options, &block)
2193
- callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
2194
- add_states(callback.known_states)
2195
- callback
2196
- end
2197
-
2198
- # Tracks the given set of states in the list of all known states for
2199
- # this machine
2200
- def add_states(new_states)
2201
- new_states.map do |new_state|
2202
- # Check for other states that use a different class type for their name.
2203
- # This typically prevents string / symbol misuse.
2204
- if new_state && (conflict = states.detect { |state| state.name && state.name.class != new_state.class })
2205
- raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
2206
- end
2207
-
2208
- unless (state = states[new_state])
2209
- states << state = State.new(self, new_state)
2210
-
2211
- # Copy states over to sibling machines
2212
- sibling_machines.each { |machine| machine.states << state }
2213
- end
2214
-
2215
- state
2216
- end
2217
- end
2218
-
2219
- # Tracks the given set of events in the list of all known events for
2220
- # this machine
2221
- def add_events(new_events)
2222
- new_events.map do |new_event|
2223
- # Check for other states that use a different class type for their name.
2224
- # This typically prevents string / symbol misuse.
2225
- if (conflict = events.detect { |event| event.name.class != new_event.class })
2226
- raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
2227
- end
2228
-
2229
- unless (event = events[new_event])
2230
- events << event = Event.new(self, new_event)
2231
- end
2232
-
2233
- event
2234
- end
2235
- end
2236
1646
  end
2237
1647
  end