teckel 0.8.0 → 0.9.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.
@@ -1,531 +0,0 @@
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) { self.class.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
- let(:frozen_error) do
233
- # different ruby versions raise different errors
234
- defined?(FrozenError) ? FrozenError : RuntimeError
235
- end
236
-
237
- context "predefined classes" do
238
- specify "Input" do
239
- expect(TeckelOperationPredefinedClassesTest::CreateUser.input).to eq(TeckelOperationPredefinedClassesTest::CreateUserInput)
240
- end
241
-
242
- specify "Output" do
243
- expect(TeckelOperationPredefinedClassesTest::CreateUser.output).to eq(TeckelOperationPredefinedClassesTest::CreateUserOutput)
244
- end
245
-
246
- specify "Error" do
247
- expect(TeckelOperationPredefinedClassesTest::CreateUser.error).to eq(TeckelOperationPredefinedClassesTest::CreateUserError)
248
- end
249
-
250
- context "success" do
251
- specify do
252
- result = TeckelOperationPredefinedClassesTest::CreateUser.call(name: "Bob", age: 23)
253
- expect(result).to be_a(User)
254
- end
255
- end
256
-
257
- context "error" do
258
- specify do
259
- result = TeckelOperationPredefinedClassesTest::CreateUser.call(name: "Bob", age: 7)
260
- expect(result).to be_a(TeckelOperationPredefinedClassesTest::CreateUserError)
261
- expect(result).to have_attributes(
262
- message: "Could not create User",
263
- status_code: 400,
264
- meta: { validation: [{ age: "underage" }] }
265
- )
266
- end
267
- end
268
- end
269
-
270
- context "inline classes" do
271
- specify "Input" do
272
- expect(TeckelOperationInlineClassesTest::CreateUser.input).to be <= Dry::Struct
273
- end
274
-
275
- specify "Output" do
276
- expect(TeckelOperationInlineClassesTest::CreateUser.output).to be_a Dry::Types::Constrained
277
- end
278
-
279
- specify "Error" do
280
- expect(TeckelOperationInlineClassesTest::CreateUser.error).to be <= Dry::Struct
281
- end
282
-
283
- context "success" do
284
- specify do
285
- result = TeckelOperationInlineClassesTest::CreateUser.call(name: "Bob", age: 23)
286
- expect(result).to be_a(User)
287
- end
288
- end
289
-
290
- context "error" do
291
- specify do
292
- result = TeckelOperationInlineClassesTest::CreateUser.call(name: "Bob", age: 7)
293
- expect(result).to have_attributes(
294
- message: "Could not create User",
295
- status_code: 400,
296
- meta: { validation: [{ age: "underage" }] }
297
- )
298
- end
299
- end
300
- end
301
-
302
- context "annon classes" do
303
- specify "output" do
304
- expect(TeckelOperationAnnonClassesTest::CreateUser.call(name: "Bob", age: 23)).to be_a(User)
305
- end
306
-
307
- specify "errors" do
308
- expect(TeckelOperationAnnonClassesTest::CreateUser.call(name: "Bob", age: 10)).to eq(message: "Could not save User", errors: [{ age: "underage" }])
309
- end
310
- end
311
-
312
- context "keyword contracts" do
313
- specify do
314
- expect(TeckelOperationKeywordContracts::MyOperation.call(name: "Bob", age: 23)).to be_a(User)
315
- end
316
- end
317
-
318
- context "splat contracts" do
319
- specify do
320
- expect(TeckelOperationCreateUserSplatInit::MyOperation.call(["Bob", 23])).to be_a(User)
321
- end
322
- end
323
-
324
- context "generated output" do
325
- specify "result" do
326
- result = TeckelOperationGeneratedOutputTest::MyOperation.call
327
- expect(result).to be_a(Struct)
328
- expect(result.some_key).to eq("some_value")
329
- end
330
- end
331
-
332
- context "inject settings" do
333
- it "settings in operation instances are nil by default" do
334
- op = TeckelOperationInjectSettingsTest::MyOperation.new
335
- expect(op.settings).to be_nil
336
- end
337
-
338
- it "uses injected data" do
339
- result = TeckelOperationInjectSettingsTest::MyOperation.
340
- with(injected: [:stuff]).
341
- call
342
-
343
- expect(result).to eq([:stuff, :operation_data])
344
-
345
- expect(TeckelOperationInjectSettingsTest::MyOperation.call).to eq([:operation_data])
346
- end
347
-
348
- specify "calling `with` multiple times raises an error" do
349
- op = TeckelOperationInjectSettingsTest::MyOperation.with(injected: :stuff1)
350
-
351
- expect {
352
- op.with(more: :stuff2)
353
- }.to raise_error(Teckel::Error, "Operation already has settings assigned.")
354
- end
355
- end
356
-
357
- context "operation with no settings" do
358
- it "uses None as default settings class" do
359
- expect(TeckelOperationNoSettingsTest::MyOperation.settings).to eq(Teckel::Contracts::None)
360
- expect(TeckelOperationNoSettingsTest::MyOperation.new.settings).to be_nil
361
- end
362
-
363
- it "raises error when trying to set settings" do
364
- expect {
365
- TeckelOperationNoSettingsTest::MyOperation.with(any: :thing)
366
- }.to raise_error(ArgumentError, "None called with arguments")
367
- end
368
- end
369
-
370
- context "None in, out, err" do
371
- let(:operation) { TeckelOperationNoneDataTest::MyOperation }
372
-
373
- it "raises error when called with input data" do
374
- expect { operation.call("stuff") }.to raise_error(ArgumentError)
375
- end
376
-
377
- it "raises error when fail! with data" do
378
- expect {
379
- operation.with(fail_it: true, fail_data: "stuff").call
380
- }.to raise_error(ArgumentError)
381
- end
382
-
383
- it "returns nil as failure result when fail! without arguments" do
384
- expect(operation.with(fail_it: true).call).to be_nil
385
- end
386
-
387
- it "raises error when success! with data" do
388
- expect {
389
- operation.with(success_it: true, success_data: "stuff").call
390
- }.to raise_error(ArgumentError)
391
- end
392
-
393
- it "returns nil as success result when success! without arguments" do
394
- expect(operation.with(success_it: true).call).to be_nil
395
- end
396
-
397
- it "returns nil as success result when returning nil" do
398
- expect(operation.call).to be_nil
399
- end
400
- end
401
-
402
- describe "#finalize!" do
403
- subject do
404
- Class.new do
405
- include ::Teckel::Operation
406
-
407
- input Struct.new(:input_data)
408
- output Struct.new(:output_data)
409
-
410
- def call(input)
411
- success!(input.input_data * 2)
412
- end
413
- end
414
- end
415
-
416
- it "fails b/c error config is missing" do
417
- expect {
418
- subject.finalize!
419
- }.to raise_error(Teckel::MissingConfigError, "Missing error config for #{subject}")
420
- end
421
-
422
- specify "#dup" do
423
- new_operation = subject.dup
424
- new_operation.error Struct.new(:error)
425
- expect { new_operation.finalize! }.to_not raise_error
426
-
427
- expect {
428
- subject.finalize!
429
- }.to raise_error(Teckel::MissingConfigError, "Missing error config for #{subject}")
430
- end
431
-
432
- specify "#clone" do
433
- new_operation = subject.clone
434
- new_operation.error Struct.new(:error)
435
- expect { new_operation.finalize! }.to_not raise_error
436
-
437
- expect {
438
- subject.finalize!
439
- }.to raise_error(Teckel::MissingConfigError, "Missing error config for #{subject}")
440
- end
441
-
442
- it "rejects any config changes" do
443
- subject.error Struct.new(:error)
444
- expect { subject.finalize! }.to_not raise_error
445
-
446
- # no more after finalize!
447
- subject.finalize!
448
-
449
- expect {
450
- subject.error Struct.new(:other_error)
451
- }.to raise_error(Teckel::FrozenConfigError, "Configuration error is already set")
452
- end
453
-
454
- it "runs" do
455
- subject.error Struct.new(:error)
456
- subject.finalize!
457
-
458
- result = subject.call("test")
459
- expect(result.output_data).to eq("testtest")
460
- end
461
-
462
- it "accepts mocks" do
463
- subject.error Struct.new(:error)
464
- subject.finalize!
465
-
466
- allow(subject).to receive(:call) { :mocked }
467
- expect(subject.call).to eq(:mocked)
468
- end
469
- end
470
-
471
- describe "overwriting configs is not allowed" do
472
- it "raises" do
473
- expect {
474
- Class.new do
475
- include ::Teckel::Operation
476
- input none
477
- input Struct.new(:name)
478
- end
479
- }.to raise_error Teckel::FrozenConfigError, "Configuration input is already set"
480
- end
481
- end
482
-
483
- describe "frozen" do
484
- subject do
485
- Class.new do
486
- include ::Teckel::Operation
487
-
488
- input none
489
- output none
490
- error none
491
-
492
- def call(_input); end
493
- end
494
- end
495
-
496
- it "also freezes the config" do
497
- expect { subject.freeze }.to change {
498
- [
499
- subject.frozen?,
500
- subject.instance_variable_get(:@config).frozen?
501
- ]
502
- }.from([false, false]).to([true, true])
503
- end
504
-
505
- it "prevents changes to config" do
506
- subject.freeze
507
- expect { subject.settings Struct.new(:test) }.to raise_error(frozen_error)
508
- end
509
-
510
- describe '#clone' do
511
- it 'clones the class' do
512
- subject.freeze
513
- klone = subject.clone
514
-
515
- expect(klone).to be_frozen
516
- expect(klone.object_id).not_to be_eql(subject.object_id)
517
- end
518
-
519
- it 'cloned class uses the same, frozen config' do
520
- subject.freeze
521
- klone = subject.clone
522
-
523
- orig_config = subject.instance_variable_get(:@config)
524
- klone_config = klone.instance_variable_get(:@config)
525
-
526
- expect(klone_config).to be_frozen
527
- expect(klone_config.object_id).to be_eql(orig_config.object_id)
528
- end
529
- end
530
- end
531
- end
@@ -1,193 +0,0 @@
1
- # frozen_string_literal: true
2
- @warning = Warning[:experimental]
3
- Warning[:experimental] = false
4
-
5
- require 'support/dry_base'
6
- require 'support/fake_models'
7
-
8
- RSpec.describe "Ruby 2.7 pattern matches for Result and Chain" do
9
- module TeckelChainPatternMatchingTest
10
- class CreateUser
11
- include ::Teckel::Operation
12
-
13
- result!
14
-
15
- input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
16
- output Types.Instance(User)
17
- error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
18
-
19
- def call(input)
20
- user = User.new(name: input[:name], age: input[:age])
21
- if user.save
22
- success!(user)
23
- else
24
- fail!(message: "Could not save User", errors: user.errors)
25
- end
26
- end
27
- end
28
-
29
- class LogUser
30
- include ::Teckel::Operation
31
-
32
- result!
33
-
34
- input Types.Instance(User)
35
- error none
36
- output input
37
-
38
- def call(usr)
39
- Logger.new(File::NULL).info("User #{usr.name} created")
40
- success! usr
41
- end
42
- end
43
-
44
- class AddFriend
45
- include ::Teckel::Operation
46
-
47
- result!
48
-
49
- settings Struct.new(:fail_befriend)
50
-
51
- input Types.Instance(User)
52
- output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
53
- error Types::Hash.schema(message: Types::String)
54
-
55
- def call(user)
56
- if settings&.fail_befriend
57
- fail!(message: "Did not find a friend.")
58
- else
59
- success!(user: user, friend: User.new(name: "A friend", age: 42))
60
- end
61
- end
62
- end
63
-
64
- class Chain
65
- include Teckel::Chain
66
-
67
- step :create, CreateUser
68
- step :log, LogUser
69
- step :befriend, AddFriend
70
- end
71
- end
72
-
73
- describe "Operation Result" do
74
- context "success" do
75
- specify "pattern matching with keys" do
76
- x =
77
- case TeckelChainPatternMatchingTest::AddFriend.call(User.new(name: "bob", age: 23))
78
- in { success: false, value: value }
79
- ["Failed", value]
80
- in { success: true, value: value }
81
- ["Success result", value]
82
- else
83
- raise "Unexpected Result"
84
- end
85
-
86
- expect(x).to contain_exactly("Success result", hash_including(:friend, :user))
87
- end
88
- end
89
-
90
- context "failure" do
91
- specify "pattern matching with keys" do
92
- result =
93
- TeckelChainPatternMatchingTest::AddFriend.
94
- with(befriend: :fail).
95
- call(User.new(name: "bob", age: 23))
96
-
97
- x =
98
- case result
99
- in { success: false, value: value }
100
- ["Failed", value]
101
- in { success: true, value: value }
102
- ["Success result", value]
103
- else
104
- raise "Unexpected Result"
105
- end
106
-
107
- expect(x).to contain_exactly("Failed", hash_including(:message))
108
- end
109
-
110
- specify "pattern matching array" do
111
- result =
112
- TeckelChainPatternMatchingTest::AddFriend.
113
- with(befriend: :fail).
114
- call(User.new(name: "bob", age: 23))
115
-
116
- x =
117
- case result
118
- in [false, value]
119
- ["Failed", value]
120
- in [true, value]
121
- ["Success result", value]
122
- else
123
- raise "Unexpected Result"
124
- end
125
- expect(x).to contain_exactly("Failed", hash_including(:message))
126
- end
127
- end
128
- end
129
-
130
- describe "Chain" do
131
- context "success" do
132
- specify "pattern matching with keys" do
133
- x =
134
- case TeckelChainPatternMatchingTest::Chain.call(name: "Bob", age: 23)
135
- in { success: false, step: :befriend, value: value }
136
- ["Failed", value]
137
- in { success: true, value: value }
138
- ["Success result", value]
139
- else
140
- raise "Unexpected Result"
141
- end
142
- expect(x).to contain_exactly("Success result", hash_including(:friend, :user))
143
- end
144
-
145
- specify "pattern matching array" do
146
- x =
147
- case TeckelChainPatternMatchingTest::Chain.call(name: "Bob", age: 23)
148
- in [false, :befriend, value]
149
- "Failed in befriend with #{value}"
150
- in [true, _, value]
151
- "Success result"
152
- end
153
- expect(x).to eq("Success result")
154
- end
155
- end
156
-
157
- context "failure" do
158
- specify "pattern matching with keys" do
159
- result =
160
- TeckelChainPatternMatchingTest::Chain.
161
- with(befriend: :fail).
162
- call(name: "bob", age: 23)
163
-
164
- x =
165
- case result
166
- in { success: false, step: :befriend, value: value }
167
- "Failed in befriend with #{value}"
168
- in { success: true, value: value }
169
- "Success result"
170
- end
171
- expect(x).to eq("Failed in befriend with #{ {message: "Did not find a friend."} }")
172
- end
173
-
174
- specify "pattern matching array" do
175
- result =
176
- TeckelChainPatternMatchingTest::Chain.
177
- with(befriend: :fail).
178
- call(name: "Bob", age: 23)
179
-
180
- x =
181
- case result
182
- in [false, :befriend, value]
183
- "Failed in befriend with #{value}"
184
- in [true, value]
185
- "Success result"
186
- end
187
- expect(x).to eq("Failed in befriend with #{ {message: "Did not find a friend."} }")
188
- end
189
- end
190
- end
191
- end
192
-
193
- Warning[:experimental] = @warning