rails-workflow 1.4.5.4 → 1.4.6.4

Sign up to get free protection for your applications and to get access to all the features.
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