wesm 0.1.8.8
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +136 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/wesm.rb +5 -0
- data/lib/wesm/errors.rb +6 -0
- data/lib/wesm/helpers/transitions_helper.rb +21 -0
- data/lib/wesm/transition.rb +65 -0
- data/lib/wesm/version.rb +3 -0
- data/lib/wesm/wesm.rb +71 -0
- data/wesm.gemspec +26 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d3d0921469fa5f7b77f24e5831c934543a624dd9
|
4
|
+
data.tar.gz: 40e543a39e9d89d66ebac43fc337c26b172580ef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 47ddb686ce25b8cb9040f3b2e19da0c7010fb89d4af9a847169ffb95bc84f309f97a55fe60af4bc8347bda68bbc734c517330313c0e915d53d95ace2c1c9cea0
|
7
|
+
data.tar.gz: fa01c0ec54c8836f33ce902f56f2fb15c865d6ef523ffab91572d203831ec927c588de675ab18b74eb8c062ad499ceff3eaeaae0888903c56a51bc90e47f860f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# Wesm
|
2
|
+
|
3
|
+
[](https://travis-ci.org/arthurweisz/wesm)
|
4
|
+
[](https://codeclimate.com/github/arthurweisz/wesm)
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'wesm'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install wesm
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
### Specifying Transitions
|
25
|
+
|
26
|
+
Extend your class with *Wesm* module and define transitions with *transition* method
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class OrderStateMachine
|
30
|
+
extend Wesm
|
31
|
+
|
32
|
+
transition :pending => :approved, actor: Manager
|
33
|
+
transition :pending => :rejected, actor: Manager, required: :reject_reason
|
34
|
+
transition :approved => :payment_verification, actor: :owner, required: :payment
|
35
|
+
transition :payment_verification => :paid, actor: StripePaymentsService
|
36
|
+
transition :rejected => :closed, actor: [Manager, OrdersSchedulerJob]
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
### Performing transitions
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
user = Manager.new
|
44
|
+
order = Order.new(state: 'pending')
|
45
|
+
|
46
|
+
OrderStateMachine.perform_transition(order, 'approved', user)
|
47
|
+
|
48
|
+
order.state
|
49
|
+
=> "approved"
|
50
|
+
```
|
51
|
+
|
52
|
+
Default implementation is just changing object's state
|
53
|
+
|
54
|
+
You can customize it by overriding *process_transition* method which accepts **object**, **transition**, **actor** and all extra arguments passed to *perform_transition*
|
55
|
+
|
56
|
+
ActiveRecord example with persistence:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class OrderStateMachine
|
60
|
+
extend Wesm
|
61
|
+
|
62
|
+
def self.process_transition(object, transition, actor)
|
63
|
+
ActiveRecord::Base.transaction do
|
64
|
+
# any actions
|
65
|
+
object.state = transition.to_state
|
66
|
+
object.save!
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
### Subsequent transitions information
|
73
|
+
|
74
|
+
*successors* method can be used to list all possible subsequent states for transition regardless of actor and required fields
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
order = Order.new(state: 'pending')
|
78
|
+
|
79
|
+
OrderStateMachine.successors(order)
|
80
|
+
=> ['approved', 'rejected']
|
81
|
+
```
|
82
|
+
|
83
|
+
*show_transitions* method shows list of allowed transitions for provided actor
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
user = User.new
|
87
|
+
manager = Manager.new
|
88
|
+
order = Order.new(state: 'pending', owner: user)
|
89
|
+
|
90
|
+
OrderStateMachine.show_transitions(order, user)
|
91
|
+
=> [{
|
92
|
+
to_state: 'approved',
|
93
|
+
is_authorized: false,
|
94
|
+
can_perform: false,
|
95
|
+
required_fields: []
|
96
|
+
},
|
97
|
+
{
|
98
|
+
to_state: 'rejected',
|
99
|
+
is_authorized: false,
|
100
|
+
can_perform: false,
|
101
|
+
required_fields: [:reject_reason]
|
102
|
+
}]
|
103
|
+
|
104
|
+
OrderStateMachine.show_transitions(order, manager)
|
105
|
+
=> [{
|
106
|
+
to_state: 'approved',
|
107
|
+
is_authorized: true,
|
108
|
+
can_perform: true,
|
109
|
+
required_fields: []
|
110
|
+
},
|
111
|
+
{
|
112
|
+
to_state: 'rejected',
|
113
|
+
is_authorized: true,
|
114
|
+
can_perform: false,
|
115
|
+
required_fields: [:reject_reason]
|
116
|
+
}]
|
117
|
+
```
|
118
|
+
### State field
|
119
|
+
|
120
|
+
Default field for object's mapping is **state** and can be set by overriding *state_field*
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
class OrderStateMachine
|
124
|
+
extend Wesm
|
125
|
+
|
126
|
+
def self.state_field
|
127
|
+
:custom_field
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
## Contributing
|
135
|
+
|
136
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/arthurweisz/wesm.
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/wesm.rb
ADDED
data/lib/wesm/errors.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Wesm
|
2
|
+
module Helpers
|
3
|
+
module TransitionsHelper
|
4
|
+
def successors
|
5
|
+
self.class.state_machine.successors(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def show_transitions(actor)
|
9
|
+
self.class.state_machine.show_transitions(self, actor)
|
10
|
+
end
|
11
|
+
|
12
|
+
def required_fields(to_state)
|
13
|
+
self.class.state_machine.required_fields(self, to_state)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform_transition(to_state, actor, *extras)
|
17
|
+
self.class.state_machine.perform_transition(self, to_state, actor, *extras)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Wesm
|
2
|
+
class Transition
|
3
|
+
attr_accessor :from_state, :to_state, :performer, :required
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@from_state = options.keys.first.to_s
|
7
|
+
@to_state = options.values.first.to_s
|
8
|
+
@valid_actors = Array(options[:actor])
|
9
|
+
@scope = options[:scope]
|
10
|
+
@required = Array(options[:required])
|
11
|
+
@performer = options[:performer]
|
12
|
+
end
|
13
|
+
|
14
|
+
def actor_is_valid?(object, actor)
|
15
|
+
@valid_actors.empty? || \
|
16
|
+
@valid_actors.map { |valid_actor| compare_actor(valid_actor, object, actor) }.any?
|
17
|
+
end
|
18
|
+
|
19
|
+
def required_fields(object)
|
20
|
+
@required.select { |required_field| object.public_send(required_field).nil? }
|
21
|
+
end
|
22
|
+
|
23
|
+
def required_fields_present?(object)
|
24
|
+
@required.each do |required_field|
|
25
|
+
return false if object.public_send(required_field).nil?
|
26
|
+
end
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid_scope?(object)
|
31
|
+
@scope.to_h.each do |field, value|
|
32
|
+
return false unless \
|
33
|
+
begin
|
34
|
+
if value.is_a? Proc
|
35
|
+
value.call(*[object.public_send(field), object][0...value.arity])
|
36
|
+
else
|
37
|
+
object.public_send(field) == value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_performer_method(method, *args)
|
45
|
+
return unless @performer && @performer.respond_to?(method)
|
46
|
+
@performer.public_send(method, *args)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def compare_actor(valid_actor, object, actor)
|
52
|
+
if [String, Symbol].include? valid_actor.class
|
53
|
+
object.public_send(valid_actor) == actor
|
54
|
+
elsif valid_actor.is_a? Class
|
55
|
+
if actor.is_a?(Class)
|
56
|
+
actor == valid_actor
|
57
|
+
else
|
58
|
+
actor.is_a?(valid_actor)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/wesm/version.rb
ADDED
data/lib/wesm/wesm.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Wesm
|
2
|
+
def transition(options)
|
3
|
+
@transitions ||= {}
|
4
|
+
transition = Transition.new(options)
|
5
|
+
(@transitions[transition.from_state] ||= []) << transition.freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
def successors(object)
|
9
|
+
transitions_for(object).map(&:to_state).uniq
|
10
|
+
end
|
11
|
+
|
12
|
+
def show_transitions(object, actor)
|
13
|
+
transitions_for(object).map do |transition|
|
14
|
+
is_authorized = transition.actor_is_valid?(object, actor)
|
15
|
+
|
16
|
+
{
|
17
|
+
to_state: transition.to_state,
|
18
|
+
is_authorized: is_authorized,
|
19
|
+
can_perform: is_authorized && transition.required_fields_present?(object),
|
20
|
+
required_fields: transition.required_fields(object)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def required_fields(object, to_state)
|
26
|
+
transition = get_transition(object, to_state)
|
27
|
+
transition && transition.required_fields(object)
|
28
|
+
end
|
29
|
+
|
30
|
+
def perform_transition(object, to_state, actor, *extras)
|
31
|
+
transition = get_transition!(object, to_state, actor)
|
32
|
+
|
33
|
+
process_transition(object, transition, actor, *extras)
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_transition(object, transition, actor, *extras)
|
37
|
+
object.public_send("#{state_field}=", transition.to_state)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def transitions_for(object)
|
43
|
+
(@transitions[object.public_send(state_field)] || [])
|
44
|
+
.reject { |transition| !transition.valid_scope?(object) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_transition(object, to_state)
|
48
|
+
transitions_for(object).detect { |transition| transition.to_state == to_state }
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_transition!(object, to_state, actor)
|
52
|
+
transition = get_transition(object, to_state)
|
53
|
+
|
54
|
+
if transition.nil?
|
55
|
+
error_desc = "from '#{object.public_send(state_field)}' to '#{to_state}'"
|
56
|
+
raise UnexpectedTransitionError.new(error_desc)
|
57
|
+
elsif !transition.actor_is_valid?(object, actor)
|
58
|
+
error_desc = "for actor: #{actor}"
|
59
|
+
raise AccessViolationError.new(error_desc)
|
60
|
+
elsif !transition.required_fields_present?(object)
|
61
|
+
error_desc = "fields: #{transition.required_fields(object).join(', ')}"
|
62
|
+
raise TransitionRequirementError.new(error_desc)
|
63
|
+
else
|
64
|
+
transition
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def state_field
|
69
|
+
:state
|
70
|
+
end
|
71
|
+
end
|
data/wesm.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'wesm/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'wesm'
|
8
|
+
spec.version = Wesm::VERSION
|
9
|
+
spec.authors = ['arthurweisz']
|
10
|
+
spec.email = ['cloudsong1@yandex.ru']
|
11
|
+
|
12
|
+
spec.summary = 'Wisely Explicit State Machine'
|
13
|
+
spec.description = 'Wisely Explicit State Machine'
|
14
|
+
spec.homepage = ''
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
24
|
+
spec.add_development_dependency 'pry', '~> 0.10'
|
25
|
+
spec.required_ruby_version = '>= 1.9'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wesm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.8.8
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- arthurweisz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.10'
|
69
|
+
description: Wisely Explicit State Machine
|
70
|
+
email:
|
71
|
+
- cloudsong1@yandex.ru
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".travis.yml"
|
79
|
+
- Gemfile
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- bin/console
|
83
|
+
- bin/setup
|
84
|
+
- lib/wesm.rb
|
85
|
+
- lib/wesm/errors.rb
|
86
|
+
- lib/wesm/helpers/transitions_helper.rb
|
87
|
+
- lib/wesm/transition.rb
|
88
|
+
- lib/wesm/version.rb
|
89
|
+
- lib/wesm/wesm.rb
|
90
|
+
- wesm.gemspec
|
91
|
+
homepage: ''
|
92
|
+
licenses: []
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.9'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 2.6.11
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: Wisely Explicit State Machine
|
114
|
+
test_files: []
|