excom 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []