adama 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +8 -0
- data/README.md +133 -0
- data/Rakefile +6 -0
- data/adama.gemspec +36 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/adama/command.rb +53 -0
- data/lib/adama/errors.rb +16 -0
- data/lib/adama/invoker.rb +116 -0
- data/lib/adama/version.rb +3 -0
- data/lib/adama.rb +4 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4e62f6593e1502cfac772a33f167c0bbac14635c
|
4
|
+
data.tar.gz: 2eda051000fc7e546346b3db4eda8a7830542dee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8e2c2916e4431d88986df3fc430d0e09ff6a0868011cb30310c2af39b468dc542c74dc74083fdd6c8011f446355d5a9a29f6de1591578dda9618c53dbb78b711
|
7
|
+
data.tar.gz: 504500aec933751018fb61dac3d943ed3e5654cca601de17063ddbe4d1ac69f20582d7e49d7740d08a4afd0edecd4e315e10acfcc7814c4a81e78c9c7da64a1e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
MIT License
|
2
|
+
Copyright (c) 2017 Bugcrowd, Inc.
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
5
|
+
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
7
|
+
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# Commander Adama
|
2
|
+
|
3
|
+
Commander Adama is a bare bones command pattern library inspired by Collective Idea's [Interactor](https://github.com/collectiveidea/interactor) gem.
|
4
|
+
|
5
|
+
Commands are small classes that represent individual units of work. Each command is executed by a client "calling" it. An invoker is a class responsible for the execution of one or more commands.
|
6
|
+
|
7
|
+
## Getting Started
|
8
|
+
|
9
|
+
Add Commander Adama to your Gemfile and `bundle install`.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'adama'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
### Command
|
18
|
+
|
19
|
+
To create a command, include the `Adama::Command` module in your command's class definition:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class DestroyCylons
|
23
|
+
include Adama::Command
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
Including the `Adama::Command` module extends the class with the `.call` class method. So you would execute the command like this:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
DestroyCylons.call(captain: :apollo)
|
31
|
+
```
|
32
|
+
|
33
|
+
The above `.call` method creates an instance of the `DestroyCylons` class, then calls the `#call` instance method. If the `#call` method fails, the `#rollback` method is then called.
|
34
|
+
|
35
|
+
At this point our command `DestroyCylons` doesn't do much. As explained above, the `Adama::Command` module has two instance methods: `call` and `rollback`. By default these methods are empty and should be overridden like this:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class DestroyCylons
|
39
|
+
include Adama::Command
|
40
|
+
|
41
|
+
def call
|
42
|
+
got_destroy_cylons(kwargs[:captain])
|
43
|
+
end
|
44
|
+
|
45
|
+
def rollback
|
46
|
+
retreat_and_jump_away()
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
The kwargs are available within the `#call` and `#rollback` instance methods due to an `attr_reader` in the `Adama::Command` module.
|
52
|
+
|
53
|
+
### Invoker
|
54
|
+
|
55
|
+
To create an invoker, include the `Adama::Invoker` module in your invoker's class definition:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class RebuildHumanRace
|
59
|
+
include Adama::Invoker
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Because the `Adama::Invoker` module extends `Adama::Command` you can execute an invoker in the exact same way you execute a command, with the `.call` class method:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
RebuildHumanRace.call(captain: :apollo, president: :laura)
|
67
|
+
```
|
68
|
+
|
69
|
+
The `Adama::Invoker` module _also_ extends your invoker class with the `.invoke` class method, which allows you to specify a list of commands to run in sequence, e.g.:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class RebuildHumanRace
|
73
|
+
include Adama::Invoker
|
74
|
+
|
75
|
+
invoke(
|
76
|
+
GetArrowOfApollo,
|
77
|
+
DestroyCylons,
|
78
|
+
FindEarth,
|
79
|
+
)
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Now, when you run `RebuildHumanRace.call(captain: :apollo, president: :laura)` it will execute `GetArrowOfApollo`, then `DestroyCylons`, then finally `FindEarth` commands in order.
|
84
|
+
|
85
|
+
If there is an error in any of those commands, the invoker will call `FindEarth.rollback`, then `DestroyCylons.rollback`, then `GetArrowOfApollo.rollback` leaving everything just as it was in the beginning.
|
86
|
+
|
87
|
+
### Errors
|
88
|
+
|
89
|
+
`Adama::Command#call` or `Adama::Invoker#call` will *always* raise an error of type `Adama::Errors::BaseError`.
|
90
|
+
|
91
|
+
More specifically:
|
92
|
+
|
93
|
+
If a command fails, it will raise `Adama::Errors::CommandError`.
|
94
|
+
|
95
|
+
If a command fails while being called in an invoker, the commands will be rolled back and the invoker will raise `Adama::Errors::InvokerError`.
|
96
|
+
|
97
|
+
If a command fails while rolling back within the invoker, the invoker will raise `Adama::Errors::InvokerRollbackError`.
|
98
|
+
|
99
|
+
The base error type `Adama::Errors::Adama` is designed to be initialized with three optional keyword args:
|
100
|
+
|
101
|
+
`error` - the original exception that was rescued in the command or invoker.
|
102
|
+
`command` - the failed command instance.
|
103
|
+
`invoker` - the failed invoker instance, set if the command or rollback failed in an invoker.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
module Adama
|
107
|
+
module Errors
|
108
|
+
class BaseError < StandardError
|
109
|
+
attr_reader :error, :command, :invoker
|
110
|
+
|
111
|
+
def initialize(error: nil, command: nil, invoker: nil)
|
112
|
+
@error = error
|
113
|
+
@command = command
|
114
|
+
@invoker = invoker
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
### TODOS
|
122
|
+
|
123
|
+
I'm contemplating adding support for per-command validation, potentially through the [dry-validation](https://github.com/dry-rb/dry-validation) gem,
|
124
|
+
|
125
|
+
## Development
|
126
|
+
|
127
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
128
|
+
|
129
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bugcrowd/adama. So Say We All.
|
data/Rakefile
ADDED
data/adama.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'adama/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "adama"
|
8
|
+
spec.version = Adama::VERSION
|
9
|
+
spec.licenses = ['MIT']
|
10
|
+
spec.authors = ["dradford"]
|
11
|
+
spec.email = ["damienradford@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = %q{Commander of the fleet.}
|
14
|
+
spec.description = %q{Command and Invoker pattern.}
|
15
|
+
spec.homepage = "https://github.com/bugcrowd/adama"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
34
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
36
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "adama"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Adama
|
2
|
+
# Extend class with module methods
|
3
|
+
#
|
4
|
+
# class CollectUnderpantsCommand
|
5
|
+
# include Adama::Command
|
6
|
+
#
|
7
|
+
# def call
|
8
|
+
# got_get_underpants()
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# def rollback
|
12
|
+
# return_underpants_to_rightful_owner()
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
module Command
|
16
|
+
# Internal: Install Command's behavior in the given class.
|
17
|
+
def self.included(base)
|
18
|
+
base.class_eval do
|
19
|
+
extend ClassMethods
|
20
|
+
attr_reader :kwargs
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
# public invoke a command
|
26
|
+
def call(kwargs = {})
|
27
|
+
new(kwargs).tap(&:run)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(kwargs = {})
|
32
|
+
@kwargs = kwargs
|
33
|
+
end
|
34
|
+
|
35
|
+
# Internal instance method. Called by both the call class method, and by
|
36
|
+
# the call method in the invoker. If it fails it rolls back the command
|
37
|
+
# and raises a CommandError.
|
38
|
+
def run(enable_rollback: true)
|
39
|
+
call
|
40
|
+
rescue => error
|
41
|
+
rollback if enable_rollback
|
42
|
+
raise Errors::CommandError.new error: error, command: self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public instance method. Override this in classes this module is
|
46
|
+
# included in.
|
47
|
+
def call; end
|
48
|
+
|
49
|
+
# Public instance method. Override this in classes this module is
|
50
|
+
# included in.
|
51
|
+
def rollback; end
|
52
|
+
end
|
53
|
+
end
|
data/lib/adama/errors.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Adama
|
2
|
+
module Errors
|
3
|
+
class BaseError < StandardError
|
4
|
+
attr_reader :error, :command, :invoker
|
5
|
+
|
6
|
+
def initialize(error: nil, command: nil, invoker: nil)
|
7
|
+
@error = error
|
8
|
+
@command = command
|
9
|
+
@invoker = invoker
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class CommandError < BaseError; end
|
13
|
+
class InvokerError < BaseError; end
|
14
|
+
class InvokerRollbackError < BaseError; end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Adama
|
2
|
+
# Invoker lets you run many commands in sequence, and roll them back
|
3
|
+
# in reverse order.
|
4
|
+
#
|
5
|
+
# class SuccessfulBusinessCreator
|
6
|
+
# include Adama::Invoker
|
7
|
+
#
|
8
|
+
# invoke(
|
9
|
+
# CollectUnderpantsCommand,
|
10
|
+
# MagicHappensCommand,
|
11
|
+
# ProfitCommand,
|
12
|
+
# )
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# SuccessfulBusinessCreator.call(min_underpants: 100)
|
16
|
+
#
|
17
|
+
module Invoker
|
18
|
+
# Internal: Install Command's behavior in the given class.
|
19
|
+
def self.included(base)
|
20
|
+
base.class_eval do
|
21
|
+
# We inherit the Command module's call methods
|
22
|
+
include Command
|
23
|
+
|
24
|
+
# Our new class methods enable us to set the command list
|
25
|
+
extend ClassMethods
|
26
|
+
|
27
|
+
# We override the Command class instance methods:
|
28
|
+
#
|
29
|
+
# run
|
30
|
+
# call
|
31
|
+
# rollback
|
32
|
+
include InstanceMethods
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Our new class methods enable us to set the command list
|
37
|
+
module ClassMethods
|
38
|
+
|
39
|
+
# Public class method. Call invoke in your class definition to
|
40
|
+
# specify which commands will be executed.
|
41
|
+
#
|
42
|
+
# class SuccessfulBusinessCreator
|
43
|
+
# include Adama::Invoker
|
44
|
+
#
|
45
|
+
# invoke(
|
46
|
+
# CollectUnderpantsCommand,
|
47
|
+
# MagicHappensCommand,
|
48
|
+
# ProfitCommand,
|
49
|
+
# )
|
50
|
+
# end
|
51
|
+
def invoke(*command_list)
|
52
|
+
@commands = command_list.flatten
|
53
|
+
end
|
54
|
+
|
55
|
+
# internal class method. So we can loop through the commands that
|
56
|
+
# have been assigned by the including class.
|
57
|
+
def commands
|
58
|
+
@commands ||= []
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module InstanceMethods
|
63
|
+
# Internal instance method. Called by the included Command module's .call
|
64
|
+
# class method. We've overridden command's instance method because we
|
65
|
+
# don't want it to have optional rollback.
|
66
|
+
#
|
67
|
+
# Always raises Errors::InvokerError and has the #invoker and #error
|
68
|
+
# attribute set. In the case where the error is raised within the
|
69
|
+
# invoker "call" instance method, we won't have access to error's
|
70
|
+
# command so need to test for it's existence.
|
71
|
+
def run
|
72
|
+
call
|
73
|
+
rescue => error
|
74
|
+
rollback
|
75
|
+
raise Errors::InvokerError.new(
|
76
|
+
error: error,
|
77
|
+
command: error.respond_to?(:command) ? error.command : nil,
|
78
|
+
invoker: self
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Maintain an array of commands that have been called.
|
83
|
+
def _called
|
84
|
+
@called ||= []
|
85
|
+
end
|
86
|
+
|
87
|
+
# To unwind the invoker, we rollback the _called array in reverse order.
|
88
|
+
#
|
89
|
+
# If anything fails in the command's rollback method we should raise
|
90
|
+
# and drop out of the process as we'll need to manually remove something.
|
91
|
+
def rollback
|
92
|
+
_called.reverse_each do |command|
|
93
|
+
begin
|
94
|
+
command.rollback
|
95
|
+
rescue => error
|
96
|
+
raise Errors::InvokerRollbackError.new(error: error, command: command, invoker: self)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Iterate over the commands array, instantiate each command, add it to
|
102
|
+
# the called list and then run it. We don't want the command to call
|
103
|
+
# rollback itself as that will be handled by the rollback method above.
|
104
|
+
# To ensure this doesn't happen we pass in enable_rollback: false.
|
105
|
+
# Please ensure the command is placed on the array _prior_ to calling
|
106
|
+
# run, or else we'll miss rolling back the command that failed.
|
107
|
+
def call
|
108
|
+
self.class.commands.each do |command_klass|
|
109
|
+
command = command_klass.new(kwargs)
|
110
|
+
_called << command
|
111
|
+
command.run(enable_rollback: false)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/adama.rb
ADDED
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adama
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- dradford
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-24 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.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
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
|
+
description: Command and Invoker pattern.
|
56
|
+
email:
|
57
|
+
- damienradford@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- adama.gemspec
|
70
|
+
- bin/console
|
71
|
+
- bin/setup
|
72
|
+
- lib/adama.rb
|
73
|
+
- lib/adama/command.rb
|
74
|
+
- lib/adama/errors.rb
|
75
|
+
- lib/adama/invoker.rb
|
76
|
+
- lib/adama/version.rb
|
77
|
+
homepage: https://github.com/bugcrowd/adama
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata:
|
81
|
+
allowed_push_host: https://rubygems.org
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.5.1
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Commander of the fleet.
|
102
|
+
test_files: []
|