rails-workflow 1.4.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.
@@ -0,0 +1,29 @@
1
+ module Workflow
2
+ module Errors
3
+ class TransitionHaltedError < StandardError
4
+
5
+ attr_reader :halted_because
6
+
7
+ def initialize(msg = nil)
8
+ @halted_because = msg
9
+ super msg
10
+ end
11
+
12
+ end
13
+
14
+ class NoMatchingTransitionError < StandardError
15
+ end
16
+
17
+ class NoTransitionAllowed < StandardError
18
+ end
19
+
20
+ class WorkflowError < StandardError
21
+ end
22
+
23
+ class CallbackArityError < StandardError
24
+ end
25
+
26
+ class WorkflowDefinitionError < StandardError
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,129 @@
1
+ module Workflow
2
+ class Event
3
+ attr_reader :name, :transitions, :meta
4
+
5
+ def initialize(name, meta)
6
+ @name = name.to_sym
7
+ @transitions = []
8
+ @meta = meta || {}
9
+ end
10
+
11
+ def inspect
12
+ "<Event name=#{name.inspect} transitions(#{transitions.length})=#{transitions.inspect}>"
13
+ end
14
+
15
+ def evaluate(target)
16
+ transition = transitions.find{|t| t.apply? target}
17
+ if transition
18
+ return transition.target_state
19
+ else
20
+ nil
21
+ end
22
+ end
23
+
24
+ def to(target_state, **conditions_def, &block)
25
+ conditions = Conditions.new &&conditions_def, block
26
+ self.transitions << Transition.new(target_state, conditions_def, &block)
27
+ end
28
+
29
+ private
30
+ class Transition
31
+ attr_accessor :target_state, :conditions
32
+ def apply?(target)
33
+ conditions.apply?(target)
34
+ end
35
+ # delegate :apply?, to: :conditions
36
+ def initialize(target_state, conditions_def, &block)
37
+ @target_state = target_state
38
+ @conditions = Conditions.new conditions_def, &block
39
+ end
40
+
41
+ def inspect
42
+ "<to=#{target_state.inspect} conditions=#{conditions.inspect}"
43
+ end
44
+ end
45
+
46
+ class Conditions #:nodoc:#
47
+
48
+ def initialize(**options, &block)
49
+ @if = Array(options[:if])
50
+ @unless = Array(options[:unless])
51
+ @if << block if block_given?
52
+ @conditions_lambdas = conditions_lambdas
53
+ end
54
+
55
+ def inspect
56
+ "if: #{@if}, unless: #{@unless}"
57
+ end
58
+
59
+ def apply?(target)
60
+ # TODO: Remove the second parameter from the conditions below.
61
+ @conditions_lambdas.all?{|l| l.call(target, ->(){})}
62
+ end
63
+
64
+ private
65
+
66
+ # Copied from https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L366
67
+ def invert_lambda(l)
68
+ lambda { |*args, &blk| !l.call(*args, &blk) }
69
+ end
70
+
71
+ # Filters support:
72
+ #
73
+ # Symbols:: A method to call.
74
+ # Strings:: Some content to evaluate.
75
+ # Procs:: A proc to call with the object.
76
+ # Objects:: An object with a <tt>before_foo</tt> method on it to call.
77
+ #
78
+ # All of these objects are converted into a lambda and handled
79
+ # the same after this point.
80
+ # Copied from https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L379
81
+ def make_lambda(filter)
82
+ case filter
83
+ when Symbol
84
+ lambda { |target, _, &blk| target.send filter, &blk }
85
+ when String
86
+ l = eval "lambda { |value| #{filter} }"
87
+ lambda { |target, value| target.instance_exec(value, &l) }
88
+ # when Conditionals::Value then filter
89
+ when ::Proc
90
+ if filter.arity > 1
91
+ return lambda { |target, _, &block|
92
+ raise ArgumentError unless block
93
+ target.instance_exec(target, block, &filter)
94
+ }
95
+ end
96
+
97
+ if filter.arity <= 0
98
+ lambda { |target, _| target.instance_exec(&filter) }
99
+ else
100
+ lambda { |target, _| target.instance_exec(target, &filter) }
101
+ end
102
+ else
103
+ scopes = Array(chain_config[:scope])
104
+ method_to_call = scopes.map{ |s| public_send(s) }.join("_")
105
+
106
+ lambda { |target, _, &blk|
107
+ filter.public_send method_to_call, target, &blk
108
+ }
109
+ end
110
+ end
111
+
112
+ # From https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L410
113
+ def compute_identifier(filter)
114
+ case filter
115
+ when String, ::Proc
116
+ filter.object_id
117
+ else
118
+ filter
119
+ end
120
+ end
121
+
122
+ # From https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L419
123
+ def conditions_lambdas
124
+ @if.map { |c| make_lambda c } +
125
+ @unless.map { |c| invert_lambda make_lambda c }
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,137 @@
1
+ require 'workflow/state'
2
+ require 'workflow/event'
3
+ require 'workflow/errors'
4
+ require 'active_support/callbacks'
5
+
6
+ module Workflow
7
+ # Metadata object describing available states and state transitions.
8
+ class Specification
9
+ include ActiveSupport::Callbacks
10
+
11
+ # The state objects defined for this specification, keyed by name
12
+ # @return [Hash]
13
+ attr_reader :states
14
+
15
+ # State object to be given to newly created objects under this workflow.
16
+ # @return [State]
17
+ attr_reader :initial_state
18
+
19
+ # Optional metadata stored with this workflow specification
20
+ # @return [Hash]
21
+ attr_reader :meta
22
+
23
+ # List of symbols, for attribute accessors to be added to {TransitionContext} object
24
+ # @return [Array]
25
+ attr_reader :named_arguments
26
+
27
+ define_callbacks :spec_definition
28
+ set_callback(:spec_definition, :after, if: :define_revert_events?) do |spec|
29
+ spec.states.each do |state|
30
+ state.events.reject{ |e|
31
+ e.name.to_s =~ /^revert_/
32
+ }.select{|e| e.transitions.length == 1}.each do |event|
33
+ revert_event_name = "revert_#{event.name}".to_sym
34
+ from_state_for_revert = event.transitions.first.target_state
35
+ from_state_for_revert.on revert_event_name, to: state
36
+ end
37
+ end
38
+ end
39
+
40
+ set_callback(:spec_definition, :after) do |spec|
41
+ spec.states.each do |state|
42
+ state.events.each do |event|
43
+ event.transitions.each do |transition|
44
+ target_state = spec.find_state(transition.target_state)
45
+ unless target_state.present?
46
+ raise Workflow::Errors::WorkflowDefinitionError.new("Event #{event.name} transitions to #{transition.target_state} but there is no such state.")
47
+ end
48
+ transition.target_state = target_state
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def find_state(name)
55
+ states.find{|t| t.name == name.to_sym}
56
+ end
57
+
58
+
59
+
60
+
61
+ # @api private
62
+ #
63
+ # @param [Hash] meta Metadata
64
+ # @yield [] Block for workflow definition
65
+ # @return [Specification]
66
+ def initialize(meta = {}, &specification)
67
+ @states = []
68
+ @meta = meta
69
+ run_callbacks :spec_definition do
70
+ instance_eval(&specification)
71
+ end
72
+ end
73
+
74
+ # Define a new state named [name]
75
+ #
76
+ # @param [Symbol] name name of state
77
+ # @param [Hash] meta Metadata to be stored with the state within the {Specification} object
78
+ # @yield [] block defining events for this state.
79
+ # @return [nil]
80
+ def state(name, meta: {}, &events)
81
+ name = name.to_sym
82
+ new_state = Workflow::State.new(name, self, meta)
83
+ @initial_state ||= new_state
84
+ @states << new_state
85
+ new_state.instance_eval(&events) if block_given?
86
+ end
87
+
88
+
89
+
90
+ # Specify attributes to make available on the {TransitionContext} object
91
+ # during transitions taking place in this specification.
92
+ # The attributes' values will be taken in order from the arguments passed to
93
+ # the event transit method call.
94
+ #
95
+ # @param [Array] names A list of symbols
96
+ # @return [nil]
97
+ def event_args(*names)
98
+ @named_arguments = names
99
+ end
100
+
101
+
102
+ # Also create additional event transitions that will move each configured transition
103
+ # in the reverse direction.
104
+ #
105
+ # @return [nil]
106
+ #
107
+ #```ruby
108
+ # class Article
109
+ # include Workflow
110
+ # workflow do
111
+ # define_revert_events!
112
+ # state :foo do
113
+ # event :bar, transitions_to: :bax
114
+ # end
115
+ # state :bax
116
+ # end
117
+ # end
118
+ #
119
+ # a = Article.new
120
+ # a.process_event! :foo
121
+ # a.current_state.name # => :bax
122
+ # a.process_event! :revert_bar
123
+ # a.current_state.name # => :foo
124
+ #```
125
+ def define_revert_events!
126
+ @define_revert_events = true
127
+ end
128
+
129
+ private
130
+
131
+
132
+ def define_revert_events?
133
+ !!@define_revert_events
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,88 @@
1
+ module Workflow
2
+ class State
3
+ include Comparable
4
+
5
+ attr_accessor :name, :events, :meta, :on_entry, :on_exit
6
+ attr_reader :sequence
7
+
8
+ def initialize(name, sequence, **meta)
9
+ @name, @sequence, @events, @meta = name.to_sym, sequence, [], meta
10
+ end
11
+
12
+ def find_event(name)
13
+ events.find{|t| t.name == name}
14
+ end
15
+
16
+ # Define an event on this specification.
17
+ # Must be called within the scope of the block within a call to {#state}.
18
+ #
19
+ # @param [Symbol] name The name of the event
20
+ # @param [Hash] args
21
+ # @option args [Symbol] :transitions_to The state this event transitions to.
22
+ # @option args [Symbol] :if optional instance method name or [Proc] that will receive the object when called.
23
+ # @option args [Hash] :meta Optional metadata to be stored on the event object
24
+ # @return [nil]
25
+ #
26
+ #```ruby
27
+ #workflow do
28
+ # state :new do
29
+ # on :review, to: :being_reviewed
30
+ #
31
+ # on :submit do
32
+ # to :submitted,
33
+ # if: [ "name == 'The Dude'", :abides?, -> (rug) {rug.tied_the_room_together?}],
34
+ # unless: :nihilist?
35
+ #
36
+ # to :trash, unless: :body?
37
+ # to :another_place do |article|
38
+ # article.foo?
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # state :kitchen
44
+ # state :the_bar
45
+ # state :the_diner
46
+ #end
47
+ #```
48
+ def on(name, to: nil, meta: nil, &transitions)
49
+ if to && block_given?
50
+ raise Errors::WorkflowDefinitionError.new("Event target can only be received in the method call or the block, not both.")
51
+ end
52
+
53
+ unless to || block_given?
54
+ raise Errors::WorkflowDefinitionError.new("No event target given for event #{name}")
55
+ end
56
+
57
+ if find_event(name)
58
+ raise Errors::WorkflowDefinitionError.new("Already defined an event [#{name}] for state[#{self.name}]")
59
+ end
60
+
61
+ event = Workflow::Event.new(name, meta)
62
+
63
+ if to
64
+ event.to to
65
+ else
66
+ event.instance_eval(&transitions)
67
+ end
68
+
69
+ if event.transitions.empty?
70
+ raise Errors::WorkflowDefinitionError.new("No transitions defined for event [#{name}] on state [#{self.name}]")
71
+ end
72
+
73
+ events << event
74
+ nil
75
+ end
76
+
77
+ def inspect
78
+ "<State name=#{name.inspect} events(#{events.length})=#{events.inspect}>"
79
+ end
80
+
81
+ def <=>(other_state)
82
+ unless other_state.is_a?(State)
83
+ raise StandardError.new "Other State #{other_state} is a #{other_state.class}. I can only be compared with a Workflow::State."
84
+ end
85
+ self.sequence <=> other_state.sequence
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,54 @@
1
+ module Workflow
2
+ # During transitions, an instance of this class can be found
3
+ # on the object as `transition_context`.
4
+ # Contains metadata related to the current transition underway.
5
+ #
6
+ # == To name parameters:
7
+ #
8
+ # During workflow definition, do the following:
9
+ #
10
+ # before_transition :transition_handler
11
+ #
12
+ # def transition_handler
13
+ # transition_context.name1 # will equal 1
14
+ # transition_context.name2 # will equal 2
15
+ # transition_context.name3 # will equal 3
16
+ # end
17
+ #
18
+ # workflow do
19
+ # event_args :name1, :name2, :name3
20
+ # state :foo do
21
+ # event :bar, transitions_to: :bax
22
+ # end
23
+ # end
24
+ #
25
+ # Then later call:
26
+ #
27
+ # my_obj.submit! 1, 2, 3
28
+ #
29
+ # The entire list of passed parameters will still be available on +event_args+.
30
+ # If you pass fewer parameters, the later ones will simply be nil.
31
+ class TransitionContext
32
+ attr_reader :from, :to, :event, :event_args, :attributes, :named_arguments
33
+ def initialize(from:, to:, event:, event_args:, attributes:, named_arguments: [])
34
+ @from = from
35
+ @to = to
36
+ @event = event
37
+ @event_args = event_args
38
+ @attributes = attributes
39
+ @named_arguments = (named_arguments || []).zip(event_args).to_h
40
+ end
41
+
42
+ def values
43
+ [from, to, event, event_args]
44
+ end
45
+
46
+ def method_missing(method, *args)
47
+ if named_arguments.key?(method)
48
+ named_arguments[method]
49
+ else
50
+ super
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module Workflow
2
+ VERSION = "1.4.0"
3
+ end