adama 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: 4e62f6593e1502cfac772a33f167c0bbac14635c
4
+ data.tar.gz: 2eda051000fc7e546346b3db4eda8a7830542dee
5
+ SHA512:
6
+ metadata.gz: 8e2c2916e4431d88986df3fc430d0e09ff6a0868011cb30310c2af39b468dc542c74dc74083fdd6c8011f446355d5a9a29f6de1591578dda9618c53dbb78b711
7
+ data.tar.gz: 504500aec933751018fb61dac3d943ed3e5654cca601de17063ddbe4d1ac69f20582d7e49d7740d08a4afd0edecd4e315e10acfcc7814c4a81e78c9c7da64a1e
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
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in adama.gemspec
4
+ gemspec
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
@@ -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/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,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,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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Adama
2
+ VERSION = "0.1.0"
3
+ end
data/lib/adama.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "adama/version"
2
+ require "adama/errors"
3
+ require "adama/command"
4
+ require "adama/invoker"
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: []