teckel 0.7.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +48 -7
- data/README.md +3 -3
- data/lib/teckel/chain/config.rb +35 -23
- data/lib/teckel/chain/result.rb +5 -9
- data/lib/teckel/chain/runner.rb +9 -7
- data/lib/teckel/chain.rb +19 -13
- data/lib/teckel/config.rb +15 -9
- data/lib/teckel/contracts.rb +3 -3
- data/lib/teckel/operation/config.rb +55 -48
- data/lib/teckel/operation/result.rb +16 -16
- data/lib/teckel/operation/runner.rb +5 -5
- data/lib/teckel/operation.rb +37 -18
- data/lib/teckel/result.rb +5 -3
- data/lib/teckel/version.rb +2 -1
- data/lib/teckel.rb +4 -1
- metadata +34 -53
- data/spec/chain/around_hook_spec.rb +0 -100
- data/spec/chain/default_settings_spec.rb +0 -39
- data/spec/chain/inheritance_spec.rb +0 -116
- data/spec/chain/none_input_spec.rb +0 -36
- data/spec/chain/results_spec.rb +0 -53
- data/spec/chain_spec.rb +0 -180
- data/spec/config_spec.rb +0 -26
- data/spec/doctest_helper.rb +0 -8
- data/spec/operation/contract_trace_spec.rb +0 -116
- data/spec/operation/default_settings_spec.rb +0 -94
- data/spec/operation/fail_on_input_spec.rb +0 -103
- data/spec/operation/inheritance_spec.rb +0 -94
- data/spec/operation/result_spec.rb +0 -55
- data/spec/operation/results_spec.rb +0 -117
- data/spec/operation_spec.rb +0 -483
- data/spec/rb27/pattern_matching_spec.rb +0 -193
- data/spec/result_spec.rb +0 -22
- data/spec/spec_helper.rb +0 -28
- data/spec/support/dry_base.rb +0 -8
- data/spec/support/fake_db.rb +0 -12
- data/spec/support/fake_models.rb +0 -20
- data/spec/teckel_spec.rb +0 -7
@@ -1,100 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'support/dry_base'
|
4
|
-
require 'support/fake_db'
|
5
|
-
require 'support/fake_models'
|
6
|
-
|
7
|
-
module TeckelChainAroundHookTest
|
8
|
-
class CreateUser
|
9
|
-
include ::Teckel::Operation
|
10
|
-
|
11
|
-
result!
|
12
|
-
|
13
|
-
input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
|
14
|
-
output Types.Instance(User)
|
15
|
-
error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
16
|
-
|
17
|
-
def call(input)
|
18
|
-
user = User.new(name: input[:name], age: input[:age])
|
19
|
-
if user.save
|
20
|
-
success!(user)
|
21
|
-
else
|
22
|
-
fail!(message: "Could not safe User", errors: user.errors)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
class AddFriend
|
28
|
-
include ::Teckel::Operation
|
29
|
-
|
30
|
-
result!
|
31
|
-
|
32
|
-
settings Struct.new(:fail_befriend)
|
33
|
-
|
34
|
-
input Types.Instance(User)
|
35
|
-
output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
|
36
|
-
error Types::Hash.schema(message: Types::String)
|
37
|
-
|
38
|
-
def call(user)
|
39
|
-
if settings&.fail_befriend
|
40
|
-
fail!(message: "Did not find a friend.")
|
41
|
-
else
|
42
|
-
success! user: user, friend: User.new(name: "A friend", age: 42)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
@stack = []
|
48
|
-
def self.stack
|
49
|
-
@stack
|
50
|
-
end
|
51
|
-
|
52
|
-
class Chain
|
53
|
-
include Teckel::Chain
|
54
|
-
|
55
|
-
around ->(chain, input) {
|
56
|
-
result = nil
|
57
|
-
begin
|
58
|
-
TeckelChainAroundHookTest.stack << :before
|
59
|
-
|
60
|
-
FakeDB.transaction do
|
61
|
-
result = chain.call(input)
|
62
|
-
raise FakeDB::Rollback if result.failure?
|
63
|
-
end
|
64
|
-
|
65
|
-
TeckelChainAroundHookTest.stack << :after
|
66
|
-
result
|
67
|
-
rescue FakeDB::Rollback
|
68
|
-
result
|
69
|
-
end
|
70
|
-
}
|
71
|
-
|
72
|
-
step :create, CreateUser
|
73
|
-
step :befriend, AddFriend
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
RSpec.describe Teckel::Chain do
|
78
|
-
before { TeckelChainAroundHookTest.stack.clear }
|
79
|
-
|
80
|
-
context "success" do
|
81
|
-
it "result matches" do
|
82
|
-
result = TeckelChainAroundHookTest::Chain.call(name: "Bob", age: 23)
|
83
|
-
expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
|
84
|
-
end
|
85
|
-
|
86
|
-
it "runs around hook" do
|
87
|
-
TeckelChainAroundHookTest::Chain.call(name: "Bob", age: 23)
|
88
|
-
expect(TeckelChainAroundHookTest.stack).to eq([:before, :after])
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
context "failure" do
|
93
|
-
it "runs around hook" do
|
94
|
-
TeckelChainAroundHookTest::Chain.
|
95
|
-
with(befriend: :fail).
|
96
|
-
call(name: "Bob", age: 23)
|
97
|
-
expect(TeckelChainAroundHookTest.stack).to eq([:before])
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module TeckelChainDefaultSettingsTest
|
4
|
-
class MyOperation
|
5
|
-
include Teckel::Operation
|
6
|
-
result!
|
7
|
-
|
8
|
-
settings Struct.new(:say, :other)
|
9
|
-
settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) } # ruby 2.4 way for `keyword_init: true`
|
10
|
-
|
11
|
-
input none
|
12
|
-
output Hash
|
13
|
-
error none
|
14
|
-
|
15
|
-
def call(_)
|
16
|
-
success! settings.to_h
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class Chain
|
21
|
-
include Teckel::Chain
|
22
|
-
|
23
|
-
default_settings!(a: { say: "Chain Default" })
|
24
|
-
|
25
|
-
step :a, MyOperation
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
RSpec.describe Teckel::Chain do
|
30
|
-
specify "call chain without settings, uses default settings" do
|
31
|
-
result = TeckelChainDefaultSettingsTest::Chain.call
|
32
|
-
expect(result.success).to eq(say: "Chain Default", other: nil)
|
33
|
-
end
|
34
|
-
|
35
|
-
specify "call chain with explicit settings, overwrites defaults" do
|
36
|
-
result = TeckelChainDefaultSettingsTest::Chain.with(a: { other: "What" }).call
|
37
|
-
expect(result.success).to eq(say: nil, other: "What")
|
38
|
-
end
|
39
|
-
end
|
@@ -1,116 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'support/dry_base'
|
4
|
-
require 'support/fake_models'
|
5
|
-
|
6
|
-
module TeckelChainDefaultsViaBaseClass
|
7
|
-
LOG = [] # rubocop:disable Style/MutableConstant
|
8
|
-
|
9
|
-
class LoggingChain
|
10
|
-
include Teckel::Chain
|
11
|
-
|
12
|
-
around do |chain, input|
|
13
|
-
require 'benchmark'
|
14
|
-
result = nil
|
15
|
-
LOG << Benchmark.measure { result = chain.call(input) }
|
16
|
-
result
|
17
|
-
end
|
18
|
-
|
19
|
-
freeze
|
20
|
-
end
|
21
|
-
|
22
|
-
class OperationA
|
23
|
-
include Teckel::Operation
|
24
|
-
|
25
|
-
result!
|
26
|
-
|
27
|
-
input none
|
28
|
-
output Types::Integer
|
29
|
-
error none
|
30
|
-
|
31
|
-
def call(_)
|
32
|
-
success! rand(1000)
|
33
|
-
end
|
34
|
-
|
35
|
-
finalize!
|
36
|
-
end
|
37
|
-
|
38
|
-
class OperationB
|
39
|
-
include Teckel::Operation
|
40
|
-
|
41
|
-
result!
|
42
|
-
|
43
|
-
input none
|
44
|
-
output Types::String
|
45
|
-
error none
|
46
|
-
|
47
|
-
def call(_)
|
48
|
-
success! ("a".."z").to_a.sample
|
49
|
-
end
|
50
|
-
|
51
|
-
finalize!
|
52
|
-
end
|
53
|
-
|
54
|
-
class ChainA < LoggingChain
|
55
|
-
step :roll, OperationA
|
56
|
-
|
57
|
-
finalize!
|
58
|
-
end
|
59
|
-
|
60
|
-
class ChainB < LoggingChain
|
61
|
-
step :say, OperationB
|
62
|
-
|
63
|
-
finalize!
|
64
|
-
end
|
65
|
-
|
66
|
-
class ChainC < ChainB
|
67
|
-
finalize!
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
RSpec.describe Teckel::Chain do
|
72
|
-
before do
|
73
|
-
TeckelChainDefaultsViaBaseClass::LOG.clear
|
74
|
-
end
|
75
|
-
|
76
|
-
let(:base_chain) { TeckelChainDefaultsViaBaseClass::LoggingChain }
|
77
|
-
let(:chain_a) { TeckelChainDefaultsViaBaseClass::ChainA }
|
78
|
-
let(:chain_b) { TeckelChainDefaultsViaBaseClass::ChainB }
|
79
|
-
let(:chain_c) { TeckelChainDefaultsViaBaseClass::ChainC }
|
80
|
-
|
81
|
-
it "inherits config" do
|
82
|
-
expect(chain_a.around)
|
83
|
-
expect(chain_b.around)
|
84
|
-
|
85
|
-
expect(base_chain.steps).to eq([])
|
86
|
-
expect(chain_a.steps.size).to eq(1)
|
87
|
-
expect(chain_b.steps.size).to eq(1)
|
88
|
-
end
|
89
|
-
|
90
|
-
it "runs chain a" do
|
91
|
-
expect {
|
92
|
-
result = chain_a.call
|
93
|
-
expect(result.success).to be_a(Integer)
|
94
|
-
}.to change {
|
95
|
-
TeckelChainDefaultsViaBaseClass::LOG.size
|
96
|
-
}.from(0).to(1)
|
97
|
-
end
|
98
|
-
|
99
|
-
it "runs chain b" do
|
100
|
-
expect {
|
101
|
-
result = chain_b.call
|
102
|
-
expect(result.success).to be_a(String)
|
103
|
-
}.to change {
|
104
|
-
TeckelChainDefaultsViaBaseClass::LOG.size
|
105
|
-
}.from(0).to(1)
|
106
|
-
end
|
107
|
-
|
108
|
-
it "inherits steps" do
|
109
|
-
expect {
|
110
|
-
result = chain_c.call
|
111
|
-
expect(result.success).to be_a(String)
|
112
|
-
}.to change {
|
113
|
-
TeckelChainDefaultsViaBaseClass::LOG.size
|
114
|
-
}.from(0).to(1)
|
115
|
-
end
|
116
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module TeckelChainNoneInputTest
|
4
|
-
class MyOperation
|
5
|
-
include Teckel::Operation
|
6
|
-
result!
|
7
|
-
|
8
|
-
settings Struct.new(:say)
|
9
|
-
|
10
|
-
input none
|
11
|
-
output String
|
12
|
-
error none
|
13
|
-
|
14
|
-
def call(_)
|
15
|
-
success!(settings&.say || "Called")
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
class Chain
|
20
|
-
include Teckel::Chain
|
21
|
-
|
22
|
-
step :a, MyOperation
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
RSpec.describe Teckel::Chain do
|
27
|
-
specify "call chain without input value" do
|
28
|
-
result = TeckelChainNoneInputTest::Chain.call
|
29
|
-
expect(result.success).to eq("Called")
|
30
|
-
end
|
31
|
-
|
32
|
-
specify "call chain runner without input value" do
|
33
|
-
result = TeckelChainNoneInputTest::Chain.with(a: "What").call
|
34
|
-
expect(result.success).to eq("What")
|
35
|
-
end
|
36
|
-
end
|
data/spec/chain/results_spec.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'support/dry_base'
|
4
|
-
require 'support/fake_models'
|
5
|
-
|
6
|
-
module TeckelChainResultTest
|
7
|
-
class Message
|
8
|
-
include ::Teckel::Operation
|
9
|
-
|
10
|
-
result!
|
11
|
-
|
12
|
-
input Types::Hash.schema(message: Types::String)
|
13
|
-
error none
|
14
|
-
output Types::String
|
15
|
-
|
16
|
-
def call(input)
|
17
|
-
success! input[:message].upcase
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class Chain
|
22
|
-
include Teckel::Chain
|
23
|
-
|
24
|
-
step :message, Message
|
25
|
-
|
26
|
-
class Result < Teckel::Operation::Result
|
27
|
-
def initialize(value, success, step, opts = {})
|
28
|
-
super(value, success)
|
29
|
-
@step = step
|
30
|
-
@opts = opts
|
31
|
-
end
|
32
|
-
|
33
|
-
class << self
|
34
|
-
alias :[] :new # Alias the default constructor to :new
|
35
|
-
end
|
36
|
-
|
37
|
-
attr_reader :opts, :step
|
38
|
-
end
|
39
|
-
|
40
|
-
result_constructor ->(value, success, step) {
|
41
|
-
result.new(value, success, step, time: Time.now.to_i)
|
42
|
-
}
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
RSpec.describe Teckel::Chain do
|
47
|
-
specify do
|
48
|
-
result = TeckelChainResultTest::Chain.call(message: "Hello World!")
|
49
|
-
expect(result).to be_successful
|
50
|
-
expect(result.success).to eq("HELLO WORLD!")
|
51
|
-
expect(result.opts).to include(time: kind_of(Integer))
|
52
|
-
end
|
53
|
-
end
|
data/spec/chain_spec.rb
DELETED
@@ -1,180 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'support/dry_base'
|
4
|
-
require 'support/fake_models'
|
5
|
-
|
6
|
-
module TeckelChainTest
|
7
|
-
class CreateUser
|
8
|
-
include ::Teckel::Operation
|
9
|
-
result!
|
10
|
-
|
11
|
-
input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
|
12
|
-
output Types.Instance(User)
|
13
|
-
error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
14
|
-
|
15
|
-
def call(input)
|
16
|
-
user = User.new(name: input[:name], age: input[:age])
|
17
|
-
if user.save
|
18
|
-
success!(user)
|
19
|
-
else
|
20
|
-
fail!(message: "Could not save User", errors: user.errors)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
class LogUser
|
26
|
-
include ::Teckel::Operation
|
27
|
-
|
28
|
-
result!
|
29
|
-
|
30
|
-
input Types.Instance(User)
|
31
|
-
error none
|
32
|
-
output input
|
33
|
-
|
34
|
-
def call(usr)
|
35
|
-
Logger.new(File::NULL).info("User #{usr.name} created")
|
36
|
-
success! usr
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class AddFriend
|
41
|
-
include ::Teckel::Operation
|
42
|
-
|
43
|
-
result!
|
44
|
-
|
45
|
-
settings Struct.new(:fail_befriend)
|
46
|
-
|
47
|
-
input Types.Instance(User)
|
48
|
-
output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
|
49
|
-
error Types::Hash.schema(message: Types::String)
|
50
|
-
|
51
|
-
def call(user)
|
52
|
-
if settings&.fail_befriend
|
53
|
-
fail!(message: "Did not find a friend.")
|
54
|
-
else
|
55
|
-
success! user: user, friend: User.new(name: "A friend", age: 42)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
class Chain
|
61
|
-
include Teckel::Chain
|
62
|
-
|
63
|
-
step :create, CreateUser
|
64
|
-
step :log, LogUser
|
65
|
-
step :befriend, AddFriend
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
RSpec.describe Teckel::Chain do
|
70
|
-
it 'Chain input points to first step input' do
|
71
|
-
expect(TeckelChainTest::Chain.input).to eq(TeckelChainTest::CreateUser.input)
|
72
|
-
end
|
73
|
-
|
74
|
-
it 'Chain output points to last steps output' do
|
75
|
-
expect(TeckelChainTest::Chain.output).to eq(TeckelChainTest::AddFriend.output)
|
76
|
-
end
|
77
|
-
|
78
|
-
it 'Chain errors maps all step errors' do
|
79
|
-
expect(TeckelChainTest::Chain.errors).to eq([
|
80
|
-
TeckelChainTest::CreateUser.error,
|
81
|
-
Teckel::Contracts::None,
|
82
|
-
TeckelChainTest::AddFriend.error
|
83
|
-
])
|
84
|
-
end
|
85
|
-
|
86
|
-
context "success" do
|
87
|
-
it "result matches" do
|
88
|
-
result =
|
89
|
-
TeckelChainTest::Chain.
|
90
|
-
with(befriend: nil).
|
91
|
-
call(name: "Bob", age: 23)
|
92
|
-
|
93
|
-
expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
context "failure" do
|
98
|
-
it "returns a Result for invalid input" do
|
99
|
-
result =
|
100
|
-
TeckelChainTest::Chain.
|
101
|
-
with(befriend: :fail).
|
102
|
-
call(name: "Bob", age: 0)
|
103
|
-
|
104
|
-
expect(result).to be_a(Teckel::Chain::Result)
|
105
|
-
expect(result).to be_failure
|
106
|
-
expect(result.step).to eq(:create)
|
107
|
-
expect(result.value).to eq(errors: [{ age: "underage" }], message: "Could not save User")
|
108
|
-
end
|
109
|
-
|
110
|
-
it "returns a Result for failed step" do
|
111
|
-
result =
|
112
|
-
TeckelChainTest::Chain.
|
113
|
-
with(befriend: :fail).
|
114
|
-
call(name: "Bob", age: 23)
|
115
|
-
|
116
|
-
expect(result).to be_a(Teckel::Chain::Result)
|
117
|
-
expect(result).to be_failure
|
118
|
-
expect(result.step).to eq(:befriend)
|
119
|
-
expect(result.value).to eq(message: "Did not find a friend.")
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
describe "#finalize!" do
|
124
|
-
let(:frozen_error) do
|
125
|
-
# different ruby versions raise different errors
|
126
|
-
defined?(FrozenError) ? FrozenError : RuntimeError
|
127
|
-
end
|
128
|
-
|
129
|
-
subject { TeckelChainTest::Chain.dup }
|
130
|
-
|
131
|
-
it "freezes the Chain class and operation classes" do
|
132
|
-
subject.finalize!
|
133
|
-
|
134
|
-
steps = subject.steps
|
135
|
-
expect(steps).to be_frozen
|
136
|
-
expect(steps).to all be_frozen
|
137
|
-
end
|
138
|
-
|
139
|
-
it "disallows adding new steps" do
|
140
|
-
subject.class_eval do
|
141
|
-
step :other, TeckelChainTest::AddFriend
|
142
|
-
end
|
143
|
-
|
144
|
-
subject.finalize!
|
145
|
-
|
146
|
-
expect {
|
147
|
-
subject.class_eval do
|
148
|
-
step :yet_other, TeckelChainTest::AddFriend
|
149
|
-
end
|
150
|
-
}.to raise_error(frozen_error)
|
151
|
-
end
|
152
|
-
|
153
|
-
it "disallows changing around hook" do
|
154
|
-
subject.class_eval do
|
155
|
-
around ->{}
|
156
|
-
end
|
157
|
-
|
158
|
-
chain2 = TeckelChainTest::Chain.dup.finalize!
|
159
|
-
expect {
|
160
|
-
chain2.class_eval do
|
161
|
-
around ->{}
|
162
|
-
end
|
163
|
-
}.to raise_error(frozen_error)
|
164
|
-
end
|
165
|
-
|
166
|
-
it "runs" do
|
167
|
-
subject.finalize!
|
168
|
-
|
169
|
-
result = subject.call(name: "Bob", age: 23)
|
170
|
-
expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
|
171
|
-
end
|
172
|
-
|
173
|
-
it "accepts mocks" do
|
174
|
-
subject.finalize!
|
175
|
-
|
176
|
-
allow(subject).to receive(:call) { :mocked }
|
177
|
-
expect(subject.call).to eq(:mocked)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
data/spec/config_spec.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'support/dry_base'
|
4
|
-
require 'support/fake_models'
|
5
|
-
|
6
|
-
RSpec.describe Teckel::Config do
|
7
|
-
let(:sample_config) do
|
8
|
-
Teckel::Config.new
|
9
|
-
end
|
10
|
-
|
11
|
-
it "set and retrieve key" do
|
12
|
-
sample_config.for(:some_key, "some_value")
|
13
|
-
expect(sample_config.for(:some_key)).to eq("some_value")
|
14
|
-
end
|
15
|
-
|
16
|
-
it "allow default value via block" do
|
17
|
-
expect(sample_config.for(:some_key) { "default" }).to eq("default")
|
18
|
-
# and sets the block value
|
19
|
-
expect(sample_config.for(:some_key)).to eq("default")
|
20
|
-
end
|
21
|
-
|
22
|
-
it "raises FrozenConfigError when setting a key twice" do
|
23
|
-
sample_config.for(:some_key, "some_value")
|
24
|
-
expect { sample_config.for(:some_key, "other_value") }.to raise_error(Teckel::FrozenConfigError)
|
25
|
-
end
|
26
|
-
end
|
data/spec/doctest_helper.rb
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'support/dry_base'
|
4
|
-
|
5
|
-
module TeckelOperationContractTrace
|
6
|
-
DefaultError = Struct.new(:message, :status_code)
|
7
|
-
Settings = Struct.new(:fail_it)
|
8
|
-
|
9
|
-
class ApplicationOperation
|
10
|
-
include Teckel::Operation
|
11
|
-
|
12
|
-
class Input < Dry::Struct
|
13
|
-
attribute :input_data, Types::String
|
14
|
-
end
|
15
|
-
|
16
|
-
class Output < Dry::Struct
|
17
|
-
attribute :output_data, Types::String
|
18
|
-
end
|
19
|
-
|
20
|
-
class Error < Dry::Struct
|
21
|
-
attribute :error_data, Types::String
|
22
|
-
end
|
23
|
-
|
24
|
-
# Freeze the base class to make sure it's inheritable configuration is not altered
|
25
|
-
freeze
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# Hack to get reliable stack traces
|
30
|
-
eval <<~RUBY, binding, "operation_success_error.rb"
|
31
|
-
module TeckelOperationContractTrace
|
32
|
-
class OperationSuccessError < ApplicationOperation
|
33
|
-
# Includes a deliberate bug while crating a success output
|
34
|
-
def call(input)
|
35
|
-
success!(incorrect_key: 1)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
RUBY
|
40
|
-
|
41
|
-
eval <<~RUBY, binding, "operation_simple_success_error.rb"
|
42
|
-
module TeckelOperationContractTrace
|
43
|
-
class OperationSimpleSuccessNil < ApplicationOperation
|
44
|
-
# Includes a deliberate bug while crating a success output
|
45
|
-
def call(input)
|
46
|
-
return { incorrect_key: 1 }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
RUBY
|
51
|
-
|
52
|
-
eval <<~RUBY, binding, "operation_failure_error.rb"
|
53
|
-
module TeckelOperationContractTrace
|
54
|
-
class OperationFailureError < ApplicationOperation
|
55
|
-
# Includes a deliberate bug while crating an error output
|
56
|
-
def call(input)
|
57
|
-
fail!(incorrect_key: 1)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
RUBY
|
62
|
-
|
63
|
-
eval <<~RUBY, binding, "operation_ok.rb"
|
64
|
-
module TeckelOperationContractTrace
|
65
|
-
class OperationOk < ApplicationOperation
|
66
|
-
def call(input)
|
67
|
-
success!(output_data: "all fine")
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
RUBY
|
72
|
-
|
73
|
-
eval <<~RUBY, binding, "operation_input_error.rb"
|
74
|
-
module TeckelOperationContractTrace
|
75
|
-
def self.run_operation(operation)
|
76
|
-
operation.call(error_input_data: "failure")
|
77
|
-
end
|
78
|
-
end
|
79
|
-
RUBY
|
80
|
-
|
81
|
-
RSpec.describe Teckel::Operation do
|
82
|
-
context "contract errors include meaningful trace" do
|
83
|
-
specify "incorrect success" do
|
84
|
-
expect {
|
85
|
-
TeckelOperationContractTrace::OperationSuccessError.call(input_data: "ok")
|
86
|
-
}.to raise_error(Dry::Struct::Error) { |error|
|
87
|
-
expect(error.backtrace).to include /^#{Regexp.escape("operation_success_error.rb:5:in `call'")}$/
|
88
|
-
}
|
89
|
-
end
|
90
|
-
|
91
|
-
specify "incorrect success via simple return results in +nil+, but no meaningful trace" do
|
92
|
-
expect(
|
93
|
-
TeckelOperationContractTrace::OperationSimpleSuccessNil.call(input_data: "ok")
|
94
|
-
).to be_nil
|
95
|
-
end
|
96
|
-
|
97
|
-
specify "incorrect fail" do
|
98
|
-
expect {
|
99
|
-
TeckelOperationContractTrace::OperationFailureError.call(input_data: "ok")
|
100
|
-
}.to raise_error(Dry::Struct::Error) { |error|
|
101
|
-
expect(error.backtrace).to include /^#{Regexp.escape("operation_failure_error.rb:5:in `call'")}$/
|
102
|
-
}
|
103
|
-
end
|
104
|
-
|
105
|
-
specify "incorrect input" do
|
106
|
-
operation = TeckelOperationContractTrace::OperationOk
|
107
|
-
|
108
|
-
expect(operation.call(input_data: "ok")).to eq(operation.output[output_data: "all fine"])
|
109
|
-
expect {
|
110
|
-
TeckelOperationContractTrace.run_operation(operation)
|
111
|
-
}.to raise_error(Dry::Struct::Error) { |error|
|
112
|
-
expect(error.backtrace).to include /^#{Regexp.escape("operation_input_error.rb:3:in `run_operation'")}$/
|
113
|
-
}
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|