teckel 0.1.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 +114 -0
- data/LICENSE_LOGO +4 -0
- data/README.md +22 -14
- data/lib/teckel.rb +11 -2
- data/lib/teckel/chain.rb +47 -152
- data/lib/teckel/chain/config.rb +246 -0
- data/lib/teckel/chain/result.rb +38 -0
- data/lib/teckel/chain/runner.rb +62 -0
- data/lib/teckel/chain/step.rb +18 -0
- data/lib/teckel/config.rb +41 -52
- data/lib/teckel/contracts.rb +19 -0
- data/lib/teckel/operation.rb +108 -253
- data/lib/teckel/operation/config.rb +396 -0
- data/lib/teckel/operation/result.rb +92 -0
- data/lib/teckel/operation/runner.rb +75 -0
- data/lib/teckel/result.rb +52 -53
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/default_settings_spec.rb +39 -0
- data/spec/chain/inheritance_spec.rb +116 -0
- data/spec/chain/none_input_spec.rb +36 -0
- data/spec/chain/results_spec.rb +53 -0
- data/spec/chain_around_hook_spec.rb +100 -0
- data/spec/chain_spec.rb +180 -0
- data/spec/config_spec.rb +26 -0
- data/spec/doctest_helper.rb +7 -0
- data/spec/operation/contract_trace_spec.rb +116 -0
- data/spec/operation/default_settings_spec.rb +94 -0
- data/spec/operation/inheritance_spec.rb +94 -0
- data/spec/operation/result_spec.rb +34 -0
- data/spec/operation/results_spec.rb +117 -0
- data/spec/operation_spec.rb +483 -0
- data/spec/rb27/pattern_matching_spec.rb +193 -0
- data/spec/result_spec.rb +22 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/dry_base.rb +8 -0
- data/spec/support/fake_db.rb +12 -0
- data/spec/support/fake_models.rb +20 -0
- data/spec/teckel_spec.rb +7 -0
- metadata +64 -46
- data/.github/workflows/ci.yml +0 -67
- data/.github/workflows/pages.yml +0 -50
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -12
- data/.ruby-version +0 -1
- data/DEVELOPMENT.md +0 -28
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -71
- data/Rakefile +0 -30
- data/bin/console +0 -15
- data/bin/rake +0 -29
- data/bin/rspec +0 -29
- data/bin/rubocop +0 -18
- data/bin/setup +0 -8
- data/lib/teckel/operation/results.rb +0 -71
- data/teckel.gemspec +0 -33
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TeckelOperationDefaultsViaBaseClass
|
4
|
+
DefaultError = Struct.new(:message, :status_code)
|
5
|
+
Settings = Struct.new(:fail_it)
|
6
|
+
|
7
|
+
class ApplicationOperation
|
8
|
+
include Teckel::Operation
|
9
|
+
|
10
|
+
settings Settings
|
11
|
+
settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
|
12
|
+
|
13
|
+
error DefaultError
|
14
|
+
error_constructor ->(data) { error.new(*data.values_at(*error.members)) }
|
15
|
+
|
16
|
+
result!
|
17
|
+
|
18
|
+
# Freeze the base class to make sure it's inheritable configuration is not altered
|
19
|
+
freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
class OperationA < ApplicationOperation
|
23
|
+
input Struct.new(:input_data_a)
|
24
|
+
output Struct.new(:output_data_a)
|
25
|
+
|
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)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
finalize!
|
35
|
+
end
|
36
|
+
|
37
|
+
class OperationB < ApplicationOperation
|
38
|
+
input Struct.new(:input_data_b)
|
39
|
+
output Struct.new(:output_data_b)
|
40
|
+
|
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)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
finalize!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
RSpec.describe Teckel::Operation do
|
54
|
+
context "default settings via base class" do
|
55
|
+
let(:operation_a) { TeckelOperationDefaultsViaBaseClass::OperationA }
|
56
|
+
let(:operation_b) { TeckelOperationDefaultsViaBaseClass::OperationB }
|
57
|
+
|
58
|
+
it "inherits config" do
|
59
|
+
expect(operation_a.result).to eq(Teckel::Operation::Result)
|
60
|
+
expect(operation_a.settings).to eq(TeckelOperationDefaultsViaBaseClass::Settings)
|
61
|
+
|
62
|
+
expect(operation_b.result).to eq(Teckel::Operation::Result)
|
63
|
+
expect(operation_b.settings).to eq(TeckelOperationDefaultsViaBaseClass::Settings)
|
64
|
+
end
|
65
|
+
|
66
|
+
context "operation_a" do
|
67
|
+
it "can run" do
|
68
|
+
result = operation_a.call(10)
|
69
|
+
expect(result.success.to_h).to eq(output_data_a: 20)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "can fail" do
|
73
|
+
result = operation_a.with(fail_it: "D'oh!").call(10)
|
74
|
+
expect(result.failure.to_h).to eq(
|
75
|
+
message: "D'oh!", status_code: 400
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "operation_b" do
|
81
|
+
it "can run" do
|
82
|
+
result = operation_b.call(10)
|
83
|
+
expect(result.success.to_h).to eq(output_data_b: 40)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "can fail" do
|
87
|
+
result = operation_b.with(fail_it: "D'oh!").call(10)
|
88
|
+
expect(result.failure.to_h).to eq(
|
89
|
+
message: "D'oh!", status_code: 500
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Teckel::Operation::Result do
|
4
|
+
let(:failure_value) { "some error" }
|
5
|
+
let(:failed_result) { Teckel::Operation::Result.new(failure_value, false) }
|
6
|
+
|
7
|
+
let(:success_value) { "some error" }
|
8
|
+
let(:successful_result) { Teckel::Operation::Result.new(failure_value, true) }
|
9
|
+
|
10
|
+
it { expect(successful_result.successful?).to be(true) }
|
11
|
+
it { expect(failed_result.successful?).to be(false) }
|
12
|
+
|
13
|
+
it { expect(successful_result.failure?).to be(false) }
|
14
|
+
it { expect(failed_result.failure?).to be(true) }
|
15
|
+
|
16
|
+
it { expect(successful_result.value).to eq(success_value) }
|
17
|
+
it { expect(failed_result.value).to eq(failure_value) }
|
18
|
+
|
19
|
+
describe "#success" do
|
20
|
+
it { expect(successful_result.success).to eq(success_value) }
|
21
|
+
|
22
|
+
it { expect(failed_result.success).to eq(nil) }
|
23
|
+
it { expect(failed_result.success("other")).to eq("other") }
|
24
|
+
it { expect(failed_result.success { |value| "Failed: #{value}" } ).to eq("Failed: some error") }
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#failure" do
|
28
|
+
it { expect(failed_result.failure).to eq(failure_value) }
|
29
|
+
|
30
|
+
it { expect(successful_result.failure).to eq(nil) }
|
31
|
+
it { expect(successful_result.failure("other")).to eq("other") }
|
32
|
+
it { expect(successful_result.failure { |value| "Failed: #{value}" } ).to eq("Failed: some error") }
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'support/dry_base'
|
4
|
+
require 'support/fake_models'
|
5
|
+
|
6
|
+
class CreateUserWithResult
|
7
|
+
include Teckel::Operation
|
8
|
+
|
9
|
+
result!
|
10
|
+
|
11
|
+
input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
|
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 CreateUserCustomResult
|
26
|
+
include Teckel::Operation
|
27
|
+
|
28
|
+
class MyResult
|
29
|
+
include Teckel::Result # makes sure this can be used in a Chain
|
30
|
+
|
31
|
+
def initialize(value, success, opts = {})
|
32
|
+
@value, @success, @opts = value, success, opts
|
33
|
+
end
|
34
|
+
|
35
|
+
# implementing Teckel::Result
|
36
|
+
def successful?
|
37
|
+
@success
|
38
|
+
end
|
39
|
+
|
40
|
+
# implementing Teckel::Result
|
41
|
+
attr_reader :value
|
42
|
+
|
43
|
+
attr_reader :opts
|
44
|
+
end
|
45
|
+
|
46
|
+
result MyResult
|
47
|
+
result_constructor ->(value, success) { result.new(value, success, time: Time.now.to_i) }
|
48
|
+
|
49
|
+
input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
|
50
|
+
output Types.Instance(User)
|
51
|
+
error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
52
|
+
|
53
|
+
def call(input)
|
54
|
+
user = User.new(name: input[:name], age: input[:age])
|
55
|
+
if user.save
|
56
|
+
success! user
|
57
|
+
else
|
58
|
+
fail!(message: "Could not save User", errors: user.errors)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class CreateUserOverwritingResult
|
64
|
+
include Teckel::Operation
|
65
|
+
|
66
|
+
class Result
|
67
|
+
include Teckel::Result # makes sure this can be used in a Chain
|
68
|
+
|
69
|
+
def initialize(value, success); end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
RSpec.describe Teckel::Operation do
|
74
|
+
context "with build in result object" do
|
75
|
+
specify "output" do
|
76
|
+
result = CreateUserWithResult.call(name: "Bob", age: 23)
|
77
|
+
expect(result).to be_a(Teckel::Result)
|
78
|
+
expect(result).to be_successful
|
79
|
+
expect(result.success).to be_a(User)
|
80
|
+
end
|
81
|
+
|
82
|
+
specify "errors" do
|
83
|
+
result = CreateUserWithResult.call(name: "Bob", age: 10)
|
84
|
+
expect(result).to be_a(Teckel::Result)
|
85
|
+
expect(result).to be_failure
|
86
|
+
expect(result.failure).to eq(message: "Could not save User", errors: [{ age: "underage" }])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "using custom result" do
|
91
|
+
specify "output" do
|
92
|
+
result = CreateUserCustomResult.call(name: "Bob", age: 23)
|
93
|
+
expect(result).to be_a(CreateUserCustomResult::MyResult)
|
94
|
+
expect(result).to be_successful
|
95
|
+
expect(result.value).to be_a(User)
|
96
|
+
|
97
|
+
expect(result.opts).to include(time: kind_of(Integer))
|
98
|
+
end
|
99
|
+
|
100
|
+
specify "errors" do
|
101
|
+
result = CreateUserCustomResult.call(name: "Bob", age: 10)
|
102
|
+
expect(result).to be_a(CreateUserCustomResult::MyResult)
|
103
|
+
expect(result).to be_failure
|
104
|
+
expect(result.value).to eq(message: "Could not save User", errors: [{ age: "underage" }])
|
105
|
+
|
106
|
+
expect(result.opts).to include(time: kind_of(Integer))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "overwriting Result" do
|
111
|
+
it "uses the class definition" do
|
112
|
+
expect(CreateUserOverwritingResult.result).to_not eq(Teckel::Operation::Result)
|
113
|
+
expect(CreateUserOverwritingResult.result).to eq(CreateUserOverwritingResult::Result)
|
114
|
+
expect(CreateUserOverwritingResult.result_constructor).to eq(CreateUserOverwritingResult::Result.method(:[]))
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,483 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'support/dry_base'
|
4
|
+
require 'support/fake_models'
|
5
|
+
|
6
|
+
module TeckelOperationPredefinedClassesTest
|
7
|
+
class CreateUserInput < Dry::Struct
|
8
|
+
attribute :name, Types::String
|
9
|
+
attribute :age, Types::Coercible::Integer
|
10
|
+
end
|
11
|
+
|
12
|
+
CreateUserOutput = Types.Instance(User)
|
13
|
+
|
14
|
+
class CreateUserError < Dry::Struct
|
15
|
+
attribute :message, Types::String
|
16
|
+
attribute :status_code, Types::Integer
|
17
|
+
attribute :meta, Types::Hash.optional
|
18
|
+
end
|
19
|
+
|
20
|
+
class CreateUser
|
21
|
+
include Teckel::Operation
|
22
|
+
|
23
|
+
input CreateUserInput
|
24
|
+
output CreateUserOutput
|
25
|
+
error CreateUserError
|
26
|
+
|
27
|
+
def call(input)
|
28
|
+
user = User.new(**input.attributes)
|
29
|
+
if user.save
|
30
|
+
success!(user)
|
31
|
+
else
|
32
|
+
fail!(
|
33
|
+
message: "Could not create User",
|
34
|
+
status_code: 400,
|
35
|
+
meta: { validation: user.errors }
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module TeckelOperationInlineClassesTest
|
43
|
+
class CreateUser
|
44
|
+
include Teckel::Operation
|
45
|
+
|
46
|
+
class Input < Dry::Struct
|
47
|
+
attribute :name, Types::String
|
48
|
+
attribute :age, Types::Coercible::Integer
|
49
|
+
end
|
50
|
+
|
51
|
+
Output = Types.Instance(User)
|
52
|
+
|
53
|
+
class Error < Dry::Struct
|
54
|
+
attribute :message, Types::String
|
55
|
+
attribute :status_code, Types::Integer
|
56
|
+
attribute :meta, Types::Hash.optional
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(input)
|
60
|
+
user = User.new(**input.attributes)
|
61
|
+
if user.save
|
62
|
+
success!(user)
|
63
|
+
else
|
64
|
+
fail!(
|
65
|
+
message: "Could not create User",
|
66
|
+
status_code: 400,
|
67
|
+
meta: { validation: user.errors }
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module TeckelOperationAnnonClassesTest
|
75
|
+
class CreateUser
|
76
|
+
include ::Teckel::Operation
|
77
|
+
|
78
|
+
input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
|
79
|
+
output Types.Instance(User)
|
80
|
+
error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
|
81
|
+
|
82
|
+
def call(input)
|
83
|
+
user = User.new(name: input[:name], age: input[:age])
|
84
|
+
if user.save
|
85
|
+
success!(user)
|
86
|
+
else
|
87
|
+
fail!(message: "Could not save User", errors: user.errors)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module TeckelOperationKeywordContracts
|
94
|
+
class MyOperation
|
95
|
+
include Teckel::Operation
|
96
|
+
|
97
|
+
class Input
|
98
|
+
def initialize(name:, age:)
|
99
|
+
@name, @age = name, age
|
100
|
+
end
|
101
|
+
attr_reader :name, :age
|
102
|
+
end
|
103
|
+
|
104
|
+
input_constructor ->(data) { input.new(**data) }
|
105
|
+
|
106
|
+
Output = ::User
|
107
|
+
|
108
|
+
class Error
|
109
|
+
def initialize(message, errors)
|
110
|
+
@message, @errors = message, errors
|
111
|
+
end
|
112
|
+
attr_reader :message, :errors
|
113
|
+
end
|
114
|
+
error_constructor :new
|
115
|
+
|
116
|
+
def call(input)
|
117
|
+
user = ::User.new(name: input.name, age: input.age)
|
118
|
+
if user.save
|
119
|
+
success!(user)
|
120
|
+
else
|
121
|
+
fail!(message: "Could not save User", errors: user.errors)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module TeckelOperationCreateUserSplatInit
|
128
|
+
class MyOperation
|
129
|
+
include Teckel::Operation
|
130
|
+
|
131
|
+
input Struct.new(:name, :age)
|
132
|
+
input_constructor ->(data) { input.new(*data) }
|
133
|
+
|
134
|
+
Output = ::User
|
135
|
+
|
136
|
+
class Error
|
137
|
+
def initialize(message, errors)
|
138
|
+
@message, @errors = message, errors
|
139
|
+
end
|
140
|
+
attr_reader :message, :errors
|
141
|
+
end
|
142
|
+
error_constructor :new
|
143
|
+
|
144
|
+
def call(input)
|
145
|
+
user = ::User.new(name: input.name, age: input.age)
|
146
|
+
if user.save
|
147
|
+
success!(user)
|
148
|
+
else
|
149
|
+
fail!(message: "Could not save User", errors: user.errors)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module TeckelOperationGeneratedOutputTest
|
156
|
+
class MyOperation
|
157
|
+
include ::Teckel::Operation
|
158
|
+
|
159
|
+
input none
|
160
|
+
output Struct.new(:some_key)
|
161
|
+
output_constructor ->(data) { output.new(*data.values_at(*output.members)) } # ruby 2.4 way for `keyword_init: true`
|
162
|
+
error none
|
163
|
+
|
164
|
+
def call(_input)
|
165
|
+
success!(some_key: "some_value")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
module TeckelOperationNoSettingsTest
|
171
|
+
class MyOperation
|
172
|
+
include ::Teckel::Operation
|
173
|
+
|
174
|
+
input none
|
175
|
+
output none
|
176
|
+
error none
|
177
|
+
|
178
|
+
def call(_input); end
|
179
|
+
end
|
180
|
+
MyOperation.finalize!
|
181
|
+
end
|
182
|
+
|
183
|
+
module TeckelOperationNoneDataTest
|
184
|
+
class MyOperation
|
185
|
+
include ::Teckel::Operation
|
186
|
+
|
187
|
+
settings Struct.new(:fail_it, :fail_data, :success_it, :success_data)
|
188
|
+
settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
|
189
|
+
|
190
|
+
input none
|
191
|
+
output none
|
192
|
+
error none
|
193
|
+
|
194
|
+
def call(_input)
|
195
|
+
if settings&.fail_it
|
196
|
+
if settings&.fail_data
|
197
|
+
fail!(settings.fail_data)
|
198
|
+
else
|
199
|
+
fail!
|
200
|
+
end
|
201
|
+
elsif settings&.success_it
|
202
|
+
if settings&.success_data
|
203
|
+
success!(settings.success_data)
|
204
|
+
else
|
205
|
+
success!
|
206
|
+
end
|
207
|
+
else
|
208
|
+
settings&.success_data
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
module TeckelOperationInjectSettingsTest
|
215
|
+
class MyOperation
|
216
|
+
include ::Teckel::Operation
|
217
|
+
|
218
|
+
settings Struct.new(:injected)
|
219
|
+
settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
|
220
|
+
|
221
|
+
input none
|
222
|
+
output Array
|
223
|
+
error none
|
224
|
+
|
225
|
+
def call(_input)
|
226
|
+
success!((settings&.injected || []) << :operation_data)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
RSpec.describe Teckel::Operation do
|
232
|
+
context "predefined classes" do
|
233
|
+
specify "Input" do
|
234
|
+
expect(TeckelOperationPredefinedClassesTest::CreateUser.input).to eq(TeckelOperationPredefinedClassesTest::CreateUserInput)
|
235
|
+
end
|
236
|
+
|
237
|
+
specify "Output" do
|
238
|
+
expect(TeckelOperationPredefinedClassesTest::CreateUser.output).to eq(TeckelOperationPredefinedClassesTest::CreateUserOutput)
|
239
|
+
end
|
240
|
+
|
241
|
+
specify "Error" do
|
242
|
+
expect(TeckelOperationPredefinedClassesTest::CreateUser.error).to eq(TeckelOperationPredefinedClassesTest::CreateUserError)
|
243
|
+
end
|
244
|
+
|
245
|
+
context "success" do
|
246
|
+
specify do
|
247
|
+
result = TeckelOperationPredefinedClassesTest::CreateUser.call(name: "Bob", age: 23)
|
248
|
+
expect(result).to be_a(User)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context "error" do
|
253
|
+
specify do
|
254
|
+
result = TeckelOperationPredefinedClassesTest::CreateUser.call(name: "Bob", age: 7)
|
255
|
+
expect(result).to be_a(TeckelOperationPredefinedClassesTest::CreateUserError)
|
256
|
+
expect(result).to have_attributes(
|
257
|
+
message: "Could not create User",
|
258
|
+
status_code: 400,
|
259
|
+
meta: { validation: [{ age: "underage" }] }
|
260
|
+
)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context "inline classes" do
|
266
|
+
specify "Input" do
|
267
|
+
expect(TeckelOperationInlineClassesTest::CreateUser.input).to be <= Dry::Struct
|
268
|
+
end
|
269
|
+
|
270
|
+
specify "Output" do
|
271
|
+
expect(TeckelOperationInlineClassesTest::CreateUser.output).to be_a Dry::Types::Constrained
|
272
|
+
end
|
273
|
+
|
274
|
+
specify "Error" do
|
275
|
+
expect(TeckelOperationInlineClassesTest::CreateUser.error).to be <= Dry::Struct
|
276
|
+
end
|
277
|
+
|
278
|
+
context "success" do
|
279
|
+
specify do
|
280
|
+
result = TeckelOperationInlineClassesTest::CreateUser.call(name: "Bob", age: 23)
|
281
|
+
expect(result).to be_a(User)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
context "error" do
|
286
|
+
specify do
|
287
|
+
result = TeckelOperationInlineClassesTest::CreateUser.call(name: "Bob", age: 7)
|
288
|
+
expect(result).to have_attributes(
|
289
|
+
message: "Could not create User",
|
290
|
+
status_code: 400,
|
291
|
+
meta: { validation: [{ age: "underage" }] }
|
292
|
+
)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context "annon classes" do
|
298
|
+
specify "output" do
|
299
|
+
expect(TeckelOperationAnnonClassesTest::CreateUser.call(name: "Bob", age: 23)).to be_a(User)
|
300
|
+
end
|
301
|
+
|
302
|
+
specify "errors" do
|
303
|
+
expect(TeckelOperationAnnonClassesTest::CreateUser.call(name: "Bob", age: 10)).to eq(message: "Could not save User", errors: [{ age: "underage" }])
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context "keyword contracts" do
|
308
|
+
specify do
|
309
|
+
expect(TeckelOperationKeywordContracts::MyOperation.call(name: "Bob", age: 23)).to be_a(User)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
context "splat contracts" do
|
314
|
+
specify do
|
315
|
+
expect(TeckelOperationCreateUserSplatInit::MyOperation.call(["Bob", 23])).to be_a(User)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
context "generated output" do
|
320
|
+
specify "result" do
|
321
|
+
result = TeckelOperationGeneratedOutputTest::MyOperation.call
|
322
|
+
expect(result).to be_a(Struct)
|
323
|
+
expect(result.some_key).to eq("some_value")
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
context "inject settings" do
|
328
|
+
it "settings in operation instances are nil by default" do
|
329
|
+
op = TeckelOperationInjectSettingsTest::MyOperation.new
|
330
|
+
expect(op.settings).to be_nil
|
331
|
+
end
|
332
|
+
|
333
|
+
it "uses injected data" do
|
334
|
+
result =
|
335
|
+
TeckelOperationInjectSettingsTest::MyOperation.
|
336
|
+
with(injected: [:stuff]).
|
337
|
+
call
|
338
|
+
|
339
|
+
expect(result).to eq([:stuff, :operation_data])
|
340
|
+
|
341
|
+
expect(TeckelOperationInjectSettingsTest::MyOperation.call).to eq([:operation_data])
|
342
|
+
end
|
343
|
+
|
344
|
+
specify "calling `with` multiple times raises an error" do
|
345
|
+
op = TeckelOperationInjectSettingsTest::MyOperation.with(injected: :stuff_1)
|
346
|
+
|
347
|
+
expect {
|
348
|
+
op.with(more: :stuff_2)
|
349
|
+
}.to raise_error(Teckel::Error)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
context "operation with no settings" do
|
354
|
+
it "uses None as default settings class" do
|
355
|
+
expect(TeckelOperationNoSettingsTest::MyOperation.settings).to eq(Teckel::Contracts::None)
|
356
|
+
expect(TeckelOperationNoSettingsTest::MyOperation.new.settings).to be_nil
|
357
|
+
end
|
358
|
+
|
359
|
+
it "raises error when trying to set settings" do
|
360
|
+
expect {
|
361
|
+
TeckelOperationNoSettingsTest::MyOperation.with(any: :thing)
|
362
|
+
}.to raise_error(ArgumentError, "None called with arguments")
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
context "None in, out, err" do
|
367
|
+
let(:operation) { TeckelOperationNoneDataTest::MyOperation }
|
368
|
+
|
369
|
+
it "raises error when called with input data" do
|
370
|
+
expect { operation.call("stuff") }.to raise_error(ArgumentError)
|
371
|
+
end
|
372
|
+
|
373
|
+
it "raises error when fail! with data" do
|
374
|
+
expect {
|
375
|
+
operation.with(fail_it: true, fail_data: "stuff").call
|
376
|
+
}.to raise_error(ArgumentError)
|
377
|
+
end
|
378
|
+
|
379
|
+
it "returns nil as failure result when fail! without arguments" do
|
380
|
+
expect(operation.with(fail_it: true).call).to be_nil
|
381
|
+
end
|
382
|
+
|
383
|
+
it "raises error when success! with data" do
|
384
|
+
expect {
|
385
|
+
operation.with(success_it: true, success_data: "stuff").call
|
386
|
+
}.to raise_error(ArgumentError)
|
387
|
+
end
|
388
|
+
|
389
|
+
it "returns nil as success result when success! without arguments" do
|
390
|
+
expect(operation.with(success_it: true).call).to be_nil
|
391
|
+
end
|
392
|
+
|
393
|
+
it "returns nil as success result when returning nil" do
|
394
|
+
expect(operation.call).to be_nil
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
describe "#finalize!" do
|
399
|
+
let(:frozen_error) do
|
400
|
+
# different ruby versions raise different errors
|
401
|
+
defined?(FrozenError) ? FrozenError : RuntimeError
|
402
|
+
end
|
403
|
+
|
404
|
+
subject do
|
405
|
+
Class.new do
|
406
|
+
include ::Teckel::Operation
|
407
|
+
|
408
|
+
input Struct.new(:input_data)
|
409
|
+
output Struct.new(:output_data)
|
410
|
+
|
411
|
+
def call(input)
|
412
|
+
success!(input.input_data * 2)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
it "fails b/c error config is missing" do
|
418
|
+
expect {
|
419
|
+
subject.finalize!
|
420
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing error config for #{subject}")
|
421
|
+
end
|
422
|
+
|
423
|
+
specify "#dup" do
|
424
|
+
new_operation = subject.dup
|
425
|
+
new_operation.error Struct.new(:error)
|
426
|
+
expect { new_operation.finalize! }.to_not raise_error
|
427
|
+
|
428
|
+
expect {
|
429
|
+
subject.finalize!
|
430
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing error config for #{subject}")
|
431
|
+
end
|
432
|
+
|
433
|
+
specify "#clone" do
|
434
|
+
new_operation = subject.clone
|
435
|
+
new_operation.error Struct.new(:error)
|
436
|
+
expect { new_operation.finalize! }.to_not raise_error
|
437
|
+
|
438
|
+
expect {
|
439
|
+
subject.finalize!
|
440
|
+
}.to raise_error(Teckel::MissingConfigError, "Missing error config for #{subject}")
|
441
|
+
end
|
442
|
+
|
443
|
+
it "rejects any config changes" do
|
444
|
+
subject.error Struct.new(:error)
|
445
|
+
expect { subject.finalize! }.to_not raise_error
|
446
|
+
|
447
|
+
# no more after finalize!
|
448
|
+
subject.finalize!
|
449
|
+
|
450
|
+
expect {
|
451
|
+
subject.error Struct.new(:other_error)
|
452
|
+
}.to raise_error(Teckel::FrozenConfigError, "Configuration error is already set")
|
453
|
+
end
|
454
|
+
|
455
|
+
it "runs" do
|
456
|
+
subject.error Struct.new(:error)
|
457
|
+
subject.finalize!
|
458
|
+
|
459
|
+
result = subject.call("test")
|
460
|
+
expect(result.output_data).to eq("testtest")
|
461
|
+
end
|
462
|
+
|
463
|
+
it "accepts mocks" do
|
464
|
+
subject.error Struct.new(:error)
|
465
|
+
subject.finalize!
|
466
|
+
|
467
|
+
allow(subject).to receive(:call) { :mocked }
|
468
|
+
expect(subject.call).to eq(:mocked)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
describe "overwriting configs is not allowed" do
|
473
|
+
it "raises" do
|
474
|
+
expect {
|
475
|
+
Class.new do
|
476
|
+
include ::Teckel::Operation
|
477
|
+
input none
|
478
|
+
input Struct.new(:name)
|
479
|
+
end
|
480
|
+
}.to raise_error Teckel::FrozenConfigError, "Configuration input is already set"
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|