statesman-multi_state 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: abc46124837badbc6906ffb68ad2dfb368c1a79baadb7d90064527d2c22b5afc
4
+ data.tar.gz: 0b7c420e4a634798411eae000309d0b7f156db80745e8091aa3525a2d1488ca9
5
+ SHA512:
6
+ metadata.gz: f25f298ab42306d5cf579b41c3382a11e205e5fe8f7f159fd82c456f7651f3af99d4753b8e3789911f8cb069f32940c2b1230e7701a64bea11f5068af443e4bc
7
+ data.tar.gz: e3eea7741b07995f8ba036c49036cb405c90563c15df46df8e499eb39805e9b2d61fa904e729e87227cd21383767105e854cbc2c865540d94d9cdd201b74830b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 chaadow
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.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Statesman::MultiState
2
+ ![Build](https://github.com/chaadow/statesman-multi_state/actions/workflows/ruby.yml/badge.svg)
3
+
4
+ Handle multi state for `statesman` through `has_one_state_machine` ActiveRecord macro
5
+
6
+ ## Usage
7
+
8
+ After you generate your transition classes as well as your state machines, all
9
+ you need to add is this in your model:
10
+
11
+ ```ruby
12
+ class MyActiveRecordModel < ActiveRecord::Base
13
+ has_one_state_machine :state, state_machine_klass: 'StateMachineKlass', transition_klass: 'MyTransitionKlass'
14
+ end
15
+ ```
16
+
17
+ It also plugs into `ActiveRecord::Reflection` apis, so you can go fancy with
18
+ dynamic form generation
19
+ ```ruby
20
+ MyActiveRecordModel.reflect_on_all_state_machines
21
+ MyActiveRecordModel.reflect_on_state_machine(:state)
22
+ ```
23
+
24
+ ## Installation
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem "statesman-multi_state"
29
+ ```
30
+
31
+ And then execute:
32
+ ```bash
33
+ $ bundle
34
+ ```
35
+
36
+ Or install it yourself as:
37
+ ```bash
38
+ $ gem install statesman-multi_state
39
+ ```
40
+
41
+ ## License
42
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statesman
4
+ module Adapters
5
+ module CustomActiveRecordQueries
6
+ def self.[](**args)
7
+ ClassMethods.new(**args)
8
+ end
9
+
10
+ class ClassMethods < Module
11
+ def initialize(**args)
12
+ @args = args
13
+ end
14
+
15
+ def included(base)
16
+ ensure_inheritance(base)
17
+
18
+ field_name = @args.delete(:field_name)
19
+
20
+ query_builder = ::Statesman::Adapters::ActiveRecordQueries::QueryBuilder.new(base, **@args)
21
+
22
+ join_name = :"#{field_name}_most_recent_transition_join"
23
+
24
+ base.define_singleton_method(join_name) do
25
+ query_builder.most_recent_transition_join
26
+ end
27
+
28
+ define_in_state(base, query_builder, join_name, field_name)
29
+ define_not_in_state(base, query_builder, join_name, field_name)
30
+ end
31
+
32
+ private
33
+
34
+ def ensure_inheritance(base)
35
+ klass = self
36
+ existing_inherited = base.method(:inherited)
37
+ base.define_singleton_method(:inherited) do |subclass|
38
+ existing_inherited.call(subclass)
39
+ subclass.send(:include, klass)
40
+ end
41
+ end
42
+
43
+ def define_in_state(base, query_builder, join_name, field_name)
44
+ base.define_singleton_method(:"#{field_name}_in_state") do |*states|
45
+ states = states.flatten
46
+
47
+ joins(public_send(join_name))
48
+ .where(query_builder.states_where(states), states)
49
+ end
50
+ end
51
+
52
+ def define_not_in_state(base, query_builder, join_name, field_name)
53
+ base.define_singleton_method(:"#{field_name}_not_in_state") do |*states|
54
+ states = states.flatten
55
+
56
+ joins(public_send(join_name))
57
+ .where.not(query_builder.states_where(states), states)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'statesman/adapters/custom_active_record_queries'
4
+
5
+ module Statesman
6
+ module MultiState
7
+ module ActiveRecordMacro
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def has_one_state_machine(field_name, state_machine_klass:, transition_klass:, transition_name: transition_klass.to_s.underscore.pluralize.to_sym)
12
+ state_machine_name = "#{field_name}_state_machine"
13
+ virtual_attribute_name = "#{field_name}_state_form"
14
+
15
+ # To handle STI, this needs to be done to get the base klass
16
+ base_klass = caller_locations.first.label.split(':').last[...-1]
17
+
18
+ has_many transition_name, class_name: transition_klass.to_s, autosave: false, dependent: :destroy
19
+
20
+ include Statesman::Adapters::CustomActiveRecordQueries[
21
+ transition_class: transition_klass.constantize,
22
+ initial_state: state_machine_klass.constantize.send(:initial_state),
23
+ transition_name: transition_name,
24
+ most_recent_transition_alias: "#{field_name}_alias",
25
+ field_name: field_name
26
+ ]
27
+
28
+ attribute virtual_attribute_name
29
+
30
+ %w[current_state in_state? transition_to transition_to! can_transition_to? history last_transition
31
+ last_transition_to].each do |meth|
32
+ delegate meth, to: state_machine_name, prefix: field_name
33
+ end
34
+
35
+ generated_association_methods.class_eval <<~CODE, __FILE__, __LINE__ + 1
36
+
37
+ def #{state_machine_name}
38
+ key = "@#{state_machine_name}"
39
+ return instance_variable_get(key) if instance_variable_defined?(key)
40
+
41
+ instance_variable_set(key, #{state_machine_klass}.new(
42
+ self,
43
+ transition_class: #{transition_klass},
44
+ association_name: "#{transition_name}"
45
+ ))
46
+ end
47
+
48
+ def #{virtual_attribute_name}
49
+ super() || #{field_name}_current_state
50
+ end
51
+
52
+ def #{field_name}_current_state_human
53
+ Hash[self.class.#{field_name}_human_wrapper]
54
+ .invert[#{field_name}_current_state]
55
+ end
56
+ CODE
57
+
58
+ include(const_set("#{field_name}#{SecureRandom.hex(4)}_mod".classify, Module.new).tap do |mod|
59
+ mod.module_eval do
60
+ extend ActiveSupport::Concern
61
+
62
+ class_methods do
63
+ define_method :"#{field_name}_human_wrapper" do
64
+ key = "@#{field_name}_human_wrapper"
65
+ return instance_variable_get(key) if instance_variable_defined?(key)
66
+
67
+ instance_variable_set(key, state_machine_klass.constantize.send(:states).map do |s|
68
+ [I18n.t(s, scope: "statesman.#{field_name}_#{base_klass.underscore}"), s]
69
+ end)
70
+ end
71
+ end
72
+
73
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
74
+ def save_with_state(**options)
75
+ @registered_callbacks ||= []
76
+ if #{virtual_attribute_name}_changed? && #{field_name}_can_transition_to?(#{virtual_attribute_name})
77
+ @registered_callbacks << -> { #{field_name}_transition_to(#{virtual_attribute_name}, **options) }
78
+ end
79
+
80
+ if defined?(super)
81
+ super
82
+ else
83
+ save
84
+ @registered_callbacks.each(&:call)
85
+ @registered_callbacks = []
86
+ end
87
+ end
88
+ METHOD
89
+ end
90
+ end)
91
+
92
+ reflection = ActiveRecord::Reflection.create(
93
+ :has_one_state_machine,
94
+ field_name,
95
+ nil,
96
+ { state_machine_klass: state_machine_klass, transition_klass: transition_klass,
97
+ transition_name: transition_name },
98
+ self
99
+ )
100
+
101
+ yield reflection if block_given?
102
+
103
+ ActiveRecord::Reflection.add_state_machine_reflection(self, field_name, reflection)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statesman
4
+ module MultiState
5
+ class Railtie < ::Rails::Railtie
6
+ initializer 'statesman.multi_state.init' do
7
+ require 'statesman/multi_state/reflection'
8
+ require 'statesman/multi_state/active_record_macro'
9
+
10
+ ActiveSupport.on_load(:active_record) do
11
+ include Statesman::MultiState::ActiveRecordMacro
12
+ include Statesman::MultiState::Reflection::ActiveRecordExtensions
13
+
14
+ ActiveRecord::Reflection.singleton_class.prepend(Statesman::MultiState::Reflection::ReflectionExtension)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statesman
4
+ module MultiState
5
+ module Reflection
6
+ # Holds all the metadata about a state_machine as it was
7
+ # specified in the Active Record class.
8
+ class HasOneStateMachineReflection < ::ActiveRecord::Reflection::MacroReflection # :nodoc:
9
+ def macro
10
+ :has_one_state_machine
11
+ end
12
+ end
13
+
14
+ module ReflectionExtension
15
+ def add_state_machine_reflection(model, name, reflection)
16
+ model.state_machine_reflections = model.state_machine_reflections.merge(name.to_s => reflection)
17
+ end
18
+
19
+ private
20
+
21
+ def reflection_class_for(macro)
22
+ case macro
23
+ when :has_one_state_machine
24
+ HasOneStateMachineReflection
25
+ else
26
+ super
27
+ end
28
+ end
29
+ end
30
+
31
+ module ActiveRecordExtensions
32
+ extend ActiveSupport::Concern
33
+
34
+ included do
35
+ class_attribute :state_machine_reflections, instance_writer: false, default: {}
36
+ end
37
+
38
+ class_methods do
39
+ # Returns an array of reflection objects for all the state
40
+ # machines in the class.
41
+ def reflect_on_all_state_machines
42
+ state_machine_reflections.values
43
+ end
44
+
45
+ # Returns the reflection object for the named +state_machine+.
46
+ #
47
+ # User.reflect_on_state_machine(:status)
48
+ # # => the status reflection
49
+ #
50
+ def reflect_on_state_machine(state_machine)
51
+ state_machine_reflections[state_machine.to_s]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statesman
4
+ module MultiState
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'statesman/multi_state/version'
4
+ require 'statesman/multi_state/railtie'
5
+
6
+ module Statesman
7
+ module MultiState
8
+ # Your code goes here...
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :statesman_multi_state do
4
+ # # Task goes here
5
+ # end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statesman-multi_state
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chedli Bourguiba
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: statesman
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 9.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 9.0.0
41
+ description: Handle multi state for statesman through `has_one_state_machine` ActiveRecord
42
+ macro
43
+ email:
44
+ - bourguiba.chedli@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - lib/statesman/adapters/custom_active_record_queries.rb
53
+ - lib/statesman/multi_state.rb
54
+ - lib/statesman/multi_state/active_record_macro.rb
55
+ - lib/statesman/multi_state/railtie.rb
56
+ - lib/statesman/multi_state/reflection.rb
57
+ - lib/statesman/multi_state/version.rb
58
+ - lib/tasks/statesman/multi_state_tasks.rake
59
+ homepage: https://github.com/chaadow/statesman-multi_state
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://github.com/chaadow/statesman-multi_state
64
+ source_code_uri: https://github.com/chaadow/statesman-multi_state
65
+ changelog_uri: https://github.com/chaadow/statesman-multi_state/blob/master/CHANGELOG.md
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 2.7.0
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.1.6
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Handle multi state for statesman through has_one_state_machine
85
+ test_files: []