rails-workflow 1.4.5.4 → 1.4.6.4

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +23 -0
  4. data/Gemfile +2 -1
  5. data/Rakefile +4 -4
  6. data/bin/console +3 -3
  7. data/lib/active_support/overloads.rb +13 -6
  8. data/lib/workflow.rb +12 -279
  9. data/lib/workflow/adapters/active_record.rb +57 -50
  10. data/lib/workflow/adapters/active_record_validations.rb +25 -19
  11. data/lib/workflow/adapters/adapter.rb +23 -0
  12. data/lib/workflow/adapters/remodel.rb +8 -9
  13. data/lib/workflow/callbacks.rb +60 -45
  14. data/lib/workflow/callbacks/callback.rb +23 -37
  15. data/lib/workflow/callbacks/method_callback.rb +12 -0
  16. data/lib/workflow/callbacks/proc_callback.rb +23 -0
  17. data/lib/workflow/callbacks/string_callback.rb +12 -0
  18. data/lib/workflow/callbacks/transition_callback.rb +88 -78
  19. data/lib/workflow/callbacks/transition_callbacks/method_caller.rb +53 -0
  20. data/lib/workflow/callbacks/transition_callbacks/proc_caller.rb +60 -0
  21. data/lib/workflow/configuration.rb +1 -0
  22. data/lib/workflow/definition.rb +73 -0
  23. data/lib/workflow/errors.rb +37 -6
  24. data/lib/workflow/event.rb +30 -15
  25. data/lib/workflow/helper_method_configurator.rb +100 -0
  26. data/lib/workflow/specification.rb +45 -22
  27. data/lib/workflow/state.rb +45 -36
  28. data/lib/workflow/transition_context.rb +5 -4
  29. data/lib/workflow/transitions.rb +94 -0
  30. data/lib/workflow/version.rb +2 -1
  31. data/rails-workflow.gemspec +18 -18
  32. data/tags.markdown +31 -0
  33. metadata +13 -5
  34. data/lib/workflow/callbacks/transition_callbacks/method_wrapper.rb +0 -102
  35. data/lib/workflow/callbacks/transition_callbacks/proc_wrapper.rb +0 -48
  36. data/lib/workflow/draw.rb +0 -79
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Workflow
2
3
  # Represents one state for the defined workflow,
3
4
  # with a list of {Workflow::Event}s that can transition to
@@ -11,35 +12,45 @@ module Workflow
11
12
  # @return [Array] Array of {Workflow::Event}s defined for this state.
12
13
  # @!attribute [r] meta
13
14
  # @return [Hash] Extra information defined for this state.
14
- attr_reader :name, :events, :meta
15
+ # @!attribute [r] tags
16
+ # @return [Array] Tags for this state.
17
+ attr_reader :name, :events, :meta, :tags
15
18
 
16
19
  # @api private
17
20
  # For creating {Workflow::State} objects please see {Specification#state}
18
- # @param [Symbol] name The name of the state being created. Should be unique within its workflow.
19
- # @param [Fixnum] sequence Sequencing number that will affect sorting comparisons with other states.
20
- # @param [Hash] meta: Optional metadata for this state.
21
- def initialize(name, sequence, meta: {})
22
- @name, @sequence, @events, @meta = name.to_sym, sequence, [], meta
21
+ # @param [Symbol] name Name of the state being created. Must be unique within its workflow.
22
+ # @param [Fixnum] sequence Sort location among states on this workflow.
23
+ # @param [Hash] meta Optional metadata for this state.
24
+ def initialize(name, sequence, tags: [], meta: {})
25
+ @name = name.to_sym
26
+ @sequence = sequence
27
+ @events = []
28
+ @meta = meta
29
+ @tags = [tags].flatten
30
+ unless @tags.reject { |t| t.is_a? Symbol }
31
+ raise WorkflowDefinitionError, "Tags can only include symbols, state: #{name}"
32
+ end
23
33
  end
24
34
 
25
35
  # Returns the event with the given name.
26
36
  # @param [Symbol] name name of event to find
27
37
  # @return [Workflow::Event] The event with the given name, or `nil`
28
38
  def find_event(name)
29
- events.find{|t| t.name == name}
39
+ events.find { |t| t.name == name }
30
40
  end
31
41
 
32
42
  # Define an event on this specification.
33
- # Must be called within the scope of the block within a call to {#state}.
43
+ # Must be called within the scope of the block within a call to {Workflow::Specification#state}.
34
44
  #
35
45
  # @param [Symbol] name The name of the event
