has_state_machine 0.1.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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 180ea0ce6e3e597405acc08441fe8ee1c0f530ccfd176950e8683196e9232695
4
+ data.tar.gz: 97944638a358883f302463d830d506149fe169d9cf3a3c20d7ccd615724a4932
5
+ SHA512:
6
+ metadata.gz: c4e20717ca8de688ea7c766da83412b7bf0121d06d2cace551dfd1b5210632fd9840ca34d7293381431b2ff4afc27459a33bfd2711ef537ee3ca39c8598d8098
7
+ data.tar.gz: 973704bfb4403dae0b5ff00a3fe3b2d0d6d84f96a5e706bd0dea8cda113bd7dbfc7bac97ce84ae310b4fb6613f287f257a1dfedea41794e06f7afbd4b5af3834
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Benjamin Hargett
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # HasStateMachine
2
+
3
+ [![Build Status](https://github.com/bharget/has_state_machine/workflows/Tests/badge.svg)](https://github.com/bharget/has_state_machine/actions)
4
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+
6
+ ## Usage
7
+ How to use my plugin.
8
+
9
+ ## Installation
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'has_state_machine'
14
+ ```
15
+
16
+ And then execute:
17
+ ```bash
18
+ $ bundle
19
+ ```
20
+
21
+ Or install it yourself as:
22
+ ```bash
23
+ $ gem install has_state_machine
24
+ ```
25
+
26
+ ## Contributing
27
+ Contribution directions go here.
28
+
29
+ ## License
30
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ task default: %i[lint test]
6
+
7
+ namespace :test do
8
+ task :refresh do
9
+ sh "bin/appraisal clean"
10
+ sh "bin/appraisal generate"
11
+ end
12
+
13
+ task :all do
14
+ sh "bin/appraisal install"
15
+ sh "bin/appraisal bin/rake test"
16
+ end
17
+ end
18
+
19
+ task :test do
20
+ sh "bin/test"
21
+ end
22
+
23
+ task :lint do
24
+ sh "bin/standardrb --no-fix"
25
+ end
26
+
27
+ namespace :changelog do
28
+ task :refresh do
29
+ sh "bin/refresh_changelog"
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "has_state_machine/railtie"
4
+ require "has_state_machine/core_ext/string"
5
+ require "has_state_machine/definition"
6
+
7
+ module HasStateMachine
8
+ end
9
+
10
+ ActiveRecord::Base.include HasStateMachine::Definition if defined?(ActiveRecord)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ ##
5
+ # Adding our transition method to the default String class to prevent
6
+ # exceptions while transitioning from an invalid state. This method
7
+ # gets overwritten by valid HasStateMachine::State classes.
8
+ #
9
+ # @return [Boolean] false
10
+ #
11
+ # @example
12
+ # "some random string".transition_to("draft") #=> false
13
+ def transition_to(_desired_state)
14
+ false
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "has_state_machine/state"
4
+ require "has_state_machine/state_helpers"
5
+
6
+ module HasStateMachine
7
+ module Definition
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ ##
12
+ # Configures the state machine for the ActiveRecord object and adds some
13
+ # useful helper methods such as scopes, boolean checks, etc.
14
+ #
15
+ # @param states [Array<Symbol>] the list of possible states in a state machine
16
+ # @note the first state is used as the initial state
17
+ # @param options [Hash] a hash of additional options for the state machine
18
+ #
19
+ # @example
20
+ # class Post < ApplicationRecord
21
+ # has_state_machine states: %i(draft published archived)
22
+ # end
23
+ def has_state_machine(states: [], **options)
24
+ raise ArgumentError, "Please define at least one state to use has_state_machine." if states.empty?
25
+
26
+ define_helper_methods(
27
+ states: states.map(&:to_s),
28
+ options: options.with_indifferent_access
29
+ )
30
+
31
+ include HasStateMachine::StateHelpers
32
+ end
33
+
34
+ private
35
+
36
+ def define_helper_methods(states:, options:)
37
+ ##
38
+ # The list of possible states in the state machine.
39
+ # Can be overwritten to use a different column name.
40
+ define_singleton_method "workflow_states" do
41
+ states
42
+ end
43
+
44
+ ##
45
+ # Defines the column name for the attribute holding the current status.
46
+ # Can be overwritten to use a different column name.
47
+ define_singleton_method "state_attribute" do
48
+ options[:state_attribute]&.to_sym || :status
49
+ end
50
+
51
+ ##
52
+ # Defines the namespace of the models possible states.
53
+ # Can be overwritten to use a different namespace.
54
+ define_singleton_method "workflow_namespace" do
55
+ (options[:workflow_namespace] || "Workflow::#{self}")
56
+ end
57
+
58
+ ##
59
+ # Determines whether or not the state validations should be run
60
+ # as part of the object validations.
61
+ define_singleton_method "state_validations_on_object?" do
62
+ return true unless options.key?(:state_validations_on_object)
63
+
64
+ options[:state_validations_on_object]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasStateMachine
4
+ class Railtie < ::Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string"
4
+
5
+ module HasStateMachine
6
+ class State < String
7
+ extend ActiveModel::Model
8
+ extend ActiveModel::Callbacks
9
+ include ActiveModel::Validations
10
+
11
+ attr_reader :object, :state, :options
12
+
13
+ ##
14
+ # Defines the before_transition and after_transition callbacks
15
+ # for use on a HasStateMachine::State instance.
16
+ define_model_callbacks :transition, only: %i[before after]
17
+
18
+ ##
19
+ # Retrieves the next available transitions for a given state.
20
+ delegate :possible_transitions, :state, to: "self.class"
21
+
22
+ ##
23
+ # Add errors to the ActiveRecord object rather than the HasStateMachine::State
24
+ # class.
25
+ delegate :errors, to: :object
26
+
27
+ ##
28
+ # Initializes the HasStateMachine::State instance.
29
+ #
30
+ # @example
31
+ # state = HasStateMachine::State.new(post) #=> "draft"
32
+ # state.class #=> Workflow::Post::Draft
33
+ def initialize(object)
34
+ @object = object
35
+
36
+ super state
37
+ end
38
+
39
+ ##
40
+ # Checks to see if the desired state is valid and then gives
41
+ # responsibility to the desired state's instance to make the
42
+ # transition.
43
+ #
44
+ # @param desired_state [String] the state to transition to
45
+ # @param options [Hash] a hash of additional options for
46
+ # transitioning the object
47
+ #
48
+ # @return [Boolean] whether or not the transition took place
49
+ def transition_to(desired_state, **options)
50
+ with_transition_options(options) do
51
+ return false unless valid_transition?(desired_state.to_s)
52
+
53
+ state_instance(desired_state.to_s).perform_transition!
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Makes the actual transition from one state to the next and
59
+ # runs the before and after transition callbacks.
60
+ def perform_transition!
61
+ run_callbacks :transition do
62
+ object.update("#{object.state_attribute}": state)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ ##
69
+ # Determines if the given desired state exists in the predetermined
70
+ # list of allowed transitions.
71
+ def can_transition?(desired_state)
72
+ possible_transitions.include? desired_state
73
+ end
74
+
75
+ def state_instance(desired_state)
76
+ klass = "#{object.workflow_namespace}::#{desired_state.to_s.classify}".safe_constantize
77
+ klass&.new(object)
78
+ end
79
+
80
+ def valid_transition?(desired_state)
81
+ return true if options[:skip_validations]
82
+
83
+ object.valid? &&
84
+ can_transition?(desired_state) &&
85
+ state_instance(desired_state)&.valid?
86
+ end
87
+
88
+ def with_transition_options(options, &block)
89
+ @options = options
90
+ object.skip_state_validations = options[:skip_validations]
91
+ yield
92
+ object.skip_state_validations = false
93
+ end
94
+
95
+ class << self
96
+ def possible_transitions
97
+ @possible_transitions || []
98
+ end
99
+
100
+ def state
101
+ to_s.demodulize.underscore
102
+ end
103
+
104
+ ##
105
+ # Setter for the HasStateMachine::State classes to define the possible
106
+ # states the current state can transition to.
107
+ def transitions_to(states)
108
+ @possible_transitions = states.map(&:to_s)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasStateMachine
4
+ module StateHelpers
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_accessor :skip_state_validations
9
+
10
+ delegate \
11
+ :state_attribute,
12
+ :state_validations_on_object?,
13
+ :workflow_namespace,
14
+ :workflow_states,
15
+ to: "self.class"
16
+
17
+ ##
18
+ # Sets the default value of the state method to the initial state
19
+ # defined in the state machine.
20
+ attribute state_attribute, :string, default: initial_state
21
+
22
+ ##
23
+ # Validating any changes to the status attribute are represented by
24
+ # classes within the state machine and have a valid HasStateMachine::State class.
25
+ validates state_attribute, inclusion: {in: workflow_states}, presence: true
26
+ validate :state_class_defined?
27
+ validate :state_instance_validations, if: :should_validate_state?
28
+
29
+ ##
30
+ # Overwrites the default getter for the state attribute to
31
+ # instantiate a HasStateMachine::State instance instead. If the state
32
+ # class does not exist, it simply returns a string.
33
+ #
34
+ # @return [HasStateMachine::State] the current state represented by a instance
35
+ #
36
+ # @example
37
+ # post = Post.new(status: "draft")
38
+ # post.status.class #=> Workflow::Post::Draft
39
+ define_method state_attribute.to_s do
40
+ return state_class.new(self) if state_class.present?
41
+
42
+ current_state
43
+ end
44
+
45
+ workflow_states.each do |state|
46
+ ##
47
+ # Defines scopes based on the state machine possible states
48
+ #
49
+ # @return [ActiveRecord_Relation]
50
+ # @example Retreiving a users published posts
51
+ # > Post.published.where(user: user)
52
+ # #=> [#<Post>]
53
+ scope state, -> { where("#{state_attribute} = ?", state) }
54
+
55
+ ##
56
+ # Defines boolean helpers to determine if the active state matches
57
+ # the specified state.
58
+ #
59
+ # @return [Boolean] whether or not the active state matches the call
60
+ # @example Check if a post is published
61
+ # > post.published?
62
+ # #=> true
63
+ define_method "#{state}?" do
64
+ current_state == state
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ ##
71
+ # Getter for the current state of the model based on the configured state
72
+ # attribute.
73
+ def current_state
74
+ self[state_attribute]
75
+ end
76
+
77
+ ##
78
+ # Predicate method for determining whether or not the state validations
79
+ # should be run as part of the object validations.
80
+ def should_validate_state?
81
+ return false unless state_validations_on_object?
82
+
83
+ !skip_state_validations
84
+ end
85
+
86
+ ##
87
+ # Gets the HasStateMachine::State class that represents the current state
88
+ # of the model.
89
+ def state_class
90
+ return unless current_state.present?
91
+
92
+ "#{workflow_namespace}::#{current_state.classify}".safe_constantize
93
+ end
94
+
95
+ ##
96
+ # True unless unable to find the HasStateMachine::State class for the current
97
+ # state.
98
+ def state_class_defined?
99
+ return if state_class.present?
100
+
101
+ errors.add(state_attribute, :not_implemented, message: "class must be implemented")
102
+ end
103
+
104
+ ##
105
+ # Runs the validations defined on the current HasStateMachine::State when calling
106
+ # model.valid?
107
+ def state_instance_validations
108
+ return unless state_class.present?
109
+
110
+ public_send(state_attribute.to_s).valid?
111
+ end
112
+ end
113
+
114
+ class_methods do
115
+ private
116
+
117
+ ##
118
+ # The initial state of the workflow based on the first state defined in the model
119
+ # has_state_machine states array.
120
+ def initial_state
121
+ workflow_states.first
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasStateMachine
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_state_machine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Hargett
8
+ - Jake Hurd
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-10-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '5.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '5.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: sqlite3
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: pry
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pry-rails
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: standard
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: appraisal
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description: HasStateMachine uses ruby classes to make creating a finite state machine
99
+ in your ActiveRecord models a breeze.
100
+ email:
101
+ - hargettbenjamin@gmail.com
102
+ - jake.hurd@gmail.com
103
+ executables: []
104
+ extensions: []
105
+ extra_rdoc_files: []
106
+ files:
107
+ - MIT-LICENSE
108
+ - README.md
109
+ - Rakefile
110
+ - lib/has_state_machine.rb
111
+ - lib/has_state_machine/core_ext/string.rb
112
+ - lib/has_state_machine/definition.rb
113
+ - lib/has_state_machine/railtie.rb
114
+ - lib/has_state_machine/state.rb
115
+ - lib/has_state_machine/state_helpers.rb
116
+ - lib/has_state_machine/version.rb
117
+ homepage: https://www.github.com/encampment/has_state_machine
118
+ licenses:
119
+ - MIT
120
+ metadata: {}
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '2.5'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.0.3
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: Class based state machine for ActiveRecord models.
140
+ test_files: []