state_machines 0.20.0 → 0.31.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 +154 -18
  3. data/lib/state_machines/branch.rb +30 -17
  4. data/lib/state_machines/callback.rb +12 -13
  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 +178 -49
  8. data/lib/state_machines/event.rb +31 -32
  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 -619
  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 +14 -7
  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 +57 -46
  42. data/lib/state_machines/transition_collection.rb +22 -25
  43. data/lib/state_machines/version.rb +1 -1
  44. metadata +23 -9
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module StateMethods
6
+ # Gets the initial state of the machine for the given object. If a dynamic
7
+ # initial state was configured for this machine, then the object will be
8
+ # passed into the lambda block to help determine the actual state.
9
+ def initial_state(object)
10
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
11
+ end
12
+
13
+ # Whether a dynamic initial state is being used in the machine
14
+ def dynamic_initial_state?
15
+ instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
16
+ end
17
+
18
+ # Initializes the state on the given object. Initial values are only set if
19
+ # the machine's attribute hasn't been previously initialized.
20
+ #
21
+ # Configuration options:
22
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
23
+ # current value
24
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
25
+ # directly to the object
26
+ def initialize_state(object, options = {})
27
+ state = initial_state(object)
28
+ return unless state && (options[:force] || initialize_state?(object))
29
+
30
+ value = state.value
31
+
32
+ if (hash = options[:to])
33
+ hash[attribute.to_s] = value
34
+ else
35
+ write(object, :state, value)
36
+ end
37
+ end
38
+
39
+ # Customizes the definition of one or more states in the machine.
40
+ def state(*names, &)
41
+ options = names.last.is_a?(Hash) ? names.pop : {}
42
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
43
+
44
+ # Store the context so that it can be used for / matched against any state
45
+ # that gets added
46
+ @states.context(names, &) if block_given?
47
+
48
+ if names.first.is_a?(Matcher)
49
+ # Add any states referenced in the matcher. When matchers are used,
50
+ # states are not allowed to be configured.
51
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
52
+
53
+ states = add_states(names.first.values)
54
+ else
55
+ states = add_states(names)
56
+
57
+ # Update the configuration for the state(s)
58
+ states.each do |state|
59
+ if options.include?(:value)
60
+ state.value = options[:value]
61
+ self.states.update(state)
62
+ end
63
+
64
+ state.human_name = options[:human_name] if options.include?(:human_name)
65
+ state.cache = options[:cache] if options.include?(:cache)
66
+ state.matcher = options[:if] if options.include?(:if)
67
+ end
68
+ end
69
+
70
+ states.length == 1 ? states.first : states
71
+ end
72
+
73
+ alias other_states state
74
+
75
+ # Gets the current value stored in the given object's attribute.
76
+ def read(object, attribute, ivar = false)
77
+ attribute = self.attribute(attribute)
78
+ if ivar
79
+ object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
80
+ else
81
+ object.send(attribute)
82
+ end
83
+ end
84
+
85
+ # Sets a new value in the given object's attribute.
86
+ def write(object, attribute, value, ivar = false)
87
+ attribute = self.attribute(attribute)
88
+ ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
89
+ end
90
+
91
+ protected
92
+
93
+ # Determines if the machine's attribute needs to be initialized. This
94
+ # will only be true if the machine's attribute is blank.
95
+ def initialize_state?(object)
96
+ value = read(object, :state)
97
+ (value.nil? || (value.respond_to?(:empty?) && value.empty?)) && !states[value, :value]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Utilities
6
+ protected
7
+
8
+ # Looks up other machines that have been defined in the owner class and
9
+ # are targeting the same attribute as this machine. When accessing
10
+ # sibling machines, they will be automatically copied for the current
11
+ # class if they haven't been already. This ensures that any configuration
12
+ # changes made to the sibling machines only affect this class and not any
13
+ # base class that may have originally defined the machine.
14
+ def sibling_machines
15
+ owner_class.state_machines.each_with_object([]) do |(name, machine), machines|
16
+ machines << (owner_class.state_machine(name) {}) if machine.attribute == attribute && machine != self
17
+ end
18
+ end
19
+
20
+ # Looks up the ancestor class that has the given method defined. This
21
+ # is used to find the method owner which is used to determine where to
22
+ # define new methods.
23
+ def owner_class_ancestor_has_method?(scope, method)
24
+ return false unless owner_class_has_method?(scope, method)
25
+
26
+ superclasses = owner_class.ancestors.select { |ancestor| ancestor.is_a?(Class) }[1..]
27
+
28
+ if scope == :class
29
+ current = owner_class.singleton_class
30
+ superclass = superclasses.first
31
+ else
32
+ current = owner_class
33
+ superclass = owner_class.superclass
34
+ end
35
+
36
+ # Generate the list of modules that *only* occur in the owner class, but
37
+ # were included *prior* to the helper modules, in addition to the
38
+ # superclasses
39
+ ancestors = current.ancestors - superclass.ancestors + superclasses
40
+ helper_module_index = ancestors.index(@helper_modules[scope])
41
+ ancestors = helper_module_index ? ancestors[helper_module_index..].reverse : ancestors.reverse
42
+
43
+ # Search for for the first ancestor that defined this method
44
+ ancestors.detect do |ancestor|
45
+ ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
46
+ ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
47
+ end
48
+ end
49
+
50
+ # Determines whether the given method is defined in the owner class or
51
+ # in a superclass.
52
+ def owner_class_has_method?(scope, method)
53
+ target = scope == :class ? owner_class.singleton_class : owner_class
54
+ target.method_defined?(method) || target.private_method_defined?(method)
55
+ end
56
+
57
+ # Pluralizes the given word using #pluralize (if available) or simply
58
+ # adding an "s" to the end of the word
59
+ def pluralize(word)
60
+ word = word.to_s
61
+ if word.respond_to?(:pluralize)
62
+ word.pluralize
63
+ else
64
+ "#{word}s"
65
+ end
66
+ end
67
+
68
+ # Generates the results for the given scope based on one or more states to
69
+ # filter by
70
+ def run_scope(scope, machine, klass, states)
71
+ values = states.flatten.compact.map { |state| machine.states.fetch(state).value }
72
+ scope.call(klass, values)
73
+ end
74
+
75
+ # Adds sibling machine configurations to the current machine. This
76
+ # will add states from other machines that have the same attribute.
77
+ def add_sibling_machine_configs
78
+ # Add existing states
79
+ sibling_machines.each do |machine|
80
+ machine.states.each { |state| states << state unless states[state.name] }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Validation
6
+ # Frozen constant to avoid repeated array allocations
7
+ DANGEROUS_PATTERNS = [
8
+ /`.*`/, # Backticks (shell execution)
9
+ /system\s*\(/, # System calls
10
+ /exec\s*\(/, # Exec calls
11
+ /eval\s*\(/, # Nested eval
12
+ /require\s+['"]/, # Require statements
13
+ /load\s+['"]/, # Load statements
14
+ /File\./, # File operations
15
+ /IO\./, # IO operations
16
+ /Dir\./, # Directory operations
17
+ /Kernel\./ # Kernel operations
18
+ ].freeze
19
+
20
+ private
21
+
22
+ # Validates string input before eval to prevent code injection
23
+ # This is a basic safety check - not foolproof security
24
+ def validate_eval_string(method_string)
25
+ # Check for obviously dangerous patterns
26
+ DANGEROUS_PATTERNS.each do |pattern|
27
+ raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern)
28
+ end
29
+
30
+ # Basic syntax validation (cross-platform)
31
+ begin
32
+ SyntaxValidator.validate!(method_string, '(eval)')
33
+ rescue SyntaxError => e
34
+ raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end