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.
@@ -3,44 +3,238 @@
3
3
  require 'support/dry_base'
4
4
  require 'support/fake_models'
5
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
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
+ )
12
37
  end
38
+ end
39
+ end
40
+ end
13
41
 
14
- CreateUserOutput = Types.Instance(User)
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
15
58
 
16
- class CreateUserError < Dry::Struct
17
- attribute :message, Types::String
18
- attribute :status_code, Types::Integer
19
- attribute :meta, Types::Hash.optional
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
+ )
20
69
  end
70
+ end
71
+ end
72
+ end
21
73
 
22
- class CreateUser
23
- include Teckel::Operation
74
+ module TeckelOperationAnnonClassesTest
75
+ class CreateUser
76
+ include ::Teckel::Operation
24
77
 
25
- input CreateUserInput
26
- output CreateUserOutput
27
- error CreateUserError
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))
28
81
 
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
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!
40
206
  end
207
+ else
208
+ settings&.success_data
41
209
  end
42
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
43
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
44
238
  specify "Input" do
45
239
  expect(TeckelOperationPredefinedClassesTest::CreateUser.input).to eq(TeckelOperationPredefinedClassesTest::CreateUserInput)
46
240
  end
@@ -74,38 +268,6 @@ RSpec.describe Teckel::Operation do
74
268
  end
75
269
 
76
270
  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
271
  specify "Input" do
110
272
  expect(TeckelOperationInlineClassesTest::CreateUser.input).to be <= Dry::Struct
111
273
  end
@@ -138,25 +300,6 @@ RSpec.describe Teckel::Operation do
138
300
  end
139
301
 
140
302
  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
303
  specify "output" do
161
304
  expect(TeckelOperationAnnonClassesTest::CreateUser.call(name: "Bob", age: 23)).to be_a(User)
162
305
  end
@@ -167,91 +310,18 @@ RSpec.describe Teckel::Operation do
167
310
  end
168
311
 
169
312
  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
313
  specify do
203
- expect(CreateUserKeywordInit.call(name: "Bob", age: 23)).to be_a(User)
314
+ expect(TeckelOperationKeywordContracts::MyOperation.call(name: "Bob", age: 23)).to be_a(User)
204
315
  end
205
316
  end
206
317
 
207
318
  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
319
  specify do
235
- expect(CreateUserSplatInit.call(["Bob", 23])).to be_a(User)
320
+ expect(TeckelOperationCreateUserSplatInit::MyOperation.call(["Bob", 23])).to be_a(User)
236
321
  end
237
322
  end
238
323
 
239
324
  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
325
  specify "result" do
256
326
  result = TeckelOperationGeneratedOutputTest::MyOperation.call
257
327
  expect(result).to be_a(Struct)
@@ -260,31 +330,13 @@ RSpec.describe Teckel::Operation do
260
330
  end
261
331
 
262
332
  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
333
  it "settings in operation instances are nil by default" do
281
334
  op = TeckelOperationInjectSettingsTest::MyOperation.new
282
335
  expect(op.settings).to be_nil
283
336
  end
284
337
 
285
338
  it "uses injected data" do
286
- result =
287
- TeckelOperationInjectSettingsTest::MyOperation.
339
+ result = TeckelOperationInjectSettingsTest::MyOperation.
288
340
  with(injected: [:stuff]).
289
341
  call
290
342
 
@@ -294,28 +346,15 @@ RSpec.describe Teckel::Operation do
294
346
  end
295
347
 
296
348
  specify "calling `with` multiple times raises an error" do
297
- op = TeckelOperationInjectSettingsTest::MyOperation.with(injected: :stuff_1)
349
+ op = TeckelOperationInjectSettingsTest::MyOperation.with(injected: :stuff1)
298
350
 
299
351
  expect {
300
- op.with(more: :stuff_2)
301
- }.to raise_error(Teckel::Error)
352
+ op.with(more: :stuff2)
353
+ }.to raise_error(Teckel::Error, "Operation already has settings assigned.")
302
354
  end
303
355
  end
304
356
 
305
357
  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
358
  it "uses None as default settings class" do
320
359
  expect(TeckelOperationNoSettingsTest::MyOperation.settings).to eq(Teckel::Contracts::None)
