excom 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
+ SHA1:
3
+ metadata.gz: c2e0ab278ff802a81618b2db0f1549d450a96f41
4
+ data.tar.gz: ca066ba4969cec14ba67359d08c7eb93b01832b4
5
+ SHA512:
6
+ metadata.gz: 6f6c5e50e2e88fba4387545edfd7fd28834739b2e10a44c43a71802d0e06e3a0a464e854cb0024f647be6b81b7717b871b9c63f5cccdc635977839624abfc6f8
7
+ data.tar.gz: 72c07a6cd427a26c6c9fd2dc342499a39af0db3aef6524b4fa41f1ab8fac4ffacb9d0962f322c9e34a28b5964615c6ad24c688b974e4e32743139045c5bcdfc1
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .ruby-version
12
+ .ruby-gemset
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in excom.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Artem Kuzko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # Excom
2
+
3
+ Flexible and highly extensible Commands (Service Objects) for business logic.
4
+
5
+ [![build status](https://secure.travis-ci.org/akuzko/excom.png)](http://travis-ci.org/akuzko/excom)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'excom'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install excom
22
+
23
+ ## Usage
24
+
25
+ General idea behind every `excom` command is simple: each command can have arguments,
26
+ options (named arguments), and should define `run` method that is called during
27
+ command execution. Executed command has `status` and `result`.
28
+
29
+ The **very basic usage** of `Excom` commands can be shown with following example:
30
+
31
+ ```rb
32
+ # app/commands/todos/update.rb
33
+ module Todos
34
+ class Update < Excom::Command
35
+ args :todo
36
+ opts :params
37
+
38
+ def run
39
+ if todo.update(params)
40
+ result success: todo.as_json
41
+ else
42
+ result failure: todo.errors
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ # app/controllers/todos/controller
49
+ class TodosController < ApplicationController
50
+ def update
51
+ command = Todos::Update.new(todo, params: todo_params)
52
+
53
+ if command.execute.success?
54
+ render json: todo.result
55
+ else
56
+ render json: todo.result, status: :unprocessable_entity
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ However, even this basic example can be highly optimized by using Excom extensions and helper methods.
63
+
64
+ ### Command arguments and options
65
+
66
+ Read full version on [wiki](https://github.com/akuzko/excom/wiki#instantiating-command-with-arguments-and-options).
67
+
68
+ Excom commands can be initialized with _arguments_ and _options_ (named arguments). To specify list
69
+ of available arguments and options, use `args` and `opts` class methods. All arguments and options
70
+ are optional during command initialization. However, you cannot pass more arguments to command or
71
+ options that were not declared with `opts` method.
72
+
73
+ ```rb
74
+ class MyCommand < Excom::Command
75
+ args :foo
76
+ opts :bar
77
+
78
+ def run
79
+ # do something
80
+ end
81
+
82
+ def foo
83
+ super || 5
84
+ end
85
+ end
86
+
87
+ c1 = MyCommand.new
88
+ c1.foo # => 5
89
+ c1.bar # => nil
90
+
91
+ c2 = c1.with_args(1).with_opts(bar: 2)
92
+ c2.foo # => 1
93
+ c2.bar # => 2
94
+ ```
95
+
96
+ ### Command Execution
97
+
98
+ Read full version on [wiki](https://github.com/akuzko/excom/wiki#command-execution).
99
+
100
+ At the core of each command's execution lies `run` method. You can use `status` and/or
101
+ `result` methods to set execution status and result. If none were used, result and status
102
+ will be set based on `run` method's return value.
103
+
104
+ Example:
105
+
106
+ ```rb
107
+ class MyCommand < Excom::Command
108
+ alias_success :ok
109
+ args :foo
110
+
111
+ def run
112
+ if foo > 2
113
+ result ok: foo * 2
114
+ else
115
+ result failure: -1
116
+ end
117
+ end
118
+ end
119
+
120
+ command = MyCommand.new(3)
121
+ command.execute.success? # => true
122
+ command.status # => :ok
123
+ command.result # => 6
124
+ ```
125
+
126
+ ### Core API
127
+
128
+ Please read about core API and available class and instance methods on [wiki](https://github.com/akuzko/excom/wiki#core-api)
129
+
130
+ ### Command Extensions (Plugins)
131
+
132
+ Excom is built with extensions in mind. Even core functionality is organized in plugins that are
133
+ used in base `Excom::Command` class. Bellow you can see a list of plugins with some description
134
+ and examples that are shipped with `excom`:
135
+
136
+ - [`:status_helpers`](https://github.com/akuzko/excom/wiki/Plugins#status-helpers) - Allows you to
137
+ define status aliases and helper methods named after them to immediately and more explicitly assign
138
+ both status and result at the same time:
139
+
140
+ ```rb
141
+ class Todos::Update
142
+ use :status_helpers, success: [:ok], failure: [:unprocessable_entity]
143
+ args :todo, :params
144
+
145
+ def run
146
+ if todo.update(params)
147
+ ok todo.as_json
148
+ else
149
+ unprocessable_entity todo.errors
150
+ end
151
+ end
152
+ end
153
+
154
+ command = Todos::Update.(todo, todo_params)
155
+ # in case params were valid you will have:
156
+ command.success? # => true
157
+ command.status # => :ok
158
+ command.result # => {'id' => 1, ...}
159
+ ```
160
+
161
+ - [`:context`](https://github.com/akuzko/excom/wiki/Plugins#context) - Allows you to set an execution
162
+ context for a block that will be available to any command that uses this plugin via `context` method.
163
+
164
+ ```rb
165
+ # application_controller.rb
166
+ around_action :with_context
167
+
168
+ def with_context
169
+ Excom.with_context(current_user: current_user) do
170
+ yield
171
+ end
172
+ end
173
+ ```
174
+
175
+ ```rb
176
+ class Posts::Archive < Excom::Command
177
+ use :context
178
+ args :post
179
+
180
+ def run
181
+ post.update(archived: true, archived_by: context[:current_user])
182
+ end
183
+ end
184
+ ```
185
+
186
+ - [`:sentry`](https://github.com/akuzko/excom/wiki/Plugins#sentry) - Allows you to provide Sentry classes
187
+ for commands that use this plugin. Each Sentry class hosts logic responsible for allowing or denying
188
+ corresponding command's execution or related checks. Much like [pundit](https://github.com/elabs/pundit)
189
+ Policies, but more. Where pundit governs only authorization logic, Excom's Sentries can deny execution
190
+ with any reason you find appropriate.
191
+
192
+ ```rb
193
+ class Posts::Destroy < Excom::Command
194
+ use :context
195
+ use :sentry
196
+ args :post
197
+
198
+ def run
199
+ post.destroy
200
+ end
201
+ end
202
+
203
+ class Posts::DestroySentry < Excom::Sentry
204
+ delegate :post, :context, to: :command
205
+ deny_with :unauthorized
206
+
207
+ def execute?
208
+ # only author can destroy a post
209
+ post.author_id == context[:current_user].id
210
+ end
211
+
212
+ deny_with :unprocessable_entity do
213
+ def execute?
214
+ # disallow to destroy posts that are older than 1 hour
215
+ (post.created_at + 1.hour).past?
216
+ end
217
+ end
218
+ end
219
+ ```
220
+
221
+ - [`:assertions`](https://github.com/akuzko/excom/wiki/Plugins#assertions) - Provides `assert` method that
222
+ can be used for different logic checks during command execution.
223
+
224
+ - [`:caching`](https://github.com/akuzko/excom/wiki/Plugins#caching) - Simple plugin that will prevent
225
+ re-execution of command if it already has been executed, and will immediately return result.
226
+
227
+ - [`:rescue`](https://github.com/akuzko/excom/wiki/Plugins#rescue) - Provides `:rescue` execution option.
228
+ If set to `true`, any error occurred during command execution will not be raised outside.
229
+
230
+ ## Development
231
+
232
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
233
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
234
+
235
+ ## Contributing
236
+
237
+ Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko/excom.
238
+
239
+ ## License
240
+
241
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
242
+
data/Rakefile ADDED
@@ -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
data/bin/console ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "excom"
5
+
6
+ class Command < Excom::Command
7
+ use :sentry, class: 'MySentry'
8
+ use :assertions
9
+ use :caching
10
+
11
+ args :foo
12
+ opts :bar, :baz
13
+
14
+ alias_success :ok
15
+
16
+ def foo
17
+ super || 6
18
+ end
19
+
20
+ def run
21
+ result ok: foo * 2
22
+ assert { foo > bar }
23
+ end
24
+ end
25
+
26
+ class MySentry < Excom::Sentry
27
+ deny_with :unauthorized
28
+
29
+ def execute?
30
+ command.foo != 5
31
+ end
32
+
33
+ deny_with :unprocessable_entity do
34
+ def bar?
35
+ command.bar && command.bar > 0
36
+ end
37
+ end
38
+ end
39
+
40
+ require "pry"
41
+ Pry.start
data/bin/setup ADDED
@@ -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
data/excom.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'excom/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "excom"
8
+ spec.version = Excom::VERSION
9
+ spec.authors = ["Artem Kuzko"]
10
+ spec.email = ["a.kuzko@gmail.com"]
11
+
12
+ spec.summary = %q{Flexible and highly extensible Commands (Service Objects) for business logic}
13
+ spec.description = %q{Flexible and highly extensible Commands (Service Objects) for business logic}
14
+ spec.homepage = "https://github.com/akuzko/excom"
15
+ spec.license = "MIT"
16
+
17
+ spec.required_ruby_version = '>= 2.2.0'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.11"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "rspec-its", "~> 1.2"
28
+ spec.add_development_dependency "pry"
29
+ spec.add_development_dependency "pry-nav"
30
+ end
data/lib/excom.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "excom/version"
2
+
3
+ module Excom
4
+ autoload :Plugins, 'excom/plugins'
5
+ autoload :Command, 'excom/command'
6
+
7
+ extend Plugins::Context::ExcomMethods
8
+
9
+ Sentry = Plugins::Sentry::Sentinel
10
+ end
@@ -0,0 +1,8 @@
1
+ module Excom
2
+ class Command
3
+ extend Excom::Plugins::Pluggable
4
+
5
+ use :executable
6
+ use :args
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ module Excom
2
+ module Plugins
3
+ autoload :Pluggable, 'excom/plugins/pluggable'
4
+ autoload :Executable, 'excom/plugins/executable'
5
+ autoload :Context, 'excom/plugins/context'
6
+ autoload :Sentry, 'excom/plugins/sentry'
7
+
8
+ module_function
9
+
10
+ def fetch(name)
11
+ require("excom/plugins/#{name}") unless plugins.key?(name)
12
+
13
+ plugins[name] || fail("extension `#{name}` is not registered")
14
+ end
15
+
16
+ def register(name, extension, options = {})
17
+ if plugins.key?(name)
18
+ fail ArgumentError, "extension `#{name}` is already registered"
19
+ end
20
+ extension.singleton_class.send(:define_method, :excom_options) { options }
21
+ plugins[name] = extension
22
+ end
23
+
24
+ def plugins
25
+ @plugins ||= {}
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,106 @@
1
+ module Excom
2
+ module Plugins::Args
3
+ Plugins.register :args, self
4
+
5
+ def initialize(*args)
6
+ args, opts = resolve_args!(args)
7
+
8
+ assert_valid_args!(args)
9
+ assert_valid_opts!(opts)
10
+
11
+ @args = args
12
+ @opts = opts
13
+ end
14
+
15
+ def initialize_clone(*)
16
+ super
17
+ @args = @args.dup
18
+ @opts = @opts.dup
19
+ end
20
+
21
+ def with_args(*args)
22
+ clone.tap{ |copy| copy.args.replace(args) }
23
+ end
24
+
25
+ def with_opts(opts)
26
+ clone.tap{ |copy| copy.opts.merge!(opts) }
27
+ end
28
+
29
+ protected def opts
30
+ @opts
31
+ end
32
+
33
+ protected def args
34
+ @args
35
+ end
36
+
37
+ private def resolve_args!(args)
38
+ opts = args.last.is_a?(Hash) ? args.pop : {}
39
+
40
+ if args.length < self.class.args_list.length
41
+ rest = opts
42
+ opts = self.class.opts_list.each_with_object({}) do |key, ops|
43
+ ops[key] = rest[key]
44
+ rest.delete(key)
45
+ end
46
+
47
+ args.push(rest) unless rest.empty?
48
+ end
49
+
50
+ return args, opts
51
+ end
52
+
53
+ private def assert_valid_args!(actual)
54
+ allowed = self.class.args_list.length
55
+
56
+ if actual.length > allowed
57
+ fail ArgumentError, "wrong number of args (given #{actual.length}, expected 0..#{allowed})"
58
+ end
59
+ end
60
+
61
+ private def assert_valid_opts!(actual)
62
+ unexpected = actual.keys - self.class.opts_list
63
+
64
+ if unexpected.any?
65
+ fail ArgumentError, "wrong opts #{unexpected} given"
66
+ end
67
+ end
68
+
69
+ module ClassMethods
70
+ def inherited(command_class)
71
+ command_class.const_set(:ArgMethods, Module.new)
72
+ command_class.send(:include, command_class::ArgMethods)
73
+ command_class.args_list.replace args_list.dup
74
+ command_class.opts_list.replace opts_list.dup
75
+ end
76
+
77
+ def arg_methods
78
+ const_get(:ArgMethods)
79
+ end
80
+
81
+ def args(*argz)
82
+ args_list.concat(argz)
83
+
84
+ argz.each_with_index do |name, i|
85
+ arg_methods.send(:define_method, name){ @args[i] }
86
+ end
87
+ end
88
+
89
+ def opts(*optz)
90
+ opts_list.concat(optz)
91
+
92
+ optz.each do |name|
93
+ arg_methods.send(:define_method, name){ @opts[name] }
94
+ end
95
+ end
96
+
97
+ def args_list
98
+ @args_list ||= []
99
+ end
100
+
101
+ def opts_list
102
+ @opts_list ||= []
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,13 @@
1
+ module Excom
2
+ module Plugins::Assertions
3
+ Plugins.register :assertions, self
4
+
5
+ def assert(fail_with: self.fail_with)
6
+ if yield
7
+ status :success unless defined?(@status)
8
+ else
9
+ failure!(fail_with)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Excom
2
+ module Plugins::Caching
3
+ Plugins.register :caching, self, use_with: :prepend
4
+
5
+ def execute(*)
6
+ return super if block_given? || !executed?
7
+
8
+ self
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ module Excom
2
+ module Plugins::Context
3
+ Plugins.register :context, self
4
+
5
+ def initialize(*)
6
+ @local_context = {}
7
+ super
8
+ end
9
+
10
+ def initialize_clone(*)
11
+ @local_context = @local_context.dup
12
+ super
13
+ end
14
+
15
+ def context
16
+ global_context = ::Excom.context
17
+ global_context.respond_to?(:merge) ?
18
+ global_context.merge(local_context) :
19
+ local_context
20
+ end
21
+
22
+ def with_context(ctx)
23
+ clone.tap{ |copy| copy.local_context.merge!(ctx) }
24
+ end
25
+
26
+ protected def local_context
27
+ @local_context
28
+ end
29
+
30
+ module ExcomMethods
31
+ def with_context(ctx)
32
+ current, Thread.current[:excom_context] = \
33
+ context, context.respond_to?(:merge) ? context.merge(ctx) : ctx
34
+ yield
35
+ ensure
36
+ Thread.current[:excom_context] = current
37
+ end
38
+
39
+ def context
40
+ Thread.current[:excom_context] || {}
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,116 @@
1
+ module Excom
2
+ module Plugins::Executable
3
+ Plugins.register :executable, self
4
+
5
+ UNDEFINED = :__EXCOM_UNDEFINED__
6
+ private_constant :UNDEFINED
7
+
8
+ def initialize(*)
9
+ @executed = false
10
+ super
11
+ end
12
+
13
+ def initialize_clone(*)
14
+ clear_execution_state!
15
+ end
16
+
17
+ def execute(*, &block)
18
+ clear_execution_state!
19
+ result = run(&block)
20
+ result_with(result) unless defined? @result
21
+ @executed = true
22
+
23
+ self
24
+ end
25
+
26
+ def executed?
27
+ @executed
28
+ end
29
+
30
+ private def run
31
+ success!
32
+ end
33
+
34
+ private def clear_execution_state!
35
+ @executed = false
36
+ remove_instance_variable('@result'.freeze) if defined?(@result)
37
+ remove_instance_variable('@status'.freeze) if defined?(@status)
38
+ end
39
+
40
+ def result(obj = UNDEFINED)
41
+ return @result if obj == UNDEFINED
42
+
43
+ case obj
44
+ when Hash
45
+ if obj.length != 1
46
+ fail ArgumentError, "expected 1-item status-result pair, got: #{obj.inspect}"
47
+ end
48
+
49
+ @status, @result = obj.first
50
+ else
51
+ result_with(obj)
52
+ end
53
+ end
54
+
55
+ private def result_with(obj)
56
+ @status = obj ? :success : fail_with unless defined?(@status)
57
+ @result = obj
58
+ end
59
+
60
+ def status(status = UNDEFINED)
61
+ return @status = status unless status == UNDEFINED
62
+
63
+ @status
64
+ end
65
+
66
+ def success?
67
+ status == :success || self.class.success_aliases.include?(status)
68
+ end
69
+
70
+ def failure?
71
+ !success?
72
+ end
73
+
74
+ private def success!
75
+ @status = :success
76
+ @result = true
77
+ end
78
+
79
+ private def failure!(status = fail_with)
80
+ @status = status
81
+ end
82
+
83
+ protected def fail_with
84
+ self.class.fail_with
85
+ end
86
+
87
+ module ClassMethods
88
+ def call(*args)
89
+ new(*args).execute
90
+ end
91
+
92
+ def [](*args)
93
+ call(*args).result
94
+ end
95
+
96
+ def fail_with(status = nil)
97
+ return @fail_with || :failure if status.nil?
98
+
99
+ @fail_with = status
100
+ end
101
+
102
+ def success_aliases
103
+ []
104
+ end
105
+
106
+ def alias_success(*aliases)
107
+ singleton_class.send(:define_method, :success_aliases) { super() + aliases }
108
+ end
109
+
110
+ def method_added(name)
111
+ private :run if name == :run
112
+ super if defined? super
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,20 @@
1
+ module Excom
2
+ module Plugins::Pluggable
3
+ def use(name, **opts)
4
+ extension = Plugins.fetch(name)
5
+
6
+ method = extension.excom_options[:use_with] || :include
7
+ send(method, extension)
8
+
9
+ if extension.const_defined?('ClassMethods')
10
+ extend extension::ClassMethods
11
+ end
12
+
13
+ if extension.respond_to?(:used)
14
+ extension.used(self, **opts)
15
+ end
16
+
17
+ extension
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module Excom
2
+ module Plugins::Rescue
3
+ Plugins.register :rescue, self
4
+
5
+ attr_reader :error
6
+
7
+ def initialize_clone(*)
8
+ remove_instance_variable('@error') if defined?(@error)
9
+ super
10
+ end
11
+
12
+ def execute(**opts)
13
+ rezcue = opts.delete(:rescue)
14
+ super
15
+ rescue StandardError => error
16
+ clear_execution_state!
17
+ @error = error
18
+ @status = :error
19
+ raise error unless rezcue
20
+ self
21
+ end
22
+
23
+ def error?
24
+ status == :error
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ module Excom
2
+ module Plugins::Sentry
3
+ autoload :Sentinel, 'excom/plugins/sentry/sentinel'
4
+
5
+ Plugins.register :sentry, self
6
+
7
+ def self.used(command_class, **opts)
8
+ klass = opts[:class]
9
+
10
+ command_class._sentry_class = klass if klass
11
+ end
12
+
13
+ def execute(*)
14
+ reason = why_cant(:execute)
15
+
16
+ return super if reason.nil?
17
+
18
+ failure!(reason)
19
+
20
+ self
21
+ end
22
+
23
+ def can?(action)
24
+ why_cant(action).nil?
25
+ end
26
+
27
+ def why_cant(action)
28
+ sentry.denial_reason(action)
29
+ end
30
+
31
+ def sentry
32
+ @sentry ||= self.class.sentry_class.new(self)
33
+ end
34
+
35
+ module ClassMethods
36
+ attr_writer :_sentry_class
37
+
38
+ def inherited(command_class)
39
+ super
40
+ command_class.sentry_class(_sentry_class)
41
+ end
42
+
43
+ def sentry_class(klass = nil)
44
+ return self._sentry_class = klass unless klass.nil?
45
+
46
+ if _sentry_class.is_a?(String)
47
+ return _sentry_class.constantize if _sentry_class.respond_to?(:constantize)
48
+
49
+ names = _sentry_class.split('::'.freeze)
50
+ names.shift if names.first.empty?
51
+ names.reduce(Object){ |obj, name| obj.const_get(name) }
52
+ else
53
+ _sentry_class
54
+ end
55
+ end
56
+
57
+ def _sentry_class
58
+ @_sentry_class ||= "#{name}Sentry"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,96 @@
1
+ module Excom
2
+ module Plugins::Sentry
3
+ class Sentinel
4
+ def self.deny_with(reason)
5
+ return self.denial_reason = reason unless block_given?
6
+
7
+ klass = Class.new(self, &Proc.new)
8
+ klass.denial_reason = reason
9
+ sentinels << klass
10
+ end
11
+
12
+ def self.denial_reason=(reason)
13
+ @denial_reason = reason
14
+ end
15
+
16
+ def self.denial_reason
17
+ @denial_reason ||= :denied
18
+ end
19
+
20
+ def self.allow(*actions)
21
+ actions.each do |name|
22
+ define_method("#{name}?") { true }
23
+ end
24
+ end
25
+
26
+ def self.deny(*actions, with: nil)
27
+ return deny_with(with){ deny(*actions) } unless with.nil?
28
+
29
+ actions.each do |name|
30
+ define_method("#{name}?") { false }
31
+ end
32
+ end
33
+
34
+ def self.sentinels
35
+ @sentinels ||= []
36
+ end
37
+
38
+ attr_reader :command
39
+
40
+ def initialize(command)
41
+ @command = command
42
+ end
43
+
44
+ def denial_reason(action)
45
+ method = "#{action}?"
46
+
47
+ reason = sentries.reduce(nil) do |result, sentry|
48
+ result || (sentry.class.denial_reason unless !sentry.respond_to?(method) || sentry.public_send(method))
49
+ end
50
+
51
+ Proc === reason ? instance_exec(&reason) : reason
52
+ end
53
+
54
+ def sentry(klass)
55
+ unless Class === klass
56
+ klass_name = self.class.name.sub(/[^:]+\Z/, ''.freeze) + "_#{klass}".gsub!(/(_([a-z]))/){ $2.upcase } + 'Sentry'.freeze
57
+ klass = klass_name.respond_to?(:constantize) ?
58
+ klass_name.constantize :
59
+ klass_name.split('::'.freeze).reduce(Object){ |obj, name| obj.const_get(name) }
60
+ end
61
+
62
+ klass.new(command)
63
+ end
64
+
65
+ def to_hash
66
+ sentries.reduce({}) do |result, sentry|
67
+ partial = sentry.public_methods(false).grep(/\?$/).each_with_object({}) do |method, hash|
68
+ hash[method.to_s[0...-1]] = !!sentry.public_send(method)
69
+ end
70
+
71
+ result.merge!(partial){ |_k, old, new| old && new }
72
+ end
73
+ end
74
+
75
+ private def sentries
76
+ [self] + sentinels
77
+ end
78
+
79
+ private def sentinels
80
+ @sentinels ||= self.class.sentinels.map do |klass|
81
+ klass.new(command)
82
+ end
83
+ end
84
+
85
+ def method_missing(name, *)
86
+ if name.to_s.end_with?(??)
87
+ sentinels[1..-1].each do |sentry|
88
+ return sentry.public_send(name) if sentry.respond_to?(name)
89
+ end
90
+ end
91
+
92
+ super
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
1
+ module Excom
2
+ module Plugins::StatusHelpers
3
+ Plugins.register :status_helpers, self
4
+
5
+ def self.used(klass, success: [], failure: [])
6
+ klass.alias_success(*success)
7
+
8
+ helpers = Module.new do
9
+ (success + failure).each do |name|
10
+ define_method(name) do |result = nil|
11
+ @status = name
12
+ @result = result
13
+ end
14
+ end
15
+ end
16
+
17
+ klass.const_set('StatusHelpers', helpers)
18
+ klass.send(:include, helpers)
19
+ end
20
+
21
+ def success?
22
+ super || self.class.success_aliases.include?(status)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Excom
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: excom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Artem Kuzko
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-12-03 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.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
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: rspec-its
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-nav
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Flexible and highly extensible Commands (Service Objects) for business
98
+ logic
99
+ email:
100
+ - a.kuzko@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - excom.gemspec
115
+ - lib/excom.rb
116
+ - lib/excom/command.rb
117
+ - lib/excom/plugins.rb
118
+ - lib/excom/plugins/args.rb
119
+ - lib/excom/plugins/assertions.rb
120
+ - lib/excom/plugins/caching.rb
121
+ - lib/excom/plugins/context.rb
122
+ - lib/excom/plugins/executable.rb
123
+ - lib/excom/plugins/pluggable.rb
124
+ - lib/excom/plugins/rescue.rb
125
+ - lib/excom/plugins/sentry.rb
126
+ - lib/excom/plugins/sentry/sentinel.rb
127
+ - lib/excom/plugins/status_helpers.rb
128
+ - lib/excom/version.rb
129
+ homepage: https://github.com/akuzko/excom
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 2.2.0
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.6.8
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Flexible and highly extensible Commands (Service Objects) for business logic
153
+ test_files: []