36
- # @param [Symbol] to: Optional name of {Workflow::State} this event will transition to. Must be omitted if a block is provided.
37
- # @param [Hash] meta: Optional hash of metadata to be stored on the event object.
46
+ # @param [Symbol] to Optional name of {Workflow::State} this event will transition to.
47
+ # Must be omitted if a block is provided.
48
+ # @param [Hash] meta Optional hash of metadata to be stored on the event object.
38
49
  # @yield [] Transitions definition for this event.
39
50
  # @return [nil]
40
51
  #
41
- #```ruby
42
- #workflow do
52
+ # ```ruby
53
+ # workflow do
43
54
  # state :new do
44
55
  # on :review, to: :being_reviewed
45
56
  #
@@ -58,21 +69,10 @@ module Workflow
58
69
  # state :kitchen
59
70
  # state :the_bar
60
71
  # state :the_diner
61
- #end
62
- #```
72
+ # end
73
+ # ```
63
74
  def on(name, to: nil, meta: {}, &transitions)
64
- if to && block_given?
65
- raise Errors::WorkflowDefinitionError.new("Event target can only be received in the method call or the block, not both.")
66
- end
67
-
68
- unless to || block_given?
69
- raise Errors::WorkflowDefinitionError.new("No event target given for event #{name}")
70
- end
71
-
72
- if find_event(name)
73
- raise Errors::WorkflowDefinitionError.new("Already defined an event [#{name}] for state[#{self.name}]")
74
- end
75
-
75
+ check_can_add_transition!(name, to: to, &transitions)
76
76
  event = Workflow::Event.new(name, meta: meta)
77
77
 
78
78
  if to
@@ -81,14 +81,24 @@ module Workflow
81
81
  event.instance_eval(&transitions)
82
82
  end
83
83
 
84
- if event.transitions.empty?
85
- raise Errors::WorkflowDefinitionError.new("No transitions defined for event [#{name}] on state [#{self.name}]")
84
+ unless event.valid?
85
+ raise Errors::NoTransitionsDefinedError.new(self, event)
86
86
  end
87
87
 
88
88
  events << event
89
89
  nil
90
90
  end
91
91
 
92
+ private def check_can_add_transition!(name, to: nil)
93
+ raise Errors::DualEventDefinitionError if to && block_given?
94
+
95
+ unless to || block_given?
96
+ raise Errors::WorkflowDefinitionError, "No event target given for event #{name}"
97
+ end
98
+
99
+ raise Errors::EventNameCollisionError.new(self, name) if find_event(name)
100
+ end
101
+
92
102
  # @return [String] String representation of object
93
103
  def inspect
94
104
  "<State name=#{name.inspect} events(#{events.length})=#{events.inspect}>"
@@ -97,20 +107,19 @@ module Workflow
97
107
  # Overloaded comparison operator. Workflow states are sorted according to the order
98
108
  # in which they were defined.
99
109
  #
100
- # @param [Workflow::State] other_state state to be compared against.
110
+ # @param [Workflow::State] other state to be compared against.
101
111
  # @return [Integer]
102
- def <=>(other_state)
103
- unless other_state.is_a?(State)
104
- raise StandardError.new "Other State #{other_state} is a #{other_state.class}. I can only be compared with a Workflow::State."
105
- end
106
- self.sequence <=> other_state.send(:sequence)
112
+ def <=>(other)
113
+ raise Errors::StateComparisonError, other unless other.is_a?(State)
114
+ sequence <=> other.send(:sequence)
107
115
  end
108
116
 
109
117
  private
118
+
110
119
  # @api private
111
120
  # @!attribute [r] sequence
112
- # @return [Fixnum] The position of this state within the order it was defined for in its workflow.
121
+ # @return [Fixnum] The position of this state within the
122
+ # order it was defined for in its workflow.
113
123
  attr_reader :sequence
114
-
115
124
  end
116
125
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Workflow
2
3
  # During transitions, an instance of this class can be found
3
4
  # on the object as `transition_context`.
@@ -30,20 +31,20 @@ module Workflow
30
31
  # If you pass fewer parameters, the later ones will simply be nil.
31
32
  class TransitionContext
32
33
  attr_reader :from, :to, :event, :event_args, :attributes, :named_arguments
33
- def initialize(from:, to:, event:, event_args:, attributes:, named_arguments: [])
34
+ def initialize(from:, to:, event:, event_args:, **args)
34
35
  @from = from
35
36
  @to = to
36
37
  @event = event
37
38
  @event_args = event_args
38
- @attributes = attributes
39
- @named_arguments = (named_arguments || []).zip(event_args).to_h
39
+ @attributes = args[:attributes] || {}
40
+ @named_arguments = (args[:named_arguments] || []).zip(event_args).to_h
40
41
  end
