teckel 0.4.0 → 0.8.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 +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
|
|