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