teckel 0.5.0 → 0.6.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 +6 -0
- data/lib/teckel/chain/config.rb +3 -3
- data/lib/teckel/operation.rb +19 -9
- data/lib/teckel/operation/config.rb +2 -0
- data/lib/teckel/operation/runner.rb +26 -21
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/default_settings_spec.rb +18 -18
- data/spec/chain/inheritance_spec.rb +44 -44
- data/spec/chain/none_input_spec.rb +16 -16
- data/spec/chain/results_spec.rb +28 -28
- data/spec/chain_around_hook_spec.rb +54 -54
- data/spec/chain_spec.rb +46 -46
- data/spec/operation/contract_trace_spec.rb +116 -0
- data/spec/operation/default_settings_spec.rb +12 -12
- data/spec/operation/inheritance_spec.rb +38 -38
- data/spec/operation/results_spec.rb +67 -67
- data/spec/operation_spec.rb +218 -220
- data/spec/rb27/pattern_matching_spec.rb +2 -2
- data/spec/result_spec.rb +8 -6
- metadata +9 -7
@@ -4,77 +4,77 @@ require 'support/dry_base'
|
|
4
4
|
require 'support/fake_db'
|
5
5
|
require 'support/fake_models'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
fail!(message: "Could not safe User", errors: user.errors)
|
24
|
-
end
|
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)
|
25
23
|
end
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
27
|
+
class AddFriend
|
28
|
+
include ::Teckel::Operation
|
30
29
|
|
31
|
-
|
30
|
+
result!
|
32
31
|
|
33
|
-
|
32
|
+
settings Struct.new(:fail_befriend)
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
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)
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
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)
|
45
43
|
end
|
46
44
|
end
|
45
|
+
end
|
47
46
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
class Chain
|
54
|
-
include Teckel::Chain
|
47
|
+
@stack = []
|
48
|
+
def self.stack
|
49
|
+
@stack
|
50
|
+
end
|
55
51
|
|
56
|
-
|
57
|
-
|
58
|
-
begin
|
59
|
-
TeckelChainAroundHookTest.stack << :before
|
52
|
+
class Chain
|
53
|
+
include Teckel::Chain
|
60
54
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
55
|
+
around ->(chain, input) {
|
56
|
+
result = nil
|
57
|
+
begin
|
58
|
+
TeckelChainAroundHookTest.stack << :before
|
65
59
|
|
66
|
-
|
67
|
-
result
|
68
|
-
|
69
|
-
result
|
60
|
+
FakeDB.transaction do
|
61
|
+
result = chain.call(input)
|
62
|
+
raise FakeDB::Rollback if result.failure?
|
70
63
|
end
|
71
|
-
}
|
72
64
|
|
73
|
-
|
74
|
-
|
75
|
-
|
65
|
+
TeckelChainAroundHookTest.stack << :after
|
66
|
+
result
|
67
|
+
rescue FakeDB::Rollback
|
68
|
+
result
|
69
|
+
end
|
70
|
+
}
|
71
|
+
|
72
|
+
step :create, CreateUser
|
73
|
+
step :befriend, AddFriend
|
76
74
|
end
|
75
|
+
end
|
77
76
|
|
77
|
+
RSpec.describe Teckel::Chain do
|
78
78
|
before { TeckelChainAroundHookTest.stack.clear }
|
79
79
|
|
80
80
|
context "success" do
|
data/spec/chain_spec.rb
CHANGED
@@ -3,70 +3,70 @@
|
|
3
3
|
require 'support/dry_base'
|
4
4
|
require 'support/fake_models'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
fail!(message: "Could not save User", errors: user.errors)
|
22
|
-
end
|
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)
|
23
21
|
end
|
24
22
|
end
|
23
|
+
end
|
25
24
|
|
26
|
-
|
27
|
-
|
25
|
+
class LogUser
|
26
|
+
include ::Teckel::Operation
|
28
27
|
|
29
|
-
|
28
|
+
result!
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
input Types.Instance(User)
|
31
|
+
error none
|
32
|
+
output input
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
34
|
+
def call(usr)
|
35
|
+
Logger.new(File::NULL).info("User #{usr.name} created")
|
36
|
+
success! usr
|
39
37
|
end
|
38
|
+
end
|
40
39
|
|
41
|
-
|
42
|
-
|
40
|
+
class AddFriend
|
41
|
+
include ::Teckel::Operation
|
43
42
|
|
44
|
-
|
43
|
+
result!
|
45
44
|
|
46
|
-
|
45
|
+
settings Struct.new(:fail_befriend)
|
47
46
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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)
|
51
50
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
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)
|
58
56
|
end
|
59
57
|
end
|
58
|
+
end
|
60
59
|
|
61
|
-
|
62
|
-
|
60
|
+
class Chain
|
61
|
+
include Teckel::Chain
|
63
62
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
63
|
+
step :create, CreateUser
|
64
|
+
step :log, LogUser
|
65
|
+
step :befriend, AddFriend
|
68
66
|
end
|
67
|
+
end
|
69
68
|
|
69
|
+
RSpec.describe Teckel::Chain do
|
70
70
|
it 'Chain input points to first step input' do
|
71
71
|
expect(TeckelChainTest::Chain.input).to eq(TeckelChainTest::CreateUser.input)
|
72
72
|
end
|
@@ -0,0 +1,116 @@
|
|
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
|
@@ -1,21 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
class BaseOperation
|
7
|
-
include ::Teckel::Operation
|
3
|
+
module TeckelOperationDefaultSettings
|
4
|
+
class BaseOperation
|
5
|
+
include ::Teckel::Operation
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
input none
|
8
|
+
output Symbol
|
9
|
+
error none
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|
11
|
+
def call(_input)
|
12
|
+
success! settings.injected
|
17
13
|
end
|
14
|
+
end
|
15
|
+
end
|
18
16
|
|
17
|
+
RSpec.describe Teckel::Operation do
|
18
|
+
context "default settings" do
|
19
19
|
shared_examples "operation with default settings" do |operation|
|
20
20
|
subject { operation }
|
21
21
|
|
@@ -1,57 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
DefaultError = Struct.new(:message, :status_code)
|
7
|
-
Settings = Struct.new(:fail_it)
|
8
|
-
|
9
|
-
class ApplicationOperation
|
10
|
-
include Teckel::Operation
|
3
|
+
module TeckelOperationDefaultsViaBaseClass
|
4
|
+
DefaultError = Struct.new(:message, :status_code)
|
5
|
+
Settings = Struct.new(:fail_it)
|
11
6
|
|
12
|
-
|
13
|
-
|
7
|
+
class ApplicationOperation
|
8
|
+
include Teckel::Operation
|
14
9
|
|
15
|
-
|
16
|
-
|
10
|
+
settings Settings
|
11
|
+
settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
|
17
12
|
|
18
|
-
|
13
|
+
error DefaultError
|
14
|
+
error_constructor ->(data) { error.new(*data.values_at(*error.members)) }
|
19
15
|
|
20
|
-
|
21
|
-
freeze
|
22
|
-
end
|
16
|
+
result!
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
# Freeze the base class to make sure it's inheritable configuration is not altered
|
19
|
+
freeze
|
20
|
+
end
|
27
21
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
else
|
32
|
-
input.input_data_a * 2
|
33
|
-
end
|
34
|
-
end
|
22
|
+
class OperationA < ApplicationOperation
|
23
|
+
input Struct.new(:input_data_a)
|
24
|
+
output Struct.new(:output_data_a)
|
35
25
|
|
36
|
-
|
26
|
+
def call(input)
|
27
|
+
if settings&.fail_it
|
28
|
+
fail!(message: settings.fail_it, status_code: 400)
|
29
|
+
else
|
30
|
+
success!(input.input_data_a * 2)
|
37
31
|
end
|
32
|
+
end
|
38
33
|
|
39
|
-
|
40
|
-
|
41
|
-
output Struct.new(:output_data_b)
|
34
|
+
finalize!
|
35
|
+
end
|
42
36
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
else
|
47
|
-
input.input_data_b * 4
|
48
|
-
end
|
49
|
-
end
|
37
|
+
class OperationB < ApplicationOperation
|
38
|
+
input Struct.new(:input_data_b)
|
39
|
+
output Struct.new(:output_data_b)
|
50
40
|
|
51
|
-
|
41
|
+
def call(input)
|
42
|
+
if settings&.fail_it
|
43
|
+
fail!(message: settings.fail_it, status_code: 500)
|
44
|
+
else
|
45
|
+
success!(input.input_data_b * 4)
|
52
46
|
end
|
53
47
|
end
|
54
48
|
|
49
|
+
finalize!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
RSpec.describe Teckel::Operation do
|
54
|
+
context "default settings via base class" do
|
55
55
|
let(:operation_a) { TeckelOperationDefaultsViaBaseClass::OperationA }
|
56
56
|
let(:operation_b) { TeckelOperationDefaultsViaBaseClass::OperationB }
|
57
57
|
|