teckel 0.4.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/README.md +3 -3
- data/lib/teckel/chain/config.rb +286 -0
- data/lib/teckel/chain/result.rb +3 -3
- data/lib/teckel/chain/runner.rb +28 -17
- data/lib/teckel/chain.rb +14 -190
- data/lib/teckel/config.rb +13 -7
- data/lib/teckel/operation/config.rb +400 -0
- data/lib/teckel/operation/result.rb +8 -8
- data/lib/teckel/operation/runner.rb +29 -25
- data/lib/teckel/operation.rb +87 -388
- data/lib/teckel/result.rb +8 -6
- data/lib/teckel/version.rb +1 -1
- data/lib/teckel.rb +0 -1
- data/spec/chain/around_hook_spec.rb +100 -0
- data/spec/chain/default_settings_spec.rb +39 -0
- data/spec/chain/inheritance_spec.rb +44 -44
- data/spec/chain/none_input_spec.rb +36 -0
- data/spec/chain/results_spec.rb +28 -28
- data/spec/chain_spec.rb +132 -57
- data/spec/doctest_helper.rb +1 -0
- data/spec/operation/config_spec.rb +227 -0
- data/spec/operation/contract_trace_spec.rb +118 -0
- data/spec/operation/default_settings_spec.rb +120 -0
- data/spec/operation/fail_on_input_spec.rb +103 -0
- data/spec/operation/inheritance_spec.rb +38 -38
- data/spec/operation/result_spec.rb +27 -12
- data/spec/operation/results_spec.rb +67 -67
- data/spec/operation_spec.rb +276 -230
- data/spec/rb27/pattern_matching_spec.rb +2 -2
- data/spec/result_spec.rb +12 -10
- data/spec/spec_helper.rb +10 -0
- metadata +41 -12
- data/spec/chain_around_hook_spec.rb +0 -100
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
RSpec.describe Teckel::Operation do
|
6
|
+
let(:operation) do
|
7
|
+
Class.new do
|
8
|
+
include Teckel::Operation
|
9
|
+
input none
|
10
|
+
output ->(o) { o }
|
11
|
+
error none
|
12
|
+
|
13
|
+
def call(_)
|
14
|
+
success! settings
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:blank_operation) do
|
20
|
+
Class.new do
|
21
|
+
include Teckel::Operation
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ".settings" do
|
26
|
+
specify "no settings" do
|
27
|
+
expect(operation.settings).to eq(Teckel::Contracts::None)
|
28
|
+
expect(operation.settings_constructor).to eq(Teckel::Contracts::None.method(:new))
|
29
|
+
end
|
30
|
+
|
31
|
+
specify "with settings klass" do
|
32
|
+
settings_klass = Struct.new(:name)
|
33
|
+
operation.settings(settings_klass)
|
34
|
+
expect(operation.settings).to eq(settings_klass)
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "without settings class, with settings constructor as proc" do
|
38
|
+
settings_const = if RUBY_VERSION < '2.6.0'
|
39
|
+
->(sets) { sets.map { |k, v| [k.to_s, v.to_i] }.to_h }
|
40
|
+
else
|
41
|
+
->(sets) { sets.to_h { |k, v| [k.to_s, v.to_i] } }
|
42
|
+
end
|
43
|
+
|
44
|
+
operation.settings_constructor(settings_const)
|
45
|
+
|
46
|
+
expect(operation.settings).to eq(Teckel::Contracts::None)
|
47
|
+
expect(operation.settings_constructor).to eq(settings_const)
|
48
|
+
|
49
|
+
runner = operation.with(key: "1")
|
50
|
+
expect(runner).to be_a(Teckel::Operation::Runner)
|
51
|
+
expect(runner.settings).to eq({ "key" => 1 })
|
52
|
+
end
|
53
|
+
|
54
|
+
specify "with settings class, with settings constructor as symbol" do
|
55
|
+
settings_klass = Struct.new(:name) do
|
56
|
+
def self.make_one(opts)
|
57
|
+
new(opts[:name])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
operation.settings(settings_klass)
|
62
|
+
operation.settings_constructor(:make_one)
|
63
|
+
|
64
|
+
expect(operation.settings).to eq(settings_klass)
|
65
|
+
expect(operation.settings_constructor).to eq(settings_klass.method(:make_one))
|
66
|
+
|
67
|
+
runner = operation.with(name: "value")
|
68
|
+
expect(runner).to be_a(Teckel::Operation::Runner)
|
69
|
+
expect(runner.settings).to be_a(settings_klass)
|
70
|
+
expect(runner.settings.name).to eq("value")
|
71
|
+
end
|
72
|
+
|
73
|
+
specify "with settings class as constant" do
|
74
|
+
settings_klass = Struct.new(:name)
|
75
|
+
operation.const_set(:Settings, settings_klass)
|
76
|
+
|
77
|
+
expect(operation.settings).to eq(settings_klass)
|
78
|
+
expect(operation.settings_constructor).to eq(settings_klass.method(:[]))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe ".default_settings" do
|
83
|
+
specify "no default_settings" do
|
84
|
+
expect(operation.default_settings).to be_nil
|
85
|
+
expect(operation.runner).to receive(:new).with(operation).and_call_original
|
86
|
+
|
87
|
+
operation.call
|
88
|
+
end
|
89
|
+
|
90
|
+
specify "default_settings!() with no default_settings" do
|
91
|
+
operation.default_settings!
|
92
|
+
expect(operation.default_settings).to be_a(Proc)
|
93
|
+
|
94
|
+
expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
|
95
|
+
settings = original_method.call(*args, &block)
|
96
|
+
expect(settings).to be_nil
|
97
|
+
expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
|
98
|
+
settings
|
99
|
+
end
|
100
|
+
|
101
|
+
operation.call
|
102
|
+
end
|
103
|
+
|
104
|
+
specify "default_settings!() with default_settings" do
|
105
|
+
settings_klass = Struct.new(:name)
|
106
|
+
|
107
|
+
operation.settings(settings_klass)
|
108
|
+
operation.default_settings!
|
109
|
+
|
110
|
+
expect(operation.default_settings).to be_a(Proc)
|
111
|
+
|
112
|
+
expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
|
113
|
+
settings = original_method.call(*args, &block)
|
114
|
+
expect(settings).to be_a(settings_klass).and have_attributes(name: nil)
|
115
|
+
expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
|
116
|
+
settings
|
117
|
+
end
|
118
|
+
|
119
|
+
operation.call
|
120
|
+
end
|
121
|
+
|
122
|
+
specify "default_settings!(arg) with default_settings" do
|
123
|
+
settings_klass = Struct.new(:name)
|
124
|
+
|
125
|
+
operation.settings(settings_klass)
|
126
|
+
operation.default_settings!("Bob")
|
127
|
+
|
128
|
+
expect(operation.default_settings).to be_a(Proc)
|
129
|
+
|
130
|
+
expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
|
131
|
+
settings = original_method.call(*args, &block)
|
132
|
+
expect(settings).to be_a(settings_klass).and have_attributes(name: "Bob")
|
133
|
+
expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
|
134
|
+
settings
|
135
|
+
end
|
136
|
+
|
137
|
+
expect(operation.call).to be_a(Struct).and have_attributes(name: "Bob")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
%i[input output error].each do |meth|
|
142
|
+
describe ".#{meth}" do
|
143
|
+
specify "missing .#{meth} config raises MissingConfigError" do
|
144
|
+
expect {
|
145
|
+
blank_operation.public_send(meth)
|
146
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing #{meth} config for #{blank_operation}")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe ".#{meth}_constructor" do
|
151
|
+
specify "missing .#{meth}_constructor config raises MissingConfigError for missing #{meth}" do
|
152
|
+
expect {
|
153
|
+
blank_operation.public_send(:"#{meth}_constructor")
|
154
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing #{meth} config for #{blank_operation}")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
specify "default settings config" do
|
160
|
+
expect(blank_operation.settings).to eq(Teckel::Contracts::None)
|
161
|
+
end
|
162
|
+
|
163
|
+
specify "default settings_constructor" do
|
164
|
+
expect(blank_operation.settings_constructor).to eq(Teckel::Contracts::None.method(:[]))
|
165
|
+
end
|
166
|
+
|
167
|
+
specify "default settings_constructor with settings config set" do
|
168
|
+
settings_klass = Struct.new(:name)
|
169
|
+
blank_operation.settings(settings_klass)
|
170
|
+
|
171
|
+
expect(blank_operation.settings_constructor).to eq(settings_klass.method(:[]))
|
172
|
+
end
|
173
|
+
|
174
|
+
specify "unsupported constructor method" do
|
175
|
+
blank_operation.settings(Class.new)
|
176
|
+
expect {
|
177
|
+
blank_operation.settings_constructor(:nope)
|
178
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing settings_constructor config for #{blank_operation}")
|
179
|
+
|
180
|
+
expect {
|
181
|
+
blank_operation.settings_constructor
|
182
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing settings_constructor config for #{blank_operation}")
|
183
|
+
end
|
184
|
+
|
185
|
+
describe "result" do
|
186
|
+
specify "default result config" do
|
187
|
+
expect(blank_operation.result).to eq(Teckel::Operation::ValueResult)
|
188
|
+
end
|
189
|
+
|
190
|
+
specify "default result_constructor" do
|
191
|
+
expect(blank_operation.result_constructor).to eq(Teckel::Operation::ValueResult.method(:[]))
|
192
|
+
end
|
193
|
+
|
194
|
+
specify "default result_constructor with settings config set" do
|
195
|
+
result_klass = OpenStruct.new
|
196
|
+
blank_operation.result(result_klass)
|
197
|
+
|
198
|
+
expect(blank_operation.result_constructor).to eq(result_klass.method(:[]))
|
199
|
+
end
|
200
|
+
|
201
|
+
specify "unsupported constructor method" do
|
202
|
+
blank_operation.result(Class.new)
|
203
|
+
expect {
|
204
|
+
blank_operation.result_constructor(:nope)
|
205
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing result_constructor config for #{blank_operation}")
|
206
|
+
|
207
|
+
expect {
|
208
|
+
blank_operation.result_constructor
|
209
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing result_constructor config for #{blank_operation}")
|
210
|
+
end
|
211
|
+
|
212
|
+
specify "with result class as constant" do
|
213
|
+
result_klass = OpenStruct.new
|
214
|
+
blank_operation.const_set(:Result, result_klass)
|
215
|
+
|
216
|
+
expect(blank_operation.result).to eq(result_klass)
|
217
|
+
expect(blank_operation.result_constructor).to eq(result_klass.method(:[]))
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "result!" do
|
222
|
+
specify "default result config" do
|
223
|
+
blank_operation.result!
|
224
|
+
expect(blank_operation.result).to eq(Teckel::Operation::Result)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,118 @@
|
|
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
|
+
# rubocop:disable Style/EvalWithLocation
|
30
|
+
# Hack to get reliable stack traces
|
31
|
+
eval <<~RUBY, binding, "operation_success_error.rb"
|
32
|
+
module TeckelOperationContractTrace
|
33
|
+
class OperationSuccessError < ApplicationOperation
|
34
|
+
# Includes a deliberate bug while crating a success output
|
35
|
+
def call(input)
|
36
|
+
success!(incorrect_key: 1)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
RUBY
|
41
|
+
|
42
|
+
eval <<~RUBY, binding, "operation_simple_success_error.rb"
|
43
|
+
module TeckelOperationContractTrace
|
44
|
+
class OperationSimpleSuccessNil < ApplicationOperation
|
45
|
+
# Includes a deliberate bug while crating a success output
|
46
|
+
def call(input)
|
47
|
+
return { incorrect_key: 1 }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
RUBY
|
52
|
+
|
53
|
+
eval <<~RUBY, binding, "operation_failure_error.rb"
|
54
|
+
module TeckelOperationContractTrace
|
55
|
+
class OperationFailureError < ApplicationOperation
|
56
|
+
# Includes a deliberate bug while crating an error output
|
57
|
+
def call(input)
|
58
|
+
fail!(incorrect_key: 1)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
|
64
|
+
eval <<~RUBY, binding, "operation_ok.rb"
|
65
|
+
module TeckelOperationContractTrace
|
66
|
+
class OperationOk < ApplicationOperation
|
67
|
+
def call(input)
|
68
|
+
success!(output_data: "all fine")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
RUBY
|
73
|
+
|
74
|
+
eval <<~RUBY, binding, "operation_input_error.rb"
|
75
|
+
module TeckelOperationContractTrace
|
76
|
+
def self.run_operation(operation)
|
77
|
+
operation.call(error_input_data: "failure")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
RUBY
|
81
|
+
# rubocop:enable Style/EvalWithLocation
|
82
|
+
|
83
|
+
RSpec.describe Teckel::Operation do
|
84
|
+
context "contract errors include meaningful trace" do
|
85
|
+
specify "incorrect success" do
|
86
|
+
expect {
|
87
|
+
TeckelOperationContractTrace::OperationSuccessError.call(input_data: "ok")
|
88
|
+
}.to raise_error(Dry::Struct::Error) { |error|
|
89
|
+
expect(error.backtrace).to include /^#{Regexp.escape("operation_success_error.rb:5:in `call'")}$/
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
specify "incorrect success via simple return results in +nil+, but no meaningful trace" do
|
94
|
+
expect(
|
95
|
+
TeckelOperationContractTrace::OperationSimpleSuccessNil.call(input_data: "ok")
|
96
|
+
).to be_nil
|
97
|
+
end
|
98
|
+
|
99
|
+
specify "incorrect fail" do
|
100
|
+
expect {
|
101
|
+
TeckelOperationContractTrace::OperationFailureError.call(input_data: "ok")
|
102
|
+
}.to raise_error(Dry::Struct::Error) { |error|
|
103
|
+
expect(error.backtrace).to include /^#{Regexp.escape("operation_failure_error.rb:5:in `call'")}$/
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
specify "incorrect input" do
|
108
|
+
operation = TeckelOperationContractTrace::OperationOk
|
109
|
+
|
110
|
+
expect(operation.call(input_data: "ok")).to eq(operation.output[output_data: "all fine"])
|
111
|
+
expect {
|
112
|
+
TeckelOperationContractTrace.run_operation(operation)
|
113
|
+
}.to raise_error(Dry::Struct::Error) { |error|
|
114
|
+
expect(error.backtrace).to include /^#{Regexp.escape("operation_input_error.rb:3:in `run_operation'")}$/
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TeckelOperationDefaultSettings
|
4
|
+
class BaseOperation
|
5
|
+
include ::Teckel::Operation
|
6
|
+
|
7
|
+
input none
|
8
|
+
output Symbol
|
9
|
+
error none
|
10
|
+
|
11
|
+
def call(_input)
|
12
|
+
success! settings.injected
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec.describe Teckel::Operation do
|
18
|
+
context "default settings" do
|
19
|
+
shared_examples "operation with default settings" do |operation|
|
20
|
+
subject { operation }
|
21
|
+
|
22
|
+
it "with no settings" do
|
23
|
+
expect(subject.call).to eq(:default_value)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "with settings" do
|
27
|
+
expect(subject.with(:injected_value).call).to eq(:injected_value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "with default constructor and clever Settings class" do
|
32
|
+
it_behaves_like(
|
33
|
+
"operation with default settings",
|
34
|
+
Class.new(TeckelOperationDefaultSettings::BaseOperation) do
|
35
|
+
settings(Class.new do
|
36
|
+
def initialize(injected = nil)
|
37
|
+
@injected = injected
|
38
|
+
end
|
39
|
+
|
40
|
+
def injected
|
41
|
+
@injected || :default_value
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
alias :[] :new # make us respond to the default constructor
|
46
|
+
end
|
47
|
+
end)
|
48
|
+
|
49
|
+
default_settings!
|
50
|
+
end
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "with custom constructor and clever Settings class" do
|
55
|
+
it_behaves_like(
|
56
|
+
"operation with default settings",
|
57
|
+
Class.new(TeckelOperationDefaultSettings::BaseOperation) do
|
58
|
+
settings(Class.new do
|
59
|
+
def initialize(injected = nil)
|
60
|
+
@injected = injected
|
61
|
+
end
|
62
|
+
|
63
|
+
def injected
|
64
|
+
@injected || :default_value
|
65
|
+
end
|
66
|
+
end)
|
67
|
+
|
68
|
+
settings_constructor :new
|
69
|
+
default_settings!
|
70
|
+
end
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "with default constructor and simple Settings class" do
|
75
|
+
it_behaves_like(
|
76
|
+
"operation with default settings",
|
77
|
+
Class.new(TeckelOperationDefaultSettings::BaseOperation) do
|
78
|
+
settings Struct.new(:injected)
|
79
|
+
|
80
|
+
default_settings! -> { settings.new(:default_value) }
|
81
|
+
end
|
82
|
+
)
|
83
|
+
|
84
|
+
it_behaves_like(
|
85
|
+
"operation with default settings",
|
86
|
+
Class.new(TeckelOperationDefaultSettings::BaseOperation) do
|
87
|
+
settings Struct.new(:injected)
|
88
|
+
|
89
|
+
default_settings!(:default_value)
|
90
|
+
end
|
91
|
+
)
|
92
|
+
|
93
|
+
it_behaves_like(
|
94
|
+
"operation with default settings",
|
95
|
+
Class.new(TeckelOperationDefaultSettings::BaseOperation) do
|
96
|
+
settings Struct.new(:injected)
|
97
|
+
|
98
|
+
default_settings!("default_value")
|
99
|
+
|
100
|
+
output_constructor ->(out) { out&.to_sym }
|
101
|
+
end
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "with default constructor and simple Settings class responding to passed default setting" do
|
106
|
+
it_behaves_like(
|
107
|
+
"operation with default settings",
|
108
|
+
Class.new(TeckelOperationDefaultSettings::BaseOperation) do
|
109
|
+
settings(Struct.new(:injected) do
|
110
|
+
def self.default
|
111
|
+
new(:default_value)
|
112
|
+
end
|
113
|
+
end)
|
114
|
+
|
115
|
+
default_settings!(:default)
|
116
|
+
end
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/validation"
|
4
|
+
require 'support/dry_base'
|
5
|
+
require 'support/fake_models'
|
6
|
+
|
7
|
+
module TeckelOperationFailOnOInput
|
8
|
+
class NewUserContract < Dry::Validation::Contract
|
9
|
+
schema do
|
10
|
+
required(:name).filled(:string)
|
11
|
+
required(:age).value(:integer)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class CreateUser
|
16
|
+
include Teckel::Operation
|
17
|
+
|
18
|
+
result!
|
19
|
+
|
20
|
+
input(NewUserContract.new)
|
21
|
+
input_constructor(->(input){
|
22
|
+
result = self.class.input.call(input)
|
23
|
+
if result.success?
|
24
|
+
result.to_h
|
25
|
+
else
|
26
|
+
fail!(message: "Input data validation failed", errors: [result.errors.to_h])
|
27
|
+
end
|
28
|
+
})
|
29
|
+
|
30
|
+
output Types.Instance(User)
|
31
|
+
error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
32
|
+
|
33
|
+
def call(input)
|
34
|
+
user = User.new(name: input[:name], age: input[:age])
|
35
|
+
if user.save
|
36
|
+
success! user
|
37
|
+
else
|
38
|
+
fail!(message: "Could not save User", errors: user.errors)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
finalize!
|
43
|
+
end
|
44
|
+
|
45
|
+
class CreateUserIncorrectFailure
|
46
|
+
include Teckel::Operation
|
47
|
+
|
48
|
+
result!
|
49
|
+
|
50
|
+
input(->(input) { input }) # NoOp
|
51
|
+
input_constructor(->(_input) {
|
52
|
+
fail!("Input data validation failed")
|
53
|
+
})
|
54
|
+
|
55
|
+
output none
|
56
|
+
error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
57
|
+
|
58
|
+
def call(_); end
|
59
|
+
finalize!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
RSpec.describe Teckel::Operation do
|
64
|
+
specify "successs" do
|
65
|
+
result = TeckelOperationFailOnOInput::CreateUser.call(name: "Bob", age: 23)
|
66
|
+
expect(result).to be_successful
|
67
|
+
expect(result.success).to be_a(User)
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "failing in input_constructor" do
|
71
|
+
let(:failure_input) do
|
72
|
+
{ name: "", age: "incorrect type" }
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns the failure thrown in input_constructor" do
|
76
|
+
result = TeckelOperationFailOnOInput::CreateUser.call(failure_input)
|
77
|
+
expect(result).to be_a(Teckel::Operation::Result)
|
78
|
+
expect(result).to be_failure
|
79
|
+
expect(result.failure).to eq(
|
80
|
+
message: "Input data validation failed",
|
81
|
+
errors: [
|
82
|
+
{ name: ["must be filled"], age: ["must be an integer"] }
|
83
|
+
]
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not run .call" do
|
88
|
+
expect(TeckelOperationFailOnOInput::CreateUser).to receive(:new).and_wrap_original do |m, *args|
|
89
|
+
op_instance = m.call(*args)
|
90
|
+
expect(op_instance).to_not receive(:call)
|
91
|
+
op_instance
|
92
|
+
end
|
93
|
+
|
94
|
+
TeckelOperationFailOnOInput::CreateUser.call(failure_input)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
specify "thrown failure needs to conform to :error" do
|
99
|
+
expect {
|
100
|
+
TeckelOperationFailOnOInput::CreateUserIncorrectFailure.call(name: "Bob", age: 23)
|
101
|
+
}.to raise_error(Dry::Types::ConstraintError, /violates constraints/)
|
102
|
+
end
|
103
|
+
end
|
@@ -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
|
|