321
360
  expect(TeckelOperationNoSettingsTest::MyOperation.new.settings).to be_nil
@@ -329,37 +368,6 @@ RSpec.describe Teckel::Operation do
329
368
  end
330
369
 
331
370
  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
371
  let(:operation) { TeckelOperationNoneDataTest::MyOperation }
364
372
 
365
373
  it "raises error when called with input data" do
@@ -386,23 +394,12 @@ RSpec.describe Teckel::Operation do
386
394
  expect(operation.with(success_it: true).call).to be_nil
387
395
  end
388
396
 
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
397
  it "returns nil as success result when returning nil" do
396
398
  expect(operation.call).to be_nil
397
399
  end
398
400
  end
399
401
 
400
402
  describe "#finalize!" do
401
- let(:frozen_error) do
402
- # different ruby versions raise different errors
403
- defined?(FrozenError) ? FrozenError : RuntimeError
404
- end
405
-
406
403
  subject do
407
404
  Class.new do
408
405
  include ::Teckel::Operation
@@ -482,4 +479,53 @@ RSpec.describe Teckel::Operation do
482
479
  }.to raise_error Teckel::FrozenConfigError, "Configuration input is already set"
483
480
  end
484
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
485
531
  end
@@ -37,7 +37,7 @@ RSpec.describe "Ruby 2.7 pattern matches for Result and Chain" do
37
37
 
38
38
  def call(usr)
39
39
  Logger.new(File::NULL).info("User #{usr.name} created")
40
- usr
40
+ success! usr
41
41
  end
42
42
  end
43
43
 
@@ -56,7 +56,7 @@ RSpec.describe "Ruby 2.7 pattern matches for Result and Chain" do
56
56
  if settings&.fail_befriend
57
57
  fail!(message: "Did not find a friend.")
58
58
  else
59
- { user: user, friend: User.new(name: "A friend", age: 42) }
59
+ success!(user: user, friend: User.new(name: "A friend", age: 42))
60
60
  end
61
61
  end
62
62
  end
data/spec/result_spec.rb CHANGED
@@ -3,18 +3,20 @@
3
3
  require 'support/dry_base'
4
4
  require 'support/fake_models'
5
5
 
6
+ module TeckelResultTest
7
+ class MissingResultImplementation
8
+ include Teckel::Result
9
+ def initialize(value, success); end
10
+ end
11
+ end
12
+
6
13
  RSpec.describe Teckel::Result do
7
14
  describe "missing initialize" do
8
- class MissingResultImplementation
9
- include Teckel::Result
10
- def initialize(value, success); end
11
- end
12
-
13
- specify do
14
- result = MissingResultImplementation["value", true]
15
- expect { result.successful? }.to raise_error(NotImplementedError)
16
- expect { result.failure? }.to raise_error(NotImplementedError)
17
- expect { result.value }.to raise_error(NotImplementedError)
15
+ specify "raises NotImplementedError" do
16
+ result = TeckelResultTest::MissingResultImplementation["value", true]
17
+ expect { result.successful? }.to raise_error(NotImplementedError, "Result object does not implement `successful?`")
18
+ expect { result.failure? }.to raise_error(NotImplementedError, "Result object does not implement `successful?`")
19
+ expect { result.value }.to raise_error(NotImplementedError, "Result object does not implement `value`")
18
20
  end
19
21
  end
20
22
  end
data/spec/spec_helper.rb CHANGED
@@ -3,12 +3,22 @@
3
3
  require "bundler/setup"
4
4
  if ENV['COVERAGE'] == 'true'
5
5
  require 'simplecov'
6
+
7
+ SimpleCov.formatter = case ENV['SIMPLECOV']&.downcase
8
+ when 'html'
9
+ SimpleCov::Formatter::HTMLFormatter
10
+ else
11
+ require 'simplecov_json_formatter'
12
+ SimpleCov::Formatter::JSONFormatter
13
+ end
14
+
6
15
  SimpleCov.start do
7
16
  add_filter %r{^/spec/}
8
17
  end
9
18
  end
10
19
 
11
20
  require "teckel"
21
+ require "teckel/chain"
12
22
 
13
23
  RSpec.configure do |config|
14
24
  # Enable flags like --only-failures and --next-failure