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.
@@ -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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.DS_Store
11
+ wesm-*.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wesm.gemspec
4
+ gemspec
@@ -0,0 +1,136 @@
1
+ # Wesm
2
+
3
+ [![Build Status](https://travis-ci.org/arthurweisz/wesm.svg?branch=master)](https://travis-ci.org/arthurweisz/wesm)
4
+ [![Code Climate](https://codeclimate.com/github/arthurweisz/wesm/badges/gpa.svg)](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.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'wesm'
5
+ require 'pry'
6
+
7
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ require 'wesm/transition'
2
+ require 'wesm/helpers/transitions_helper'
3
+ require 'wesm/errors'
4
+ require 'wesm/version'
5
+ require 'wesm/wesm'
@@ -0,0 +1,6 @@
1
+ module Wesm
2
+ class BaseError < StandardError; end
3
+ class AccessViolationError < BaseError; end
4
+ class UnexpectedTransitionError < BaseError; end
5
+ class TransitionRequirementError < BaseError; end
6
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Wesm
2
+ VERSION = '0.1.8.8'
3
+ end
@@ -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
@@ -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: []