stack_commander 0.0.1

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: ae92cb3f3a8d18e64d2db4c240119b96e1062e80
4
+ data.tar.gz: d5294e75db6fb3afb27f1d56db44270c38be8840
5
+ SHA512:
6
+ metadata.gz: cccf36d9c8c16c035f3f43aef5d88daf0cb75bd6d0f95a58fd4f0dd3d3817f10fd1b23199f11dba4b8b4163c0b5a0a9475f282fd66dc0a47c8ed8a77d7e5cee5
7
+ data.tar.gz: d93ec0f9f0f91166cac8099d6362ec66bf6d6b32e08a5065df8e8213a18ec9f7b03e900457499bbac34e3e148ca2056fc65a1ea9b91e6ceb8de2fd0ca43d5f89
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stack_commander.gemspec
4
+ gemspec
5
+
6
+ gem 'yard'
7
+ gem 'redcarpet'
8
+ gem 'github-markup'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Michal Cichra
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # StackCommander
2
+
3
+ StackCommander takes fresh approach to Command Pattern in Ruby.
4
+ We needed chaining of Commands with benefits of `rescue` and `ensure` which was hard to do without execution stack.
5
+
6
+ Our use case is mainly to provide rollbacks and finalize blocks for each action so system is not left in unknown state.
7
+
8
+ When doing prototypes one thing came up: how to share state between commands?
9
+
10
+ We tried: passing state to the `action` method, passing state to the `initialize` and leaving user to instantiate commands by hand. Unfortunately each one of them had drawbacks and we decided to focus on following behaviour:
11
+
12
+ * `initialize` is called just before the execution
13
+ * to prevent mistakes of calling something in initializer that might not be there yet
14
+ * or worse, it would be evaluated not on the stack, but before
15
+ * `initialize` is called by the stack with explicit parameters instead of whole state object
16
+ * for easier testing
17
+ * and not to break Law of Demeter
18
+ * command's `action`, `rescue` and `insurance` are called without state parameter
19
+ * command has internal variable `@scope`
20
+
21
+ It is not final, so constructive feedback is very much appreciated.
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ gem 'stack_commander'
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install stack_commander
36
+
37
+ ## Usage
38
+
39
+ ```ruby
40
+ require 'stack_commander'
41
+
42
+ class StackState
43
+ attr_accessor :other
44
+
45
+ def dependency
46
+ 'gotten from somewhere'
47
+ end
48
+ end
49
+
50
+ class MyCommand
51
+ include StackCommander::Command
52
+
53
+ def initialize(dependency)
54
+ @dependency = dependency
55
+ @scope.other = 'yes, other'
56
+ end
57
+
58
+ def action
59
+ puts @dependency
60
+ end
61
+
62
+ def insurance
63
+ puts 'command insurance'
64
+ end
65
+ end
66
+
67
+ class MyOtherCommand
68
+ include StackCommander::Command
69
+
70
+
71
+ def initialize(other)
72
+ @other = other
73
+ end
74
+
75
+ def action
76
+ puts @other
77
+ end
78
+
79
+ def insurance
80
+ puts 'other command insurance'
81
+ end
82
+
83
+ def recover(ex)
84
+ puts 'recovering exception: ' + ex.to_s
85
+ end
86
+ end
87
+
88
+ scope = StackState.new
89
+ stack = StackCommander::Stack.new(scope)
90
+ stack << MyCommand
91
+ stack << MyOtherCommand
92
+ stack.call
93
+ ```
94
+
95
+ outputs:
96
+
97
+ ```
98
+ gotten from somewhere
99
+ yes, other
100
+ other command insurance
101
+ command insurance
102
+ ```
103
+
104
+ ## Contributing
105
+
106
+ 1. Fork it ( https://github.com/mikz/stack_commander/fork )
107
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
108
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
109
+ 4. Push to the branch (`git push origin my-new-feature`)
110
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+
@@ -0,0 +1,8 @@
1
+ require 'stack_commander/version'
2
+
3
+ module StackCommander
4
+ autoload :Stack, 'stack_commander/stack'
5
+ autoload :Command, 'stack_commander/command'
6
+ autoload :BaseCommand, 'stack_commander/base_command'
7
+ autoload :DependencyInjection, 'stack_commander/dependency_injection'
8
+ end
@@ -0,0 +1,7 @@
1
+ require 'stack_commander'
2
+
3
+ module StackCommander
4
+ class BaseCommand
5
+ include StackCommander::Command
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module StackCommander
2
+ module Command
3
+ def call(stack)
4
+ action
5
+ stack.call
6
+ rescue => exception
7
+ recover(exception)
8
+ raise
9
+ ensure
10
+ insurance
11
+ end
12
+
13
+ def action
14
+ end
15
+
16
+ def recover(exception)
17
+ end
18
+
19
+ def insurance
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ module StackCommander
2
+ class DependencyInjection
3
+ InvalidScope = Class.new(StandardError)
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def initialize_parameters
10
+ @klass.instance_method(:initialize).parameters
11
+ end
12
+
13
+ def required_parameters
14
+ _, parameters = initialize_parameters.transpose
15
+ Array(parameters)
16
+ end
17
+
18
+ def matches?(scope)
19
+ required_parameters.all? do |param|
20
+ scope.respond_to?(param)
21
+ end
22
+ end
23
+
24
+ def extract(scope)
25
+ raise InvalidScope, scope unless matches?(scope)
26
+
27
+ required_parameters.map do |param|
28
+ scope.public_send(param)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ require 'thread'
2
+
3
+ module StackCommander
4
+ class Stack
5
+ def initialize(scope)
6
+ @queue = Queue.new
7
+ @scope = scope
8
+ end
9
+
10
+ def size
11
+ @queue.length
12
+ end
13
+
14
+ def <<(command)
15
+ @queue.push(command)
16
+
17
+ self # for chaining awesomeness << cmd << cmd2 << cmd3
18
+ end
19
+
20
+ def call
21
+ return if @queue.empty?
22
+ command_class = @queue.pop
23
+
24
+ run_command(command_class)
25
+ end
26
+
27
+ private
28
+
29
+ def run_command(klass)
30
+ parameters = StackCommander::DependencyInjection.new(klass).extract(@scope)
31
+
32
+ command = klass.allocate
33
+ command.instance_variable_set :@scope, @scope
34
+ command.__send__(:initialize, *parameters)
35
+
36
+ command.call(self)
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,3 @@
1
+ module StackCommander
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'stack_commander/base_command'
3
+
4
+ describe StackCommander::BaseCommand do
5
+ it 'has command module' do
6
+ expect(subject.class.ancestors).to include(StackCommander::Command)
7
+ end
8
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'stack_commander/command'
3
+
4
+ describe StackCommander::Command do
5
+ expect_it { to be_a(Module) }
6
+
7
+ expect_it { to_not be_a(Class) }
8
+
9
+ context 'object including command' do
10
+ subject(:command) { Class.new{ include StackCommander::Command }.new }
11
+
12
+ expect_it { to respond_to(:action) }
13
+ expect_it { to respond_to(:insurance) }
14
+ expect_it { to respond_to(:recover).with(1).argument }
15
+
16
+
17
+ let(:stack) { double('stack').as_null_object }
18
+
19
+ it 'calls action' do
20
+ expect(subject).to receive(:action)
21
+ subject.call(stack)
22
+ end
23
+
24
+ it 'calls stack' do
25
+ expect(stack).to receive(:call)
26
+ subject.call(stack)
27
+ end
28
+
29
+ it 'calls insurance' do
30
+ expect(subject).to receive(:insurance)
31
+ subject.call(stack)
32
+ end
33
+
34
+ it 'does not call recover' do
35
+ expect(subject).not_to receive(:recover)
36
+ subject.call(stack)
37
+ end
38
+
39
+ context 'exception is raised' do
40
+
41
+ before do
42
+ expect(stack).to receive(:call).and_raise(StandardError)
43
+ end
44
+
45
+ it 'still calls insurance' do
46
+ expect(subject).to receive(:insurance)
47
+ expect { subject.call(stack) }.to raise_error(StandardError)
48
+ end
49
+
50
+ it 'calls recover' do
51
+ expect(subject).to receive(:recover).with(an_instance_of(StandardError))
52
+ expect{ subject.call(stack) }.to raise_error(StandardError)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'stack_commander/dependency_injection'
3
+
4
+ describe StackCommander::DependencyInjection do
5
+
6
+ let(:custom_class) { Class.new { def initialize(one, two); end } }
7
+ subject(:di) { StackCommander::DependencyInjection.new(custom_class) }
8
+
9
+ it 'extracts parameter names from initializer' do
10
+ expect(di.required_parameters).to eq([:one, :two])
11
+ end
12
+
13
+ context 'scope has required methods' do
14
+ let(:scope) { double('scope', one: 'one value', two: 'two') }
15
+
16
+ it do
17
+ expect(di.matches?(scope)).to be
18
+ end
19
+
20
+ it do
21
+ expect(di.extract(scope)).to eq(['one value', 'two'])
22
+ end
23
+ end
24
+
25
+ context 'scope does not have requried methods' do
26
+ let(:scope) { double('scope', one: true) }
27
+
28
+ it do
29
+ expect(di.matches?(scope)).not_to be
30
+ end
31
+
32
+ it do
33
+ expect{ di.extract(scope) }.to raise_error(StackCommander::DependencyInjection::InvalidScope)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+
18
+ config.alias_example_to :expect_it
19
+ end
20
+
21
+ RSpec::Core::MemoizedHelpers.module_eval do
22
+ alias to should
23
+ alias to_not should_not
24
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'stack_commander/stack'
3
+
4
+ describe StackCommander::Stack do
5
+
6
+ let(:scope) { double('scope') }
7
+ subject(:stack) { StackCommander::Stack.new(scope) }
8
+
9
+ it 'requires scope' do
10
+ expect { StackCommander::Stack.new }.to raise_error(ArgumentError)
11
+ end
12
+
13
+ it 'enqueues commands' do
14
+ expect(stack.size).to eq(0)
15
+ stack << double('command')
16
+ expect(stack.size).to eq(1)
17
+ end
18
+
19
+ context 'without commands' do
20
+ it 'runs' do
21
+ stack.call
22
+ end
23
+ end
24
+
25
+ context 'with commands' do
26
+ let(:command) { double('command').as_null_object }
27
+
28
+ before do
29
+ stack << command
30
+ end
31
+
32
+ it 'runs the commands' do
33
+ expect(command).to receive(:call).with(stack)
34
+ stack.call
35
+ end
36
+
37
+ it 'initializes the command' do
38
+ expect(command).to receive(:new).with
39
+ stack.call
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'stack_commander/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'stack_commander'
8
+ spec.version = StackCommander::VERSION
9
+ spec.authors = ['Michal Cichra']
10
+ spec.email = %w[michal@3scale.net]
11
+ spec.summary = %q{Mix between command pattern and stack execution}
12
+ spec.description = %q{Write simple commands that execute on stack}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake'
23
+
24
+ spec.add_development_dependency 'rspec'
25
+ spec.add_development_dependency 'yard'
26
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stack_commander
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Michal Cichra
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-08 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.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '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: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Write simple commands that execute on stack
70
+ email:
71
+ - michal@3scale.net
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rspec
78
+ - .yardopts
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - lib/stack_commander.rb
84
+ - lib/stack_commander/base_command.rb
85
+ - lib/stack_commander/command.rb
86
+ - lib/stack_commander/dependency_injection.rb
87
+ - lib/stack_commander/stack.rb
88
+ - lib/stack_commander/version.rb
89
+ - spec/base_command_spec.rb
90
+ - spec/command_spec.rb
91
+ - spec/dependency_injection_spec.rb
92
+ - spec/spec_helper.rb
93
+ - spec/stack_spec.rb
94
+ - stack_commander.gemspec
95
+ homepage: ''
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.2.2
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Mix between command pattern and stack execution
119
+ test_files:
120
+ - spec/base_command_spec.rb
121
+ - spec/command_spec.rb
122
+ - spec/dependency_injection_spec.rb
123
+ - spec/spec_helper.rb
124
+ - spec/stack_spec.rb
125
+ has_rdoc: