teckel 0.3.0 → 0.4.0

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