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.
data/spec/chain_spec.rb DELETED
@@ -1,255 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'support/dry_base'
4
- require 'support/fake_models'
5
-
6
- module TeckelChainTest
7
- class CreateUser
8
- include ::Teckel::Operation
9
- result!
10
-
11
- input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
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 LogUser
26
- include ::Teckel::Operation
27
-
28
- result!
29
-
30
- input Types.Instance(User)
31
- error none
32
- output input
33
-
34
- def call(usr)
35
- Logger.new(File::NULL).info("User #{usr.name} created")
36
- success! usr
37
- end
38
- end
39
-
40
- class AddFriend
41
- include ::Teckel::Operation
42
-
43
- result!
44
-
45
- settings Struct.new(:fail_befriend)
46
-
47
- input Types.Instance(User)
48
- output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
49
- error Types::Hash.schema(message: Types::String)
50
-
51
- def call(user)
52
- if settings&.fail_befriend
53
- fail!(message: "Did not find a friend.")
54
- else
55
- success! user: user, friend: User.new(name: "A friend", age: 42)
56
- end
57
- end
58
- end
59
-
60
- class Chain
61
- include Teckel::Chain
62
-
63
- step :create, CreateUser
64
- step :log, LogUser
65
- step :befriend, AddFriend
66
- end
67
- end
68
-
69
- RSpec.describe Teckel::Chain do
70
- let(:frozen_error) do
71
- # different ruby versions raise different errors
72
- defined?(FrozenError) ? FrozenError : RuntimeError
73
- end
74
-
75
- it 'Chain input points to first step input' do
76
- expect(TeckelChainTest::Chain.input).to eq(TeckelChainTest::CreateUser.input)
77
- end
78
-
79
- it 'Chain output points to last steps output' do
80
- expect(TeckelChainTest::Chain.output).to eq(TeckelChainTest::AddFriend.output)
81
- end
82
-
83
- it 'Chain errors maps all step errors' do
84
- expect(TeckelChainTest::Chain.errors).to eq([
85
- TeckelChainTest::CreateUser.error,
86
- Teckel::Contracts::None,
87
- TeckelChainTest::AddFriend.error
88
- ])
89
- end
90
-
91
- context "success" do
92
- it "result matches" do
93
- result = TeckelChainTest::Chain.
94
- with(befriend: nil).
95
- call(name: "Bob", age: 23)
96
-
97
- expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
98
- end
99
- end
100
-
101
- context "failure" do
102
- it "returns a Result for invalid input" do
103
- result = TeckelChainTest::Chain.
104
- with(befriend: :fail).
105
- call(name: "Bob", age: 0)
106
-
107
- expect(result).to be_a(Teckel::Chain::Result)
108
- expect(result).to be_failure
109
- expect(result.step).to eq(:create)
110
- expect(result.value).to eq(errors: [{ age: "underage" }], message: "Could not save User")
111
- end
112
-
113
- it "returns a Result for failed step" do
114
- result = TeckelChainTest::Chain.
115
- with(befriend: :fail).
116
- call(name: "Bob", age: 23)
117
-
118
- expect(result).to be_a(Teckel::Chain::Result)
119
- expect(result).to be_failure
120
- expect(result.step).to eq(:befriend)
121
- expect(result.value).to eq(message: "Did not find a friend.")
122
- end
123
- end
124
-
125
- describe "#finalize!" do
126
- subject { TeckelChainTest::Chain.dup }
127
-
128
- it "freezes the Chain class and operation classes" do
129
- subject.finalize!
130
-
131
- steps = subject.steps
132
- expect(steps).to be_frozen
133
- expect(steps).to all be_frozen
134
- end
135
-
136
- it "disallows adding new steps" do
137
- subject.class_eval do
138
- step :other, TeckelChainTest::AddFriend
139
- end
140
-
141
- subject.finalize!
142
-
143
- expect {
144
- subject.class_eval do
145
- step :yet_other, TeckelChainTest::AddFriend
146
- end
147
- }.to raise_error(frozen_error)
148
- end
149
-
150
- it "disallows changing around hook" do
151
- subject.class_eval do
152
- around ->{}
153
- end
154
-
155
- chain2 = TeckelChainTest::Chain.dup.finalize!
156
- expect {
157
- chain2.class_eval do
158
- around ->{}
159
- end
160
- }.to raise_error(frozen_error)
161
- end
162
-
163
- it "runs" do
164
- subject.finalize!
165
-
166
- result = subject.call(name: "Bob", age: 23)
167
- expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
168
- end
169
-
170
- it "accepts mocks" do
171
- subject.finalize!
172
-
173
- allow(subject).to receive(:call) { :mocked }
174
- expect(subject.call).to eq(:mocked)
175
- end
176
- end
177
-
178
- describe "#clone" do
179
- subject { TeckelChainTest::Chain.dup }
180
- let(:klone) { subject.clone }
181
-
182
- it 'clones' do
183
- expect(klone.object_id).not_to be_eql(subject.object_id)
184
- end
185
-
186
- it 'clones config' do
187
- orig_config = subject.instance_variable_get(:@config)
188
- klone_config = klone.instance_variable_get(:@config)
189
- expect(klone_config.object_id).not_to be_eql(orig_config.object_id)
190
- end
191
-
192
- it 'clones steps' do
193
- orig_settings = subject.instance_variable_get(:@config).instance_variable_get(:@config)[:steps]
194
- klone_settings = klone.instance_variable_get(:@config).instance_variable_get(:@config)[:steps]
195
-
196
- expect(orig_settings).to be_a(Array)
197
- expect(klone_settings).to be_a(Array)
198
- expect(klone_settings.object_id).not_to be_eql(orig_settings.object_id)
199
- end
200
- end
201
-
202
- describe "frozen" do
203
- subject { TeckelChainTest::Chain.dup }
204
-
205
- it "also freezes the config" do
206
- expect { subject.freeze }.to change {
207
- [
208
- subject.frozen?,
209
- subject.instance_variable_get(:@config).frozen?
210
- ]
211
- }.from([false, false]).to([true, true])
212
- end
213
-
214
- it "prevents changes to steps" do
215
- subject.freeze
216
- expect {
217
- subject.class_eval do
218
- step :yet_other, TeckelChainTest::AddFriend
219
- end
220
- }.to raise_error(frozen_error)
221
- end
222
-
223
- it "prevents changes to config" do
224
- subject.freeze
225
- expect {
226
- subject.class_eval do
227
- default_settings!(a: { say: "Chain Default" })
228
- end
229
- }.to raise_error(frozen_error)
230
- end
231
-
232
- describe '#clone' do
233
- subject { TeckelChainTest::Chain.dup }
234
-
235
- it 'clones the class' do
236
- subject.freeze
237
- klone = subject.clone
238
-
239
- expect(klone).to be_frozen
240
- expect(klone.object_id).not_to be_eql(subject.object_id)
241
- end
242
-
243
- it 'cloned class uses the same, frozen config' do
244
- subject.freeze
245
- klone = subject.clone
246
-
247
- orig_config = subject.instance_variable_get(:@config)
248
- klone_config = klone.instance_variable_get(:@config)
249
-
250
- expect(klone_config).to be_frozen
251
- expect(klone_config.object_id).to be_eql(orig_config.object_id)
252
- end
253
- end
254
- end
255
- end
data/spec/config_spec.rb DELETED
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'support/dry_base'
4
- require 'support/fake_models'
5
-
6
- RSpec.describe Teckel::Config do
7
- let(:sample_config) do
8
- Teckel::Config.new
9
- end
10
-
11
- it "set and retrieve key" do
12
- sample_config.for(:some_key, "some_value")
13
- expect(sample_config.for(:some_key)).to eq("some_value")
14
- end
15
-
16
- it "allow default value via block" do
17
- expect(sample_config.for(:some_key) { "default" }).to eq("default")
18
- # and sets the block value
19
- expect(sample_config.for(:some_key)).to eq("default")
20
- end
21
-
22
- it "raises FrozenConfigError when setting a key twice" do
23
- sample_config.for(:some_key, "some_value")
24
- expect { sample_config.for(:some_key, "other_value") }.to raise_error(Teckel::FrozenConfigError)
25
- end
26
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'support/dry_base'
4
- require_relative 'support/fake_db'
5
- require_relative 'support/fake_models'
6
-
7
- require "teckel"
8
- require "teckel/chain"
@@ -1,227 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ostruct"
4
-
5
- RSpec.describe Teckel::Operation do
6
- let(:operation) do
7
- Class.new do
8
- include Teckel::Operation
9
- input none
10
- output ->(o) { o }
11
- error none
12
-
13
- def call(_)
14
- success! settings
15
- end
16
- end
17
- end
18
-
19
- let(:blank_operation) do
20
- Class.new do
21
- include Teckel::Operation
22
- end
23
- end
24
-
25
- describe ".settings" do
26
- specify "no settings" do
27
- expect(operation.settings).to eq(Teckel::Contracts::None)
28
- expect(operation.settings_constructor).to eq(Teckel::Contracts::None.method(:new))
29
- end
30
-
31
- specify "with settings klass" do
32
- settings_klass = Struct.new(:name)
33
- operation.settings(settings_klass)
34
- expect(operation.settings).to eq(settings_klass)
35
- end
36
-
37
- specify "without settings class, with settings constructor as proc" do
38
- settings_const = if RUBY_VERSION < '2.6.0'
39
- ->(sets) { sets.map { |k, v| [k.to_s, v.to_i] }.to_h }
40
- else
41
- ->(sets) { sets.to_h { |k, v| [k.to_s, v.to_i] } }
42
- end
43
-
44
- operation.settings_constructor(settings_const)
45
-
46
- expect(operation.settings).to eq(Teckel::Contracts::None)
47
- expect(operation.settings_constructor).to eq(settings_const)
48
-
49
- runner = operation.with(key: "1")
50
- expect(runner).to be_a(Teckel::Operation::Runner)
51
- expect(runner.settings).to eq({ "key" => 1 })
52
- end
53
-
54
- specify "with settings class, with settings constructor as symbol" do
55
- settings_klass = Struct.new(:name) do
56
- def self.make_one(opts)
57
- new(opts[:name])
58
- end
59
- end
60
-
61
- operation.settings(settings_klass)
62
- operation.settings_constructor(:make_one)
63
-
64
- expect(operation.settings).to eq(settings_klass)
65
- expect(operation.settings_constructor).to eq(settings_klass.method(:make_one))
66
-
67
- runner = operation.with(name: "value")
68
- expect(runner).to be_a(Teckel::Operation::Runner)
69
- expect(runner.settings).to be_a(settings_klass)
70
- expect(runner.settings.name).to eq("value")
71
- end
72
-
73
- specify "with settings class as constant" do
74
- settings_klass = Struct.new(:name)
75
- operation.const_set(:Settings, settings_klass)
76
-
77
- expect(operation.settings).to eq(settings_klass)
78
- expect(operation.settings_constructor).to eq(settings_klass.method(:[]))
79
- end
80
- end
81
-
82
- describe ".default_settings" do
83
- specify "no default_settings" do
84
- expect(operation.default_settings).to be_nil
85
- expect(operation.runner).to receive(:new).with(operation).and_call_original
86
-
87
- operation.call
88
- end
89
-
90
- specify "default_settings!() with no default_settings" do
91
- operation.default_settings!
92
- expect(operation.default_settings).to be_a(Proc)
93
-
94
- expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
95
- settings = original_method.call(*args, &block)
96
- expect(settings).to be_nil
97
- expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
98
- settings
99
- end
100
-
101
- operation.call
102
- end
103
-
104
- specify "default_settings!() with default_settings" do
105
- settings_klass = Struct.new(:name)
106
-
107
- operation.settings(settings_klass)
108
- operation.default_settings!
109
-
110
- expect(operation.default_settings).to be_a(Proc)
111
-
112
- expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
113
- settings = original_method.call(*args, &block)
114
- expect(settings).to be_a(settings_klass).and have_attributes(name: nil)
115
- expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
116
- settings
117
- end
118
-
119
- operation.call
120
- end
121
-
122
- specify "default_settings!(arg) with default_settings" do
123
- settings_klass = Struct.new(:name)
124
-
125
- operation.settings(settings_klass)
126
- operation.default_settings!("Bob")
127
-
128
- expect(operation.default_settings).to be_a(Proc)
129
-
130
- expect(operation.default_settings).to receive(:call).with(no_args).and_wrap_original do |original_method, *args, &block|
131
- settings = original_method.call(*args, &block)
132
- expect(settings).to be_a(settings_klass).and have_attributes(name: "Bob")
133
- expect(operation.runner).to receive(:new).with(operation, settings).and_call_original
134
- settings
135
- end
136
-
137
- expect(operation.call).to be_a(Struct).and have_attributes(name: "Bob")
138
- end
139
- end
140
-
141
- %i[input output error].each do |meth|
142
- describe ".#{meth}" do
143
- specify "missing .#{meth} config raises MissingConfigError" do
144
- expect {
145
- blank_operation.public_send(meth)
146
- }.to raise_error(Teckel::MissingConfigError, "Missing #{meth} config for #{blank_operation}")
147
- end
148
- end
149
-
150
- describe ".#{meth}_constructor" do
151
- specify "missing .#{meth}_constructor config raises MissingConfigError for missing #{meth}" do
152
- expect {
153
- blank_operation.public_send(:"#{meth}_constructor")
154
- }.to raise_error(Teckel::MissingConfigError, "Missing #{meth} config for #{blank_operation}")
155
- end
156
- end
157
- end
158
-
159
- specify "default settings config" do
160
- expect(blank_operation.settings).to eq(Teckel::Contracts::None)
161
- end
162
-
163
- specify "default settings_constructor" do
164
- expect(blank_operation.settings_constructor).to eq(Teckel::Contracts::None.method(:[]))
165
- end
166
-
167
- specify "default settings_constructor with settings config set" do
168
- settings_klass = Struct.new(:name)
169
- blank_operation.settings(settings_klass)
170
-
171
- expect(blank_operation.settings_constructor).to eq(settings_klass.method(:[]))
172
- end
173
-
174
- specify "unsupported constructor method" do
175
- blank_operation.settings(Class.new)
176
- expect {
177
- blank_operation.settings_constructor(:nope)
178
- }.to raise_error(Teckel::MissingConfigError, "Missing settings_constructor config for #{blank_operation}")
179
-
180
- expect {
181
- blank_operation.settings_constructor
182
- }.to raise_error(Teckel::MissingConfigError, "Missing settings_constructor config for #{blank_operation}")
183
- end
184
-
185
- describe "result" do
186
- specify "default result config" do
187
- expect(blank_operation.result).to eq(Teckel::Operation::ValueResult)
188
- end
189
-
190
- specify "default result_constructor" do
191
- expect(blank_operation.result_constructor).to eq(Teckel::Operation::ValueResult.method(:[]))
192
- end
193
-
194
- specify "default result_constructor with settings config set" do
195
- result_klass = OpenStruct.new
196
- blank_operation.result(result_klass)
197
-
198
- expect(blank_operation.result_constructor).to eq(result_klass.method(:[]))
199
- end
200
-
201
- specify "unsupported constructor method" do
202
- blank_operation.result(Class.new)
203
- expect {
204
- blank_operation.result_constructor(:nope)
205
- }.to raise_error(Teckel::MissingConfigError, "Missing result_constructor config for #{blank_operation}")
206
-
207
- expect {
208
- blank_operation.result_constructor
209
- }.to raise_error(Teckel::MissingConfigError, "Missing result_constructor config for #{blank_operation}")
210
- end
211
-
212
- specify "with result class as constant" do
213
- result_klass = OpenStruct.new
214
- blank_operation.const_set(:Result, result_klass)
215
-
216
- expect(blank_operation.result).to eq(result_klass)
217
- expect(blank_operation.result_constructor).to eq(result_klass.method(:[]))
218
- end
219
- end
220
-
221
- describe "result!" do
222
- specify "default result config" do
223
- blank_operation.result!
224
- expect(blank_operation.result).to eq(Teckel::Operation::Result)
225
- end
226
- end
227
- end
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'support/dry_base'
4
-
5
- module TeckelOperationContractTrace
6
- DefaultError = Struct.new(:message, :status_code)
7
- Settings = Struct.new(:fail_it)
8
-
9
- class ApplicationOperation
10
- include Teckel::Operation
11
-
12
- class Input < Dry::Struct
13
- attribute :input_data, Types::String
14
- end
15
-
16
- class Output < Dry::Struct
17
- attribute :output_data, Types::String
18
- end
19
-
20
- class Error < Dry::Struct
21
- attribute :error_data, Types::String
22
- end
23
-
24
- # Freeze the base class to make sure it's inheritable configuration is not altered
25
- freeze
26
- end
27
- end
28
-
29
- # rubocop:disable Style/EvalWithLocation
30
- # Hack to get reliable stack traces
31
- eval <<~RUBY, binding, "operation_success_error.rb"
32
- module TeckelOperationContractTrace
33
- class OperationSuccessError < ApplicationOperation
34
- # Includes a deliberate bug while crating a success output
35
- def call(input)
36
- success!(incorrect_key: 1)
37
- end
38
- end
39
- end
40
- RUBY
41
-
42
- eval <<~RUBY, binding, "operation_simple_success_error.rb"
43
- module TeckelOperationContractTrace
44
- class OperationSimpleSuccessNil < ApplicationOperation
45
- # Includes a deliberate bug while crating a success output
46
- def call(input)
47
- return { incorrect_key: 1 }
48
- end
49
- end
50
- end
51
- RUBY
52
-
53
- eval <<~RUBY, binding, "operation_failure_error.rb"
54
- module TeckelOperationContractTrace
55
- class OperationFailureError < ApplicationOperation
56
- # Includes a deliberate bug while crating an error output
57
- def call(input)
58
- fail!(incorrect_key: 1)
59
- end
60
- end
61
- end
62
- RUBY
63
-
64
- eval <<~RUBY, binding, "operation_ok.rb"
65
- module TeckelOperationContractTrace
66
- class OperationOk < ApplicationOperation
67
- def call(input)
68
- success!(output_data: "all fine")
69
- end
70
- end
71
- end
72
- RUBY
73
-
74
- eval <<~RUBY, binding, "operation_input_error.rb"
75
- module TeckelOperationContractTrace
76
- def self.run_operation(operation)
77
- operation.call(error_input_data: "failure")
78
- end
79
- end
80
- RUBY
81
- # rubocop:enable Style/EvalWithLocation
82
-
83
- RSpec.describe Teckel::Operation do
84
- context "contract errors include meaningful trace" do
85
- specify "incorrect success" do
86
- expect {
87
- TeckelOperationContractTrace::OperationSuccessError.call(input_data: "ok")
88
- }.to raise_error(Dry::Struct::Error) { |error|
89
- expect(error.backtrace).to include /^#{Regexp.escape("operation_success_error.rb:5:in `call'")}$/
90
- }
91
- end
92
-
93
- specify "incorrect success via simple return results in +nil+, but no meaningful trace" do
94
- expect(
95
- TeckelOperationContractTrace::OperationSimpleSuccessNil.call(input_data: "ok")
96
- ).to be_nil
97
- end
98
-
99
- specify "incorrect fail" do
100
- expect {
101
- TeckelOperationContractTrace::OperationFailureError.call(input_data: "ok")
102
- }.to raise_error(Dry::Struct::Error) { |error|
103
- expect(error.backtrace).to include /^#{Regexp.escape("operation_failure_error.rb:5:in `call'")}$/
104
- }
105
- end
106
-
107
- specify "incorrect input" do
108
- operation = TeckelOperationContractTrace::OperationOk
109
-
110
- expect(operation.call(input_data: "ok")).to eq(operation.output[output_data: "all fine"])
111
- expect {
112
- TeckelOperationContractTrace.run_operation(operation)
113
- }.to raise_error(Dry::Struct::Error) { |error|
114
- expect(error.backtrace).to include /^#{Regexp.escape("operation_input_error.rb:3:in `run_operation'")}$/
115
- }
116
- end
117
- end
118
- end