has_state_machine 0.1.0 → 0.3.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 180ea0ce6e3e597405acc08441fe8ee1c0f530ccfd176950e8683196e9232695
4
- data.tar.gz: 97944638a358883f302463d830d506149fe169d9cf3a3c20d7ccd615724a4932
3
+ metadata.gz: 16d6a43019c37f17fc0efac4e344f1e9914b0cb923a439d1e799d690018dd0e6
4
+ data.tar.gz: 8bd1d5d4f6b72dcb9931ccbd0987b0fe2d4970e2476867fe930cf21a9b5d0da2
5
5
  SHA512:
6
- metadata.gz: c4e20717ca8de688ea7c766da83412b7bf0121d06d2cace551dfd1b5210632fd9840ca34d7293381431b2ff4afc27459a33bfd2711ef537ee3ca39c8598d8098
7
- data.tar.gz: 973704bfb4403dae0b5ff00a3fe3b2d0d6d84f96a5e706bd0dea8cda113bd7dbfc7bac97ce84ae310b4fb6613f287f257a1dfedea41794e06f7afbd4b5af3834
6
+ metadata.gz: 7c7bb13405d5f0ee66af21594e0e0633e7c5f746ea7b038e2c28f4d53670b138b702bbd85737eadf952cb2871a234374597a10279f6fd0b026d7465b3721de79
7
+ data.tar.gz: ecb5c2c8ad0f00d1eb1c3ddd709598aa8ac29210dbaffe722461ba02bd8c7bc5c328ff7c4e90bbd8fdae8cbea91d5efe50d79b2874ed84035bc6bdf9d5a42421
data/README.md CHANGED
@@ -3,8 +3,15 @@
3
3
  [![Build Status](https://github.com/bharget/has_state_machine/workflows/Tests/badge.svg)](https://github.com/bharget/has_state_machine/actions)
4
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
5
 
6
- ## Usage
7
- How to use my plugin.
6
+ HasStateMachine uses ruby classes to make creating a finite state machine for your ActiveRecord models a breeze.
7
+
8
+ ## Contents
9
+
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Changelog](https://github.com/encampment/has_state_machine/blob/master/CHANGELOG.md)
13
+ - [Contributing](#contributing)
14
+ - [License](#license)
8
15
 
9
16
  ## Installation
10
17
  Add this line to your application's Gemfile:
@@ -23,8 +30,104 @@ Or install it yourself as:
23
30
  $ gem install has_state_machine
24
31
  ```
25
32
 
33
+ ## Usage
34
+
35
+ You must first use the `has_state_machine` macro to define your state machine at
36
+ a high level. This includes defining the possible states for your object as well
37
+ as some optional configuration should you want to change the default behavior of
38
+ the state machine.
39
+ ```ruby
40
+ # By default, it is assumed that the "state" of the object is
41
+ # stored in a string column named "status".
42
+ class Post < ApplicationRecord
43
+ has_state_machine states: %i[draft published archived]
44
+ end
45
+ ```
46
+
47
+ Now you must define the classes for the states in your state machine. By default,
48
+ `HasStateMachine` assumes that these will be under the `Workflow` namespace following
49
+ the pattern of `Workflow::#{ObjectClass}::#{State}`. The state objects must inherit
50
+ from `HasStateMachine::State`.
51
+
52
+ ```ruby
53
+ module Workflow
54
+ class Post::Draft < HasStateMachine::State
55
+ # Define the possible transitions from the "draft" state
56
+ transitions_to %i[published archived]
57
+ end
58
+ end
59
+
60
+ module Workflow
61
+ class Post::Published < HasStateMachine::State
62
+ transitions_to %i[archived]
63
+
64
+ # Custom validations can be added to the state to ensure a transition is "valid"
65
+ validate :title_exists?
66
+
67
+ def title_exists?
68
+ return if object.title.present?
69
+
70
+ # Errors get added to the ActiveRecord object
71
+ errors.add(:title, "can't be blank")
72
+ end
73
+ end
74
+ end
75
+
76
+ module Workflow
77
+ class Post::Archived < HasStateMachine::State
78
+ # There are callbacks for running logic before and after
79
+ # a transition occurs.
80
+ before_transition do
81
+ Rails.logger.info "== Post is being archived ==\n"
82
+ end
83
+
84
+ after_transition do
85
+ Rails.logger.info "== Post has been archived ==\n"
86
+
87
+ # You can access the previous state of the object in
88
+ # after_transition callbacks as well.
89
+ Rails.logger.info "== Transitioned from #{previous_state} ==\n"
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ Some examples:
96
+
97
+ ```ruby
98
+ post = Post.create(status: "draft")
99
+
100
+ post.status.transition_to(:published) # => false
101
+ post.status # => "draft"
102
+
103
+ post.title = "Foobar"
104
+ post.status.transition_to(:published) # => true
105
+ post.status # => "published"
106
+
107
+ post.status.transition_to(:archived)
108
+ # == Post is being archived ==
109
+ # == Post has been archived ==
110
+ # == Transitioned from published ==
111
+ # => true
112
+ ```
113
+
26
114
  ## Contributing
27
- Contribution directions go here.
115
+
116
+ Anyone is encouraged to help improve this project. Here are a few ways you can help:
117
+
118
+ - [Report bugs](https://github.com/encampment/has_state_machine/issues)
119
+ - Fix bugs and [submit pull requests](https://github.com/encampment/has_state_machine/pulls)
120
+ - Write, clarify, or fix documentation
121
+ - Suggest or add new features
122
+
123
+ To get started with development:
124
+
125
+ ```
126
+ git clone https://github.com/encampment/has_state_machine.git
127
+ cd has_state_machine
128
+ bundle install
129
+ bundle exec rake test
130
+ ```
28
131
 
29
132
  ## License
30
133
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -36,7 +36,6 @@ module HasStateMachine
36
36
  def define_helper_methods(states:, options:)
37
37
  ##
38
38
  # The list of possible states in the state machine.
39
- # Can be overwritten to use a different column name.
40
39
  define_singleton_method "workflow_states" do
41
40
  states
42
41
  end
@@ -57,7 +56,7 @@ module HasStateMachine
57
56
 
58
57
  ##
59
58
  # Determines whether or not the state validations should be run
60
- # as part of the object validations.
59
+ # as part of the object validations; they are by default.
61
60
  define_singleton_method "state_validations_on_object?" do
62
61
  return true unless options.key?(:state_validations_on_object)
63
62
 
@@ -8,7 +8,7 @@ module HasStateMachine
8
8
  extend ActiveModel::Callbacks
9
9
  include ActiveModel::Validations
10
10
 
11
- attr_reader :object, :state, :options
11
+ attr_reader :object, :state
12
12
 
13
13
  ##
14
14
  # Defines the before_transition and after_transition callbacks
@@ -28,8 +28,7 @@ module HasStateMachine
28
28
  # Initializes the HasStateMachine::State instance.
29
29
  #
30
30
  # @example
31
- # state = HasStateMachine::State.new(post) #=> "draft"
32
- # state.class #=> Workflow::Post::Draft
31
+ # state = Workflow::Post::Draft.new(post) #=> "draft"
33
32
  def initialize(object)
34
33
  @object = object
35
34
 
@@ -47,11 +46,15 @@ module HasStateMachine
47
46
  #
48
47
  # @return [Boolean] whether or not the transition took place
49
48
  def transition_to(desired_state, **options)
49
+ transitioned = false
50
+
50
51
  with_transition_options(options) do
51
52
  return false unless valid_transition?(desired_state.to_s)
52
53
 
53
- state_instance(desired_state.to_s).perform_transition!
54
+ transitioned = state_instance(desired_state.to_s).perform_transition!
54
55
  end
56
+
57
+ transitioned
55
58
  end
56
59
 
57
60
  ##
@@ -72,13 +75,21 @@ module HasStateMachine
72
75
  possible_transitions.include? desired_state
73
76
  end
74
77
 
78
+ ##
79
+ # Helper method for grabbing the previous state of the object after
80
+ # it has been transitioned to the new state. Useful in
81
+ # after_transition blocks
82
+ def previous_state
83
+ object.previous_changes[object.state_attribute]&.first
84
+ end
85
+
75
86
  def state_instance(desired_state)
76
87
  klass = "#{object.workflow_namespace}::#{desired_state.to_s.classify}".safe_constantize
77
88
  klass&.new(object)
78
89
  end
79
90
 
80
91
  def valid_transition?(desired_state)
81
- return true if options[:skip_validations]
92
+ return true if object.skip_state_validations
82
93
 
83
94
  object.valid? &&
84
95
  can_transition?(desired_state) &&
@@ -86,7 +97,6 @@ module HasStateMachine
86
97
  end
87
98
 
88
99
  def with_transition_options(options, &block)
89
- @options = options
90
100
  object.skip_state_validations = options[:skip_validations]
91
101
  yield
92
102
  object.skip_state_validations = false
@@ -5,6 +5,10 @@ module HasStateMachine
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
+ ##
9
+ # Sometimes you may want to skip the validations defined on
10
+ # the state when validating your object; set this accessor
11
+ # to true to do so.
8
12
  attr_accessor :skip_state_validations
9
13
 
10
14
  delegate \
@@ -20,8 +24,9 @@ module HasStateMachine
20
24
  attribute state_attribute, :string, default: initial_state
21
25
 
22
26
  ##
23
- # Validating any changes to the status attribute are represented by
24
- # classes within the state machine and have a valid HasStateMachine::State class.
27
+ # Validate that the current state is a possible state, that there is a
28
+ # state class defined for it, and run the validations from the state
29
+ # class instance if need be.
25
30
  validates state_attribute, inclusion: {in: workflow_states}, presence: true
26
31
  validate :state_class_defined?
27
32
  validate :state_instance_validations, if: :should_validate_state?
@@ -44,13 +49,15 @@ module HasStateMachine
44
49
 
45
50
  workflow_states.each do |state|
46
51
  ##
47
- # Defines scopes based on the state machine possible states
52
+ # Defines scopes based on the state machine's possible states
48
53
  #
49
54
  # @return [ActiveRecord_Relation]
50
55
  # @example Retreiving a users published posts
51
56
  # > Post.published.where(user: user)
52
57
  # #=> [#<Post>]
53
- scope state, -> { where("#{state_attribute} = ?", state) }
58
+ if defined?(ActiveRecord) && (self < ActiveRecord::Base)
59
+ scope state, -> { where("#{state_attribute} = ?", state) }
60
+ end
54
61
 
55
62
  ##
56
63
  # Defines boolean helpers to determine if the active state matches
@@ -71,7 +78,7 @@ module HasStateMachine
71
78
  # Getter for the current state of the model based on the configured state
72
79
  # attribute.
73
80
  def current_state
74
- self[state_attribute]
81
+ attributes.with_indifferent_access[state_attribute]
75
82
  end
76
83
 
77
84
  ##
@@ -102,8 +109,8 @@ module HasStateMachine
102
109
  end
103
110
 
104
111
  ##
105
- # Runs the validations defined on the current HasStateMachine::State when calling
106
- # model.valid?
112
+ # Run the validations defined on the current HasStateMachine::State. Errors found there
113
+ # should be added to this object.
107
114
  def state_instance_validations
108
115
  return unless state_class.present?
109
116
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HasStateMachine
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Hargett
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-10-22 00:00:00.000000000 Z
12
+ date: 2021-01-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -133,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
133
133
  - !ruby/object:Gem::Version
134
134
  version: '0'
135
135
  requirements: []
136
- rubygems_version: 3.0.3
136
+ rubygems_version: 3.1.4
137
137
  signing_key:
138
138
  specification_version: 4
139
139
  summary: Class based state machine for ActiveRecord models.