41
42
 
42
43
  def values
43
44
  [from, to, event, event_args.dup, attributes.dup]
44
45
  end
45
46
 
46
- def respond_to?(method)
47
+ def respond_to_missing?(method, _include_private = false)
47
48
  named_arguments.key?(method) || super
48
49
  end
49
50
 
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+ module Workflow
3
+ module Transitions
4
+ extend ActiveSupport::Concern
5
+
6
+ # @api private
7
+ # @!attribute [r] transition_context
8
+ # @return [Workflow::TransitionContext] During transition, or nil if no transition is underway.
9
+ # During a state transition, contains transition-specific information:
10
+ # * The name of the {Workflow::State} being exited,
11
+ # * The name of the {Workflow::State} being entered,
12
+ # * The name of the {Workflow::Event} that was fired,
13
+ # * And whatever arguments were passed to the {Workflow#transition!} method.
14
+ included do
15
+ attr_reader :transition_context
16
+ end
17
+
18
+ # Initiates state transition via the named event
19
+ #
20
+ # @param [Symbol] name name of event to initiate
21
+ # @param [Array] args State transition arguments.
22
+ # @return [Symbol] The name of the new state, or `false` if the transition failed.
23
+ # TODO: connect args to documentation on how arguments are accessed during state transitions.
24
+ def transition!(name, *args, **attributes)
25
+ @transition_context = prepare_transition(name, args, attributes)
26
+
27
+ run_all_callbacks do
28
+ persist_workflow_state(@transition_context.to)
29
+ end
30
+ ensure
31
+ @transition_context = nil
32
+ end
33
+
34
+ # Stop the current transition and set the reason for the abort.
35
+ #
36
+ # @param [String] reason Optional reason for halting transition.
37
+ # @return [nil]
38
+ def halt(reason = nil)
39
+ @halted_because = reason
40
+ @halted = true
41
+ throw :abort
42
+ end
43
+
44
+ # Sets halt reason and raises [TransitionHaltedError] error.
45
+ #
46
+ # @param [String] reason Optional reason for halting
47
+ # @return [nil]
48
+ def halt!(reason = nil)
49
+ @halted_because = reason
50
+ @halted = true
51
+ raise Errors::TransitionHaltedError, reason
52
+ end
53
+
54
+ # Deprecated. Check for false return value from {#transition!}
55
+ # @return [Boolean] true if the last transition was halted by one of the transition callbacks.
56
+ def halted?
57
+ @halted
58
+ end
59
+
60
+ # Returns the reason given to a call to {#halt} or {#halt!}, if any.
61
+ # @return [String] The reason the transition was aborted.
62
+ attr_reader :halted_because
63
+
64
+ # load_workflow_state and persist_workflow_state
65
+ # can be overriden to handle the persistence of the workflow state.
66
+ #
67
+ # Default (non ActiveRecord) implementation stores the current state
68
+ # in a variable.
69
+ #
70
+ # Default ActiveRecord implementation uses a 'workflow_state' database column.
71
+ def load_workflow_state
72
+ @workflow_state if instance_variable_defined? :@workflow_state
73
+ end
74
+
75
+ def persist_workflow_state(new_value)
76
+ @workflow_state = new_value
77
+ end
78
+
79
+ def prepare_transition(name, args, attributes)
80
+ event = current_state.find_event(name.to_sym)
81
+ raise Errors::NoTransitionAllowed.new(current_state, name) unless event
82
+
83
+ target = event.evaluate(self)
84
+
85
+ TransitionContext.new \
86
+ from: current_state.name,
87
+ to: target.name,
88
+ event: event.name,
89
+ event_args: args,
90
+ attributes: attributes,
91
+ named_arguments: workflow_spec.named_arguments
92
+ end
93
+ end
94
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Workflow
2
- VERSION = "1.4.5.4"
3
+ VERSION = '1.4.6.4'
3
4
  end
@@ -1,41 +1,42 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'workflow/version'
5
6
  Gem::Specification.new do |spec|
6
- spec.name = "rails-workflow"
7
+ spec.name = 'rails-workflow'
7
8
  spec.version = Workflow::VERSION
8
- spec.authors = ["Tyler Gannon"]
9
- spec.email = ["tyler@aprilseven.co"]
9
+ spec.authors = ['Tyler Gannon']
10
+ spec.email = ['tyler@aprilseven.co']
10
11
 
