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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -13
  3. data/lib/state_machines/branch.rb +12 -13
  4. data/lib/state_machines/callback.rb +11 -12
  5. data/lib/state_machines/core.rb +0 -1
  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 +23 -26
  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 +73 -617
  28. data/lib/state_machines/machine_collection.rb +18 -14
  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 +18 -17
  33. data/lib/state_machines/path.rb +2 -4
  34. data/lib/state_machines/path_collection.rb +2 -3
  35. data/lib/state_machines/state.rb +6 -5
  36. data/lib/state_machines/state_collection.rb +3 -3
  37. data/lib/state_machines/state_context.rb +6 -7
  38. data/lib/state_machines/stdio_renderer.rb +16 -16
  39. data/lib/state_machines/syntax_validator.rb +57 -0
  40. data/lib/state_machines/test_helper.rb +290 -27
  41. data/lib/state_machines/transition.rb +43 -41
  42. data/lib/state_machines/transition_collection.rb +22 -25
  43. data/lib/state_machines/version.rb +1 -1
  44. 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, *args, **kwargs, &block)
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
- 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__)
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, &block)
1594
- options = (args.last.is_a?(Hash) ? args.pop : {})
1595
- options[:do] = args if args.any?
1596
- 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, &)
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, &block)
1605
- options = (args.last.is_a?(Hash) ? args.pop : {})
1606
- options[:do] = args if args.any?
1607
- 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, &)
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, &block)
1666
- options = (args.last.is_a?(Hash) ? args.pop : {})
1667
- options[:do] = args if args.any?
1668
- 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, &)
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, &block)
1700
- options = (args.last.is_a?(Hash) ? args.pop : {})
1701
- options[:do] = args if args.any?
1702
- StateMachines::OptionsValidator.assert_valid_keys!(options, :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)
1703
1488
 
1704
- add_callback(:failure, options, &block)
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 = (@messages[name] || self.class.default_messages[name])
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 { |value| value.last }
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) { yield }
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(**options)
1832
- renderer.draw_machine(self, **options)
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? { |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) })
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(object)
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