teckel 0.4.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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