composable_operations 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: 2815a0e9caf243a6e502b0f277cb98c36b44b9d5
4
+ data.tar.gz: 668f9e8de07e9287f7498909fca06f2d61ab4904
5
+ SHA512:
6
+ metadata.gz: 962cef4189be9cabea7c2df388d1f6be24929a211eafe43f9fadddec9ff4375fd553a977a01641ee812fb1e887fc2d62853bdb3eb29e2636b336f10899d3473f
7
+ data.tar.gz: e3f3d3df80f08542fa285e27fbe7d4ca9a7c217a41f0d74dfa97e737dc95e9d6742877ec08d2f5f41ccc9f28ac75bffc6b419a33ca1983d6047ea76dfe36db8f
data/.gitignore ADDED
@@ -0,0 +1,18 @@
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
+ .rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in composable_operations.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Konstantin Tennhard
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,29 @@
1
+ # ComposableOperations
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'composable_operations'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install composable_operations
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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 'composable_operations/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "composable_operations"
8
+ spec.version = ComposableOperations::VERSION
9
+ spec.authors = ["Konstantin Tennhard"]
10
+ spec.email = ["me@t6d.de"]
11
+ spec.summary = %q{Tool set for operation pipelines.}
12
+ spec.description = %q{Composable Operations is a tool set for creating easy-to-use operation pipelines.}
13
+ spec.homepage = "http://github.com/t6d/composable_operations"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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_dependency "smart_properties", "~> 1.0"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", "~> 2.11"
26
+ end
@@ -0,0 +1,11 @@
1
+ require 'smart_properties'
2
+
3
+ module ComposableOperations
4
+ # Your code goes here...
5
+ end
6
+
7
+ require_relative "composable_operations/version"
8
+ require_relative "composable_operations/operation_error"
9
+ require_relative "composable_operations/operation"
10
+ require_relative "composable_operations/composed_operation"
11
+
@@ -0,0 +1,78 @@
1
+ module ComposableOperations
2
+ class ComposedOperation < Operation
3
+ class << self
4
+
5
+ def operations
6
+ [] + Array((super if defined? super)) + Array(@operations)
7
+ end
8
+
9
+ def use(operation)
10
+ (@operations ||= []) << operation
11
+ end
12
+
13
+ def compose(*operations, &block)
14
+ raise ArgumentError, "Expects either an array of operations or a block with configuration instructions" unless !!block ^ !operations.empty?
15
+
16
+ if block
17
+ Class.new(self, &block)
18
+ else
19
+ Class.new(self) do
20
+ operations.each do |operation|
21
+ use operation
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def transitions
28
+ transitions = []
29
+ klass = self
30
+ while klass != Operation
31
+ klass = klass.superclass
32
+ transitions += Array(klass.instance_variable_get(:@transitions))
33
+ end
34
+ transitions += Array(@transitions)
35
+ transitions
36
+ end
37
+
38
+
39
+ protected
40
+
41
+ def between(&callback)
42
+ (@transitions ||= []) << callback
43
+ end
44
+
45
+ end
46
+
47
+ def operations
48
+ self.class.operations
49
+ end
50
+
51
+ protected
52
+
53
+ def execute
54
+ [nil, *operations, nil].each_cons(2).inject(input) do |data, operations|
55
+ if operation = operations.last
56
+ operation = operation.new(data)
57
+ operation.perform
58
+
59
+ if operation.failed?
60
+ fail operation.message, operation.result, operation.backtrace
61
+ elsif operation.halted?
62
+ halt operation.message, operation.result
63
+ end
64
+
65
+ transition(*operations, data) if operations.first && operations.last
66
+ operation.result
67
+ else
68
+ data
69
+ end
70
+ end
71
+ end
72
+
73
+ def transition(a, b, payload)
74
+ self.class.transitions.each { |transition| instance_exec(a, b, payload, &transition) }
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'matcher/fail_to_perform'
2
+ require_relative 'matcher/succeed_to_perform'
3
+ require_relative 'matcher/utilize_operation'
4
+
@@ -0,0 +1,83 @@
1
+ module ComposableOperations
2
+ module Matcher
3
+ module FailToPerform
4
+ class Matcher
5
+
6
+ def matches?(operation)
7
+ self.operation = operation
8
+ failed? && result_as_expected? && message_as_expected?
9
+ end
10
+
11
+ def because(message)
12
+ @message = message
13
+ self
14
+ end
15
+
16
+ def and_return(result)
17
+ @result = result
18
+ self
19
+ end
20
+
21
+ def description
22
+ description = "fail to perform"
23
+ description += " because #{message}" if message
24
+ description += " and return the expected result" if result
25
+ description
26
+ end
27
+
28
+ def failure_message
29
+ "the operation did not fail to perform for the following reason(s):\n#{failure_reasons}"
30
+ end
31
+
32
+ def negative_failure_message
33
+ "the operation failed unexpectedly"
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :operation
39
+ attr_reader :message
40
+ attr_reader :result
41
+
42
+ def operation=(operation)
43
+ operation.perform
44
+ @operation = operation
45
+ end
46
+
47
+ private
48
+
49
+ def failed?
50
+ operation.failed?
51
+ end
52
+
53
+ def message_as_expected?
54
+ return true unless message
55
+ operation.message == message
56
+ end
57
+
58
+ def result_as_expected?
59
+ return true unless result
60
+ operation.result == result
61
+ end
62
+
63
+ def failure_reasons
64
+ reasons = []
65
+ reasons << "it did not fail at all" unless failed?
66
+ reasons << "its message was not as expected" unless message_as_expected?
67
+ reasons << "it did not return the expected result" unless result_as_expected?
68
+ reasons.map { |r| "\t- #{r}" }.join("\n")
69
+ end
70
+
71
+ end
72
+
73
+ def fail_to_perform
74
+ Matcher.new
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+
81
+ RSpec.configure do |config|
82
+ config.include ComposableOperations::Matcher::FailToPerform
83
+ end
@@ -0,0 +1,70 @@
1
+ module ComposableOperations
2
+ module Matcher
3
+ module SucceedToPerform
4
+ class Matcher
5
+
6
+ def matches?(operation)
7
+ self.operation = operation
8
+ succeeded? && result_as_expected?
9
+ end
10
+
11
+ def and_return(result)
12
+ @result = result
13
+ self
14
+ end
15
+
16
+ def description
17
+ description = "succeed to perform"
18
+ description += " and return the expected result" if result
19
+ description
20
+ end
21
+
22
+ def failure_message
23
+ "the operation failed to perform for the following reason(s):\n#{failure_reasons}"
24
+ end
25
+
26
+ def negative_failure_message
27
+ "the operation succeeded unexpectedly"
28
+ end
29
+
30
+ protected
31
+
32
+ attr_reader :operation
33
+ attr_reader :result
34
+
35
+ def operation=(operation)
36
+ operation.perform
37
+ @operation = operation
38
+ end
39
+
40
+ private
41
+
42
+ def succeeded?
43
+ operation.succeeded?
44
+ end
45
+
46
+ def result_as_expected?
47
+ return true unless result
48
+ operation.result == result
49
+ end
50
+
51
+ def failure_reasons
52
+ reasons = []
53
+ reasons << "it did not succeed at all" unless succeeded?
54
+ reasons << "it did not return the expected result" unless result_as_expected?
55
+ reasons.map { |r| "\t- #{r}" }.join("\n")
56
+ end
57
+
58
+ end
59
+
60
+ def succeed_to_perform
61
+ Matcher.new
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ RSpec.configure do |config|
68
+ config.include ComposableOperations::Matcher::SucceedToPerform
69
+ end
70
+
@@ -0,0 +1,92 @@
1
+ module ComposableOperations
2
+ module Matcher
3
+ module UtilizeOperation
4
+
5
+ class DummyOperation
6
+ def initialize(*args)
7
+ end
8
+
9
+ def failed?
10
+ false
11
+ end
12
+
13
+ def halted?
14
+ false
15
+ end
16
+
17
+ def succeeded?
18
+ true
19
+ end
20
+
21
+ def result
22
+ Object.new
23
+ end
24
+
25
+ def perform
26
+ result
27
+ end
28
+ end
29
+
30
+ class Matcher
31
+
32
+ attr_reader :composite_operations
33
+ attr_reader :tested_operation
34
+ attr_reader :tested_instance
35
+
36
+ def initialize(*composite_operations)
37
+ @composite_operations = composite_operations.flatten
38
+ end
39
+
40
+ def matches?(class_or_instance)
41
+ if class_or_instance.is_a?(Class)
42
+ @tested_operation = class_or_instance
43
+ @tested_instance = class_or_instance.new
44
+ else
45
+ @tested_operation = class_or_instance.class
46
+ @tested_instance = class_or_instance
47
+ end
48
+
49
+ Operation.stub(:new => DummyOperation.new)
50
+ composite_operations.each do |composite_operation|
51
+ dummy_operation = DummyOperation.new
52
+ dummy_operation.should_receive(:perform).and_call_original
53
+ composite_operation.should_receive(:new).and_return(dummy_operation)
54
+ end
55
+ tested_instance.stub(:prepare => true, :finalize => true)
56
+ tested_instance.perform
57
+
58
+ tested_operation.operations == composite_operations
59
+ end
60
+
61
+ def description
62
+ "utilize the following operations: #{composite_operations.map(&:to_s).join(', ')}"
63
+ end
64
+
65
+ def failure_message
66
+ expected_but_not_used = composite_operations - tested_operation.operations
67
+ used_but_not_exptected = tested_operation.operations - composite_operations
68
+ message = ["Unexpected operation utilization:"]
69
+ message << "Expected: #{expected_but_not_used.join(', ')}" unless expected_but_not_used.empty?
70
+ message << "Not expected: #{used_but_not_exptected.join(', ')}" unless used_but_not_exptected.empty?
71
+ message.join("\n\t")
72
+ end
73
+
74
+ def negative_failure_message
75
+ "Unexpected operation utilization"
76
+ end
77
+
78
+ end
79
+
80
+ def utilize_operation(*args)
81
+ Matcher.new(*args)
82
+ end
83
+ alias utilize_operations utilize_operation
84
+
85
+ end
86
+ end
87
+ end
88
+
89
+ RSpec.configure do |config|
90
+ config.include ComposableOperations::Matcher::UtilizeOperation
91
+ end
92
+
@@ -0,0 +1,165 @@
1
+ module ComposableOperations
2
+ class Operation
3
+
4
+ include SmartProperties
5
+
6
+ class << self
7
+
8
+ def perform(*args)
9
+ operation = new(*args)
10
+ operation.perform
11
+
12
+ raise exception, operation.message, operation.backtrace if operation.failed?
13
+
14
+ operation.result
15
+ end
16
+
17
+ def preparators
18
+ preparators = []
19
+ klass = self
20
+ while klass != Operation
21
+ klass = klass.superclass
22
+ preparators += Array(klass.instance_variable_get(:@preparators))
23
+ end
24
+ preparators += Array(@preparators)
25
+ preparators
26
+ end
27
+
28
+ def finalizers
29
+ finalizers = []
30
+ klass = self
31
+ while klass != Operation
32
+ klass = klass.superclass
33
+ finalizers += Array(klass.instance_variable_get(:@finalizers))
34
+ end
35
+ finalizers += Array(@finalizers)
36
+ finalizers
37
+ end
38
+
39
+ def exception
40
+ @exception or defined?(super) ? super : OperationError
41
+ end
42
+
43
+ protected
44
+
45
+ def before(&callback)
46
+ (@preparators ||= []) << callback
47
+ end
48
+
49
+ def after(&callback)
50
+ (@finalizers ||= []) << callback
51
+ end
52
+
53
+ def processes(*names)
54
+ case names.length
55
+ when 0
56
+ raise ArgumentError, "#{self}.#{__callee__} expects at least one argument"
57
+ when 1
58
+ alias_method names[0].to_sym, :input
59
+ else
60
+ names.each_with_index do |name, index|
61
+ define_method(name) { input[index] }
62
+ end
63
+ end
64
+ end
65
+
66
+ def raises(exception)
67
+ @exception = exception
68
+ end
69
+
70
+ private
71
+
72
+ def method_added(method)
73
+ super
74
+ protected method if method == :execute
75
+ end
76
+
77
+ end
78
+
79
+ attr_reader :input
80
+ attr_reader :result
81
+ attr_reader :message
82
+ attr_reader :backtrace
83
+
84
+ def initialize(input = nil, options = {})
85
+ super(options)
86
+ @input = input
87
+ end
88
+
89
+ def failed?
90
+ state == :failed
91
+ end
92
+
93
+ def halted?
94
+ state == :halted
95
+ end
96
+
97
+ def succeeded?
98
+ state == :succeeded
99
+ end
100
+
101
+ def message?
102
+ message.present?
103
+ end
104
+
105
+ def name
106
+ self.class.name
107
+ end
108
+
109
+ def perform
110
+ self.result = catch(:halt) do
111
+ prepare
112
+ result = execute
113
+ self.state = :succeeded
114
+ result
115
+ end
116
+
117
+ finalize
118
+
119
+ self.result
120
+ end
121
+
122
+ protected
123
+
124
+ attr_accessor :state
125
+
126
+ attr_writer :message
127
+ attr_writer :result
128
+ attr_writer :backtrace
129
+
130
+ def execute
131
+ raise NotImplementedError, "#{name}#execute not implemented"
132
+ end
133
+
134
+ def fail(message = nil, return_value = nil, backtrace = caller)
135
+ raise "Operation execution has already been aborted" if halted? or failed?
136
+
137
+ self.state = :failed
138
+ self.backtrace = backtrace
139
+ self.message = message
140
+ throw :halt, return_value
141
+ end
142
+
143
+ def halt(message = nil, return_value = input)
144
+ raise "Operation execution has already been aborted" if halted? or failed?
145
+
146
+ self.state = :halted
147
+ self.message = message
148
+ throw :halt, return_value
149
+ end
150
+
151
+ def prepare
152
+ self.class.preparators.each { |preparator| instance_eval(&preparator) }
153
+ end
154
+
155
+ def finalize
156
+ self.class.finalizers.each do |finalizer|
157
+ self.result = catch(:halt) do
158
+ instance_eval(&finalizer)
159
+ self.result
160
+ end
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,4 @@
1
+ module ComposableOperations
2
+ class OperationError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module ComposableOperations
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,135 @@
1
+ require "spec_helper"
2
+
3
+ describe ComposableOperations::ComposedOperation do
4
+
5
+ let(:string_generator) do
6
+ Class.new(ComposableOperations::Operation) do
7
+ def self.name
8
+ "StringGenerator"
9
+ end
10
+
11
+ def execute
12
+ "chunky bacon"
13
+ end
14
+ end
15
+ end
16
+
17
+ let(:string_capitalizer) do
18
+ Class.new(ComposableOperations::Operation) do
19
+ def self.name
20
+ "StringCapitalizer"
21
+ end
22
+
23
+ def execute
24
+ input.upcase
25
+ end
26
+ end
27
+ end
28
+
29
+ let(:halting_operation) do
30
+ Class.new(ComposableOperations::Operation) do
31
+ def execute
32
+ halt
33
+ end
34
+ end
35
+ end
36
+
37
+ context "when composed of one operation that generates a string no matter the input" do
38
+
39
+ subject(:composed_operation) do
40
+ operation = string_generator
41
+
42
+ Class.new(described_class) do
43
+ use operation
44
+ end
45
+ end
46
+
47
+ it "should return this string as result" do
48
+ composed_operation.perform(nil).should be == "chunky bacon"
49
+ end
50
+
51
+ end
52
+
53
+ context "when composed of two operations using the factory method '#chain'" do
54
+
55
+ subject(:composed_operation) do
56
+ described_class.compose(string_generator, string_capitalizer).new
57
+ end
58
+
59
+ it { should succeed_to_perform.and_return("CHUNKY BACON") }
60
+
61
+ it { should utilize_operations(string_generator, string_capitalizer) }
62
+
63
+ end
64
+
65
+ context "when composed of two operations, one that generates a string and one that capitalizes strings, " do
66
+
67
+ subject(:composed_operation) do
68
+ operations = [string_generator, string_capitalizer]
69
+
70
+ Class.new(described_class) do
71
+ use operations.first
72
+ use operations.last
73
+ end
74
+ end
75
+
76
+ it "should return a capitalized version of the generated string" do
77
+ composed_operation.perform(nil).should be == "CHUNKY BACON"
78
+ end
79
+
80
+ it { should utilize_operations(string_generator, string_capitalizer) }
81
+
82
+ end
83
+
84
+ context "when composed of three operations, one that generates a string, one that halts and one that capatalizes strings" do
85
+
86
+ subject(:composed_operation) do
87
+ described_class.compose(string_generator, halting_operation, string_capitalizer)
88
+ end
89
+
90
+ it "should return a capitalized version of the generated string" do
91
+ composed_operation.perform.should be == "chunky bacon"
92
+ end
93
+
94
+ it "should only execute the first two operations" do
95
+ string_generator.any_instance.should_receive(:perform).and_call_original
96
+ halting_operation.any_instance.should_receive(:perform).and_call_original
97
+ string_capitalizer.any_instance.should_not_receive(:perform)
98
+ composed_operation.perform
99
+ end
100
+
101
+ it { should utilize_operations(string_generator, halting_operation, string_capitalizer) }
102
+ end
103
+
104
+ context "when composed of two operations and provided with a between block" do
105
+
106
+
107
+ let(:logger) { stub("Logger").as_null_object }
108
+
109
+ subject(:composed_operation) do
110
+ string_generator = string_generator()
111
+ string_capitalizer = string_capitalizer()
112
+ logger = logger()
113
+
114
+ operation = described_class.compose do
115
+ use string_generator
116
+ use string_capitalizer
117
+
118
+ between do |a, b, payload|
119
+ logger.info("#{a.name} -> #{b.name} with #{payload.inspect} as payload")
120
+ end
121
+ end
122
+
123
+ operation.new
124
+ end
125
+
126
+ it { should succeed_to_perform.and_return("CHUNKY BACON") }
127
+
128
+ it "should generate the correct log message" do
129
+ logger.should_receive(:info).with("StringGenerator -> StringCapitalizer with \"chunky bacon\" as payload")
130
+ composed_operation.perform
131
+ end
132
+
133
+ end
134
+
135
+ end
@@ -0,0 +1,187 @@
1
+ require 'spec_helper'
2
+
3
+ describe "An operation with two before and two after filters =>" do
4
+
5
+ let(:test_operation) do
6
+ Class.new(ComposableOperations::Operation) do
7
+
8
+ processes :flow_control
9
+
10
+ attr_accessor :trace
11
+
12
+ def initialize(input = [], options = {})
13
+ super
14
+ self.trace = [:initialize]
15
+ end
16
+
17
+ before do
18
+ trace << :outer_before
19
+ fail "Fail in outer before" if flow_control.include?(:fail_in_outer_before)
20
+ halt "Halt in outer before" if flow_control.include?(:halt_in_outer_before)
21
+ end
22
+
23
+ before do
24
+ trace << :inner_before
25
+ fail "Fail in inner before" if flow_control.include?(:fail_in_inner_before)
26
+ halt "Halt in inner before" if flow_control.include?(:halt_in_inner_before)
27
+ end
28
+
29
+ after do
30
+ trace << :inner_after
31
+ fail "Fail in inner after" if flow_control.include?(:fail_in_inner_after)
32
+ halt "Halt in inner after" if flow_control.include?(:halt_in_inner_after)
33
+ end
34
+
35
+ after do
36
+ trace << :outer_after
37
+ fail "Fail in outer after" if flow_control.include?(:fail_in_outer_after)
38
+ halt "Halt in outer after" if flow_control.include?(:halt_in_outer_after)
39
+ end
40
+
41
+ def execute
42
+ trace << :execute_start
43
+ fail "Fail in execute" if flow_control.include?(:fail_in_execute)
44
+ halt "Halt in execute" if flow_control.include?(:halt_in_execute)
45
+ trace << :execute_stop
46
+ :final_result
47
+ end
48
+ end
49
+ end
50
+
51
+ context "when run and everything works as expected =>" do
52
+ subject { test_operation.new }
53
+ before(:each) { subject.perform }
54
+
55
+ it ("should run 'initialize' first") { subject.trace[0].should eq(:initialize) }
56
+ it ("should run 'outer before' after 'initialize'") { subject.trace[1].should eq(:outer_before) }
57
+ it ("should run 'inner before' after 'outer before'") { subject.trace[2].should eq(:inner_before) }
58
+ it ("should start 'execute' after 'inner before'") { subject.trace[3].should eq(:execute_start) }
59
+ it ("should stop 'execute' after it started 'execute'") { subject.trace[4].should eq(:execute_stop) }
60
+ it ("should run 'inner after' after 'execute'") { subject.trace[5].should eq(:inner_after) }
61
+ it ("should run 'outer after' after 'inner after'") { subject.trace[6].should eq(:outer_after) }
62
+ it ("should return :final_result as result") { subject.result.should eq(:final_result) }
63
+ it { should be_succeeded }
64
+ it { should_not be_failed }
65
+ it { should_not be_halted }
66
+ end
67
+
68
+
69
+ # Now: TEST ALL! the possible code flows systematically
70
+
71
+ test_vectors = [
72
+ {
73
+ :context => "no complications =>",
74
+ :input => [],
75
+ :output => :final_result,
76
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
77
+ :state => :succeeded
78
+ }, {
79
+ :context => "failing in outer_before filter =>",
80
+ :input => [:fail_in_outer_before],
81
+ :output => nil,
82
+ :trace => [:initialize, :outer_before, :inner_after, :outer_after],
83
+ :state => :failed
84
+ }, {
85
+ :context => "failing in inner_before filter =>",
86
+ :input => [:fail_in_inner_before],
87
+ :output => nil,
88
+ :trace => [:initialize, :outer_before, :inner_before, :inner_after, :outer_after],
89
+ :state => :failed
90
+ }, {
91
+ :context => "failing in execute =>",
92
+ :input => [:fail_in_execute],
93
+ :output => nil,
94
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :inner_after, :outer_after],
95
+ :state => :failed
96
+ }, {
97
+ :context => "failing in inner_after filter =>",
98
+ :input => [:fail_in_inner_after],
99
+ :output => nil,
100
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
101
+ :state => :failed
102
+ }, {
103
+ :context => "failing in outer_after filter =>",
104
+ :input => [:fail_in_outer_after],
105
+ :output => nil,
106
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
107
+ :state => :failed
108
+ }, {
109
+ :context => "halting in outer_before filter =>",
110
+ :input => [:halt_in_outer_before],
111
+ :output => [:halt_in_outer_before],
112
+ :trace => [:initialize, :outer_before, :inner_after, :outer_after],
113
+ :state => :halted
114
+ }, {
115
+ :context => "halting in inner_before filter =>",
116
+ :input => [:halt_in_inner_before],
117
+ :output => [:halt_in_inner_before],
118
+ :trace => [:initialize, :outer_before, :inner_before, :inner_after, :outer_after],
119
+ :state => :halted
120
+ }, {
121
+ :context => "halting in execute =>",
122
+ :input => [:halt_in_execute],
123
+ :output => [:halt_in_execute],
124
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :inner_after, :outer_after],
125
+ :state => :halted
126
+ }, {
127
+ :context => "halting in inner_after filter =>",
128
+ :input => [:halt_in_inner_after],
129
+ :output => [:halt_in_inner_after],
130
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
131
+ :state => :halted
132
+ }, {
133
+ :context => "halting in outer_after filter =>",
134
+ :input => [:halt_in_outer_after],
135
+ :output => [:halt_in_outer_after],
136
+ :trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
137
+ :state => :halted
138
+ }
139
+ ]
140
+
141
+ context "when initialized with input that leads to =>" do
142
+ subject { test_operation.new(input) }
143
+ before(:each) { subject.perform }
144
+
145
+ test_vectors.each do |tv|
146
+ context tv[:context] do
147
+ let(:input) { tv[:input] }
148
+ let(:trace) { subject.trace }
149
+
150
+ it("then its trace should be #{tv[:trace].inspect}") { subject.trace.should eq(tv[:trace]) }
151
+ it("then its result should be #{tv[:output].inspect}") { subject.result.should eq(tv[:output]) }
152
+ it("then its succeeded? method should return #{(tv[:state] == :succeeded).inspect}") { subject.succeeded?.should eq(tv[:state] == :succeeded) }
153
+ it("then its failed? method should return #{(tv[:state] == :failed).inspect}") { subject.failed?.should eq(tv[:state] == :failed) }
154
+ it("then its halted? method should return #{(tv[:state] == :halted).inspect}") { subject.halted?.should eq(tv[:state] == :halted) }
155
+ end
156
+ end
157
+ end
158
+
159
+ context "when halt and fail are used together" do
160
+ subject { test_operation.new([:halt_in_execute, :fail_in_inner_after]) }
161
+ it "should raise on calling operation.perform" do
162
+ expect { subject.perform }.to raise_error
163
+ end
164
+ end
165
+
166
+ context "when fail and halt are used together" do
167
+ subject { test_operation.new([:fail_in_execute, :halt_in_inner_after]) }
168
+ it "should raise on calling operation.perform" do
169
+ expect { subject.perform }.to raise_error
170
+ end
171
+ end
172
+
173
+ context "when halt is used twice" do
174
+ subject { test_operation.new([:halt_in_execute, :halt_in_inner_after]) }
175
+ it "should raise on calling operation.perform" do
176
+ expect { subject.perform }.to raise_error
177
+ end
178
+ end
179
+
180
+ context "when fail is used twice" do
181
+ subject { test_operation.new([:fail_in_execute, :fail_in_inner_after]) }
182
+ it "should raise on calling operation.perform" do
183
+ expect { subject.perform }.to raise_error
184
+ end
185
+ end
186
+
187
+ end
@@ -0,0 +1,229 @@
1
+ require 'spec_helper'
2
+
3
+ describe ComposableOperations::Operation do
4
+
5
+ context "that always returns nil when executed" do
6
+
7
+ subject(:nil_operation) do
8
+ class << (operation = described_class.new(''))
9
+ def execute
10
+ nil
11
+ end
12
+ end
13
+ operation
14
+ end
15
+
16
+ it { should succeed_to_perform.and_return(nil) }
17
+
18
+ end
19
+
20
+ context "that always halts" do
21
+
22
+ let(:halting_operation) do
23
+ Class.new(described_class) do
24
+ def execute
25
+ halt "Full stop!"
26
+ end
27
+ end
28
+ end
29
+
30
+ let(:halting_operation_instance) do
31
+ halting_operation.new("Test")
32
+ end
33
+
34
+ it "should return the input value when executed using the class' method perform" do
35
+ halting_operation.perform("Test").should be == "Test"
36
+ end
37
+
38
+ it "should return the input value when executed using the instance's peform method" do
39
+ halting_operation_instance.perform.should be == "Test"
40
+ end
41
+
42
+ it "should have halted after performing" do
43
+ halting_operation_instance.perform
44
+ halting_operation_instance.should be_halted
45
+ end
46
+
47
+ end
48
+
49
+ context "that always fails" do
50
+
51
+ let(:failing_operation) do
52
+ Class.new(described_class) do
53
+ def execute
54
+ fail "Operation failed"
55
+ end
56
+ end
57
+ end
58
+
59
+ subject(:failing_operation_instance) do
60
+ failing_operation.new
61
+ end
62
+
63
+ before(:each) do
64
+ failing_operation_instance.perform
65
+ end
66
+
67
+ it "should have nil as result" do
68
+ failing_operation_instance.result.should be_nil
69
+ end
70
+
71
+ it "should have failed" do
72
+ failing_operation_instance.should be_failed
73
+ end
74
+
75
+ it "should have a message" do
76
+ failing_operation_instance.message.should_not be_nil
77
+ end
78
+
79
+ it "should raise an error when executed using the class method perform" do
80
+ expect { failing_operation.perform }.to raise_error("Operation failed")
81
+ end
82
+
83
+ context "when extended with a finalizer" do
84
+
85
+ let(:supervisor) { mock("Supervisor") }
86
+
87
+ let(:failing_operation_instance_with_finalizer) do
88
+ supervisor = supervisor()
89
+ Class.new(failing_operation) do
90
+ after { supervisor.notify }
91
+ end
92
+ end
93
+
94
+ subject(:failing_operation_instance_with_finalizer_instance) do
95
+ failing_operation_instance_with_finalizer.new
96
+ end
97
+
98
+ it "should execute the finalizers" do
99
+ supervisor.should_receive(:notify)
100
+ failing_operation_instance_with_finalizer_instance.perform
101
+ end
102
+
103
+ end
104
+
105
+ context "when configured to raise a custom exception" do
106
+
107
+ let(:custom_exception) { Class.new(RuntimeError) }
108
+
109
+ subject(:failing_operation_with_custom_exception) do
110
+ custom_exception = custom_exception()
111
+ Class.new(failing_operation) do
112
+ raises custom_exception
113
+ end
114
+ end
115
+
116
+ it "should raise the custom exeception when executed using the class method perform" do
117
+ expect { failing_operation_with_custom_exception.perform }.to raise_error(custom_exception, "Operation failed")
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ context "that always returns something when executed" do
125
+
126
+ let(:simple_operation) do
127
+ Class.new(described_class) do
128
+ def execute
129
+ ""
130
+ end
131
+ end
132
+ end
133
+
134
+ subject(:simple_operation_instance) do
135
+ simple_operation.new
136
+ end
137
+
138
+ before(:each) do
139
+ simple_operation_instance.perform
140
+ end
141
+
142
+ it "should have a result" do
143
+ simple_operation_instance.result.should be
144
+ end
145
+
146
+ it "should have succeeded" do
147
+ simple_operation_instance.should be_succeeded
148
+ end
149
+
150
+ context "when extended with a preparator and a finalizer" do
151
+
152
+ let(:logger) { double("Logger") }
153
+
154
+ subject(:simple_operation_with_preparator_and_finalizer) do
155
+ logger = logger()
156
+ Class.new(simple_operation) do
157
+ before { logger.info("preparing") }
158
+ after { logger.info("finalizing") }
159
+ end
160
+ end
161
+
162
+ it "should execute the preparator and finalizer when performing" do
163
+ logger.should_receive(:info).ordered.with("preparing")
164
+ logger.should_receive(:info).ordered.with("finalizing")
165
+ simple_operation_with_preparator_and_finalizer.perform
166
+ end
167
+
168
+ end
169
+
170
+ context "when extended with a finalizer that checks that the result is not an empty string" do
171
+
172
+ let(:simple_operation_with_sanity_check) do
173
+ Class.new(simple_operation) do
174
+ after { fail "the operational result is an empty string" if self.result == "" }
175
+ end
176
+ end
177
+
178
+ subject(:simple_operation_with_sanity_check_instance) do
179
+ simple_operation_with_sanity_check.new
180
+ end
181
+
182
+ it { should fail_to_perform.because("the operational result is an empty string") }
183
+
184
+ end
185
+
186
+ end
187
+
188
+ context "that can be parameterized" do
189
+
190
+ subject(:string_multiplier) do
191
+ Class.new(described_class) do
192
+ property :multiplier, :default => 3
193
+
194
+ def execute
195
+ input.to_s * multiplier
196
+ end
197
+ end
198
+ end
199
+
200
+ it "should operate according to the specified default value" do
201
+ string_multiplier.perform("-").should be == "---"
202
+ end
203
+
204
+ it "should allow to overwrite default settings" do
205
+ string_multiplier.perform("-", :multiplier => 5).should be == "-----"
206
+ end
207
+
208
+ end
209
+
210
+ context "that processes two values (a string and a multiplier)" do
211
+
212
+ subject(:string_multiplier) do
213
+ Class.new(described_class) do
214
+ processes :string, :multiplier
215
+
216
+ def execute
217
+ string * multiplier
218
+ end
219
+ end
220
+ end
221
+
222
+ it "should build a string that is multiplier-times long" do
223
+ string_multiplier.perform(["-", 3]).should be == "---"
224
+ end
225
+
226
+ end
227
+
228
+ end
229
+
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+
4
+ require 'composable_operations'
5
+ require 'composable_operations/matcher'
6
+
7
+ Dir[File.join(File.dirname(__FILE__), 'support', '**', '*.rb')].each { |f| require f }
8
+
9
+ RSpec.configure do |config|
10
+ end
11
+
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: composable_operations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Tennhard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: smart_properties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
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: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '2.11'
69
+ description: Composable Operations is a tool set for creating easy-to-use operation
70
+ pipelines.
71
+ email:
72
+ - me@t6d.de
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - composable_operations.gemspec
83
+ - lib/composable_operations.rb
84
+ - lib/composable_operations/composed_operation.rb
85
+ - lib/composable_operations/matcher.rb
86
+ - lib/composable_operations/matcher/fail_to_perform.rb
87
+ - lib/composable_operations/matcher/succeed_to_perform.rb
88
+ - lib/composable_operations/matcher/utilize_operation.rb
89
+ - lib/composable_operations/operation.rb
90
+ - lib/composable_operations/operation_error.rb
91
+ - lib/composable_operations/version.rb
92
+ - spec/composable_operations/composed_operation_spec.rb
93
+ - spec/composable_operations/control_flow_spec.rb
94
+ - spec/composable_operations/operation_spec.rb
95
+ - spec/spec_helper.rb
96
+ homepage: http://github.com/t6d/composable_operations
97
+ licenses:
98
+ - MIT
99
+ metadata: {}
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubyforge_project:
116
+ rubygems_version: 2.0.3
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: Tool set for operation pipelines.
120
+ test_files:
121
+ - spec/composable_operations/composed_operation_spec.rb
122
+ - spec/composable_operations/control_flow_spec.rb
123
+ - spec/composable_operations/operation_spec.rb
124
+ - spec/spec_helper.rb