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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +114 -0
  3. data/LICENSE_LOGO +4 -0
  4. data/README.md +22 -14
  5. data/lib/teckel.rb +11 -2
  6. data/lib/teckel/chain.rb +47 -152
  7. data/lib/teckel/chain/config.rb +246 -0
  8. data/lib/teckel/chain/result.rb +38 -0
  9. data/lib/teckel/chain/runner.rb +62 -0
  10. data/lib/teckel/chain/step.rb +18 -0
  11. data/lib/teckel/config.rb +41 -52
  12. data/lib/teckel/contracts.rb +19 -0
  13. data/lib/teckel/operation.rb +108 -253
  14. data/lib/teckel/operation/config.rb +396 -0
  15. data/lib/teckel/operation/result.rb +92 -0
  16. data/lib/teckel/operation/runner.rb +75 -0
  17. data/lib/teckel/result.rb +52 -53
  18. data/lib/teckel/version.rb +1 -1
  19. data/spec/chain/default_settings_spec.rb +39 -0
  20. data/spec/chain/inheritance_spec.rb +116 -0
  21. data/spec/chain/none_input_spec.rb +36 -0
  22. data/spec/chain/results_spec.rb +53 -0
  23. data/spec/chain_around_hook_spec.rb +100 -0
  24. data/spec/chain_spec.rb +180 -0
  25. data/spec/config_spec.rb +26 -0
  26. data/spec/doctest_helper.rb +7 -0
  27. data/spec/operation/contract_trace_spec.rb +116 -0
  28. data/spec/operation/default_settings_spec.rb +94 -0
  29. data/spec/operation/inheritance_spec.rb +94 -0
  30. data/spec/operation/result_spec.rb +34 -0
  31. data/spec/operation/results_spec.rb +117 -0
  32. data/spec/operation_spec.rb +483 -0
  33. data/spec/rb27/pattern_matching_spec.rb +193 -0
  34. data/spec/result_spec.rb +22 -0
  35. data/spec/spec_helper.rb +25 -0
  36. data/spec/support/dry_base.rb +8 -0
  37. data/spec/support/fake_db.rb +12 -0
  38. data/spec/support/fake_models.rb +20 -0
  39. data/spec/teckel_spec.rb +7 -0
  40. metadata +64 -46
  41. data/.github/workflows/ci.yml +0 -67
  42. data/.github/workflows/pages.yml +0 -50
  43. data/.gitignore +0 -13
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -12
  46. data/.ruby-version +0 -1
  47. data/DEVELOPMENT.md +0 -28
  48. data/Gemfile +0 -8
  49. data/Gemfile.lock +0 -71
  50. data/Rakefile +0 -30
  51. data/bin/console +0 -15
  52. data/bin/rake +0 -29
  53. data/bin/rspec +0 -29
  54. data/bin/rubocop +0 -18
  55. data/bin/setup +0 -8
  56. data/lib/teckel/operation/results.rb +0 -71
  57. 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