11
- spec.summary = %q{A finite-state-machine-inspired API for managing state changes in ActiveModel objects. Based on Vladimir Dobriakov's Workflow gem (https://github.com/geekq/workflow)}
12
- spec.description = %q{Workflow specifically for ActiveModel objects.}
13
- spec.homepage = "https://tylergannon.github.io/rails-workflow/"
14
- spec.license = "MIT"
12
+ spec.summary = "A finite-state-machine-inspired API for managing state changes in ActiveModel objects. Based on Vladimir Dobriakov's Workflow gem (https://github.com/geekq/workflow)"
13
+ spec.description = 'Workflow specifically for ActiveModel objects.'
14
+ spec.homepage = 'https://tylergannon.github.io/rails-workflow/'
15
+ spec.license = 'MIT'
15
16
 
16
17
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
18
  # to allow pushing to a single host or delete this section to allow pushing to any host.
18
19
  if spec.respond_to?(:metadata)
19
- spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
21
  else
21
- raise "RubyGems 2.0 or newer is required to protect against " \
22
- "public gem pushes."
22
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
23
+ 'public gem pushes.'
23
24
  end
24
25
 
25
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
27
  f.match(%r{^(test|spec|features|doc)/})
27
28
  end
28
- spec.bindir = "exe"
29
+ spec.bindir = 'exe'
29
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
31
+ spec.require_paths = ['lib']
31
32
  spec.extra_rdoc_files = [
32
- "README.markdown"
33
+ 'README.markdown'
33
34
  ]
34
35
 
35
36
  spec.add_dependency 'activerecord', '~> 5.0'
36
37
  spec.add_dependency 'activesupport', '~> 5.0'
37
38
 
38
- spec.add_development_dependency 'rdoc', [">= 3.12"]
39
+ spec.add_development_dependency 'rdoc', ['>= 3.12']
39
40
  spec.add_development_dependency 'sqlite3'
40
41
  spec.add_development_dependency 'mocha'
41
42
  spec.add_development_dependency 'yard'
@@ -44,8 +45,7 @@ Gem::Specification.new do |spec|
44
45
  spec.add_development_dependency 'rake'
45
46
  spec.add_development_dependency 'test-unit'
46
47
  spec.add_development_dependency 'ruby-graphviz', ['~> 1.0.0']
47
- spec.add_development_dependency "bundler", "~> 1.13"
48
- spec.add_development_dependency "rspec", "~> 3.0"
48
+ spec.add_development_dependency 'bundler', '~> 1.13'
49
+ spec.add_development_dependency 'rspec', '~> 3.0'
49
50
  spec.required_ruby_version = '>= 2.3'
50
-
51
51
  end
data/tags.markdown ADDED
@@ -0,0 +1,31 @@
1
+ ---
2
+ layout: page
3
+ ---
4
+
5
+ # Tagging States
6
+
7
+ Place your workflow states into groups by tagging them.
8
+ Also note the helper methods `initial?` and `terminal?`.
9
+
10
+ ```ruby
11
+ class Foo
12
+ include Workflow
13
+ state :new, tags: :bar do
14
+ on :complete, to: :completed
15
+ end
16
+ state :completed, tags: [:awesome, :congrats]
17
+ end
18
+
19
+ a = Foo.new
20
+ a.current_state.bar?
21
+ # => true
22
+ a.current_state.initial?
23
+ # => true
24
+ a.awesome?
25
+ # => false
26
+ a.transition! :complete
27
+ a.terminal?
28
+ # => true
29
+ a.awesome?
30
+ # => true
31
+ ```
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-workflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.5.4
4
+ version: 1.4.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Gannon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-09-25 00:00:00.000000000 Z
11
+ date: 2016-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -201,6 +201,7 @@ extra_rdoc_files:
201
201
  - README.markdown
202
202
  files:
203
203
  - ".gitignore"
204
+ - ".rubocop.yml"
204
205
  - ".travis.yml"
205
206
  - ".yardopts"
206
207
  - Gemfile
@@ -219,22 +220,29 @@ files:
219
220
  - lib/workflow.rb
220
221
  - lib/workflow/adapters/active_record.rb
221
222
  - lib/workflow/adapters/active_record_validations.rb
223
+ - lib/workflow/adapters/adapter.rb
222
224
  - lib/workflow/adapters/remodel.rb
223
225
  - lib/workflow/callbacks.rb
224
226
  - lib/workflow/callbacks/callback.rb
227
+ - lib/workflow/callbacks/method_callback.rb
228
+ - lib/workflow/callbacks/proc_callback.rb
229
+ - lib/workflow/callbacks/string_callback.rb
225
230
  - lib/workflow/callbacks/transition_callback.rb
226
- - lib/workflow/callbacks/transition_callbacks/method_wrapper.rb
227
- - lib/workflow/callbacks/transition_callbacks/proc_wrapper.rb
231
+ - lib/workflow/callbacks/transition_callbacks/method_caller.rb
232
+ - lib/workflow/callbacks/transition_callbacks/proc_caller.rb
228
233
  - lib/workflow/configuration.rb
229
- - lib/workflow/draw.rb
234
+ - lib/workflow/definition.rb
230
235
  - lib/workflow/errors.rb
231
236
  - lib/workflow/event.rb
237
+ - lib/workflow/helper_method_configurator.rb
232
238
  - lib/workflow/specification.rb
233
239
  - lib/workflow/state.rb
234
240
  - lib/workflow/transition_context.rb
241
+ - lib/workflow/transitions.rb
235
242
  - lib/workflow/version.rb
236
243
  - orders_workflow.png
237
244
  - rails-workflow.gemspec
245
+ - tags.markdown
238
246
  homepage: https://tylergannon.github.io/rails-workflow/
239
247
  licenses:
240
248
  - MIT
@@ -1,102 +0,0 @@
1
- module Workflow
2
- module Callbacks
3
- module TransitionCallbacks
4
- # A {Workflow::Callbacks::TransitionCallback} that wraps an instance method
5
- # With arity != 0.
6
- # Because the wrapped method may not have been defined at the time the callback
7
- # is defined, the string representing the method call is built at runtime
8
- # rather than at compile time.
9
- class MethodWrapper < ::Workflow::Callbacks::TransitionCallback
10
- attr_reader :calling_class
11
-
12
- # Builds a proc object that will correctly call the {#raw_proc}
13
- # by inspecting its parameters and pulling arguments from the {Workflow::TransitionContext}
14
- # object for the transition.
15
- # Given an overloaded `==` operator so for {Workflow#skip_before_transition} and other
16
- # `skip_transition` calls.
17
- # @return [Type] description of returned object
18
- def wrapper
19
- cb_object = self
20
- proc_string = build_proc(<<-EOF)
21
- arguments = [
22
- cb_object.send(:raw_proc).inspect,
23
- cb_object.send(:name_arguments_string),
24
- cb_object.send(:rest_param_string),
25
- cb_object.send(:kw_arguments_string),
26
- cb_object.send(:keyrest_string),
27
- cb_object.send(:procedure_string)].compact.join(', ')
28
- target.instance_eval("send(\#{arguments})")
29
- EOF
30
- _wrapper = eval(proc_string)
31
- _wrapper.instance_exec(raw_proc, &OVERLOAD_EQUALITY_OPERATOR_PROC)
32
- _wrapper
33
- end
34
-
35
- private
36
-
37
- # A that is instanced_exec'd on a new proc object within {#wrapper}
38
- #
39
- # Enables comparison of two wrapper procs to determine if they wrap the same
40
- # Method.
41
- OVERLOAD_EQUALITY_OPERATOR_PROC = Proc.new do |method_name|
42
- def method_name
43
- method_name
44
- end
45
-
46
- # Equality operator overload.
47
- # If other is a {Symbol}, matches this object against {#method_name} defined above.
48
- # If other is a {Proc}:
49
- # * If it responds to {#method_name}, matches the method names of the two objects.
50
- # * Otherwise false
51
- #
52
- # @param [Symbol] other A method name to compare against.
53
- # @param [Proc] other A proc to compare against.
54
- # @return [Boolean] Whether the two should be considered equivalent
55
- def ==(other)
56
- case other
57
- when ::Proc
58
- if other.respond_to?(:raw_proc)
59
- self.method_name == other.method_name
60
- else
61
- false
62
- end
63
- when ::Symbol
64
- self.method_name == other
65
- else
66
- false
67
- end
68
- end
69
- end
70
-
71
- def name_arguments_string
72
- if name_params.any?
73
- name_params.map{|name|
74
- "name_proc.call(:#{name})"
75
- }.join(', ')
76
- end
77
- end
78
-
79
- def procedure_string
80
- '&callbacks' if around_callback?
81
- end
82
-
83
- # @return [UnboundMethod] Method representation from class {#calling_class}, named by {#raw_proc}
84
- def callback_method
85
- @meth ||= calling_class.instance_method(raw_proc)
86
- end
87
-
88
- # Parameter definition for the object. See {UnboundMethod#parameters}
89
- #
90
- # @return [Array] Parameters
91
- def parameters
92
- callback_method.parameters
93
- end
94
-
95
- # @return [Fixnum] Arity of the callback method
96
- def arity
97
- callback_method.arity
98
- end
99
- end
100
- end
101
- end
102
- end