teckel 0.7.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,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'support/dry_base'
4
- require 'support/fake_db'
5
- require 'support/fake_models'
6
-
7
- module TeckelChainAroundHookTest
8
- class CreateUser
9
- include ::Teckel::Operation
10
-
11
- result!
12
-
13
- input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
14
- output Types.Instance(User)
15
- error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
16
-
17
- def call(input)
18
- user = User.new(name: input[:name], age: input[:age])
19
- if user.save
20
- success!(user)
21
- else
22
- fail!(message: "Could not safe User", errors: user.errors)
23
- end
24
- end
25
- end
26
-
27
- class AddFriend
28
- include ::Teckel::Operation
29
-
30
- result!
31
-
32
- settings Struct.new(:fail_befriend)
33
-
34
- input Types.Instance(User)
35
- output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
36
- error Types::Hash.schema(message: Types::String)
37
-
38
- def call(user)
39
- if settings&.fail_befriend
40
- fail!(message: "Did not find a friend.")
41
- else
42
- success! user: user, friend: User.new(name: "A friend", age: 42)
43
- end
44
- end
45
- end
46
-
47
- @stack = []
48
- def self.stack
49
- @stack
50
- end
51
-
52
- class Chain
53
- include Teckel::Chain
54
-
55
- around ->(chain, input) {
56
- result = nil
57
- begin
58
- TeckelChainAroundHookTest.stack << :before
59
-
60
- FakeDB.transaction do
61
- result = chain.call(input)
62
- raise FakeDB::Rollback if result.failure?
63
- end
64
-
65
- TeckelChainAroundHookTest.stack << :after
66
- result
67
- rescue FakeDB::Rollback
68
- result
69
- end
70
- }
71
-
72
- step :create, CreateUser
73
- step :befriend, AddFriend
74
- end
75
- end
76
-
77
- RSpec.describe Teckel::Chain do
78
- before { TeckelChainAroundHookTest.stack.clear }
79
-
80
- context "success" do
81
- it "result matches" do
82
- result = TeckelChainAroundHookTest::Chain.call(name: "Bob", age: 23)
83
- expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
84
- end
85
-
86
- it "runs around hook" do
87
- TeckelChainAroundHookTest::Chain.call(name: "Bob", age: 23)
88
- expect(TeckelChainAroundHookTest.stack).to eq([:before, :after])
89
- end
90
- end
91
-
92
- context "failure" do
93
- it "runs around hook" do
94
- TeckelChainAroundHookTest::Chain.
95
- with(befriend: :fail).
96
- call(name: "Bob", age: 23)
97
- expect(TeckelChainAroundHookTest.stack).to eq([:before])
98
- end
99
- end
100
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TeckelChainDefaultSettingsTest
4
- class MyOperation
5
- include Teckel::Operation
6
- result!
7
-
8
- settings Struct.new(:say, :other)
9
- settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) } # ruby 2.4 way for `keyword_init: true`
10
-
11
- input none
12
- output Hash
13
- error none
14
-
15
- def call(_)
16
- success! settings.to_h
17
- end
18
- end
19
-
20
- class Chain
21
- include Teckel::Chain
22
-
23
- default_settings!(a: { say: "Chain Default" })
24
-
25
- step :a, MyOperation
26
- end
27
- end
28
-
29
- RSpec.describe Teckel::Chain do
30
- specify "call chain without settings, uses default settings" do
31
- result = TeckelChainDefaultSettingsTest::Chain.call
32
- expect(result.success).to eq(say: "Chain Default", other: nil)
33
- end
34
-
35
- specify "call chain with explicit settings, overwrites defaults" do
36
- result = TeckelChainDefaultSettingsTest::Chain.with(a: { other: "What" }).call
37
- expect(result.success).to eq(say: nil, other: "What")
38
- end
39
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'support/dry_base'
4
- require 'support/fake_models'
5
-
6
- module TeckelChainDefaultsViaBaseClass
7
- LOG = [] # rubocop:disable Style/MutableConstant
8
-
9
- class LoggingChain
10
- include Teckel::Chain
11
-
12
- around do |chain, input|
13
- require 'benchmark'
14
- result = nil
15
- LOG << Benchmark.measure { result = chain.call(input) }
16
- result
17
- end
18
-
19
- freeze
20
- end
21
-
22
- class OperationA
23
- include Teckel::Operation
24
-
25
- result!
26
-
27
- input none
28
- output Types::Integer
29
- error none
30
-
31
- def call(_)
32
- success! rand(1000)
33
- end
34
-
35
- finalize!
36
- end
37
-
38
- class OperationB
39
- include Teckel::Operation
40
-
41
- result!
42
-
43
- input none
44
- output Types::String
45
- error none
46
-
47
- def call(_)
48
- success! ("a".."z").to_a.sample
49
- end
50
-
51
- finalize!
52
- end
53
-
54
- class ChainA < LoggingChain
55
- step :roll, OperationA
56
-
57
- finalize!
58
- end
59
-
60
- class ChainB < LoggingChain
61
- step :say, OperationB
62
-
63
- finalize!
64
- end
65
-
66
- class ChainC < ChainB
67
- finalize!
68
- end
69
- end
70
-
71
- RSpec.describe Teckel::Chain do
72
- before do
73
- TeckelChainDefaultsViaBaseClass::LOG.clear
74
- end
75
-
76
- let(:base_chain) { TeckelChainDefaultsViaBaseClass::LoggingChain }
77
- let(:chain_a) { TeckelChainDefaultsViaBaseClass::ChainA }
78
- let(:chain_b) { TeckelChainDefaultsViaBaseClass::ChainB }
79
- let(:chain_c) { TeckelChainDefaultsViaBaseClass::ChainC }
80
-
81
- it "inherits config" do
82
- expect(chain_a.around)
83
- expect(chain_b.around)
84
-
85
- expect(base_chain.steps).to eq([])
86
- expect(chain_a.steps.size).to eq(1)
87
- expect(chain_b.steps.size).to eq(1)
88
- end
89
-
90
- it "runs chain a" do
91
- expect {
92
- result = chain_a.call
93
- expect(result.success).to be_a(Integer)
94
- }.to change {
95
- TeckelChainDefaultsViaBaseClass::LOG.size
96
- }.from(0).to(1)
97
- end
98
-
99
- it "runs chain b" do
100
- expect {
101
- result = chain_b.call
102
- expect(result.success).to be_a(String)
103
- }.to change {
104
- TeckelChainDefaultsViaBaseClass::LOG.size
105
- }.from(0).to(1)
106
- end
107
-
108
- it "inherits steps" do
109
- expect {
110
- result = chain_c.call
111
- expect(result.success).to be_a(String)
112
- }.to change {
113
- TeckelChainDefaultsViaBaseClass::LOG.size
114
- }.from(0).to(1)
115
- end
116
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TeckelChainNoneInputTest
4
- class MyOperation
5
- include Teckel::Operation
6
- result!
7
-
8
- settings Struct.new(:say)
9
-
10
- input none
11
- output String
12
- error none
13
-
14
- def call(_)
15
- success!(settings&.say || "Called")
16
- end
17
- end
18
-
19
- class Chain
20
- include Teckel::Chain
21
-
22
- step :a, MyOperation
23
- end
24
- end
25
-
26
- RSpec.describe Teckel::Chain do
27
- specify "call chain without input value" do
28
- result = TeckelChainNoneInputTest::Chain.call
29
- expect(result.success).to eq("Called")
30
- end
31
-
32
- specify "call chain runner without input value" do
33
- result = TeckelChainNoneInputTest::Chain.with(a: "What").call
34
- expect(result.success).to eq("What")
35
- end
36
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'support/dry_base'
4
- require 'support/fake_models'
5
-
6
- module TeckelChainResultTest
7
- class Message
8
- include ::Teckel::Operation
9
-
10
- result!
11
-
12
- input Types::Hash.schema(message: Types::String)
13
- error none
14
- output Types::String
15
-
16
- def call(input)
17
- success! input[:message].upcase
18
- end
19
- end
20
-
21
- class Chain
22
- include Teckel::Chain
23
-
24
- step :message, Message
25
-
26
- class Result < Teckel::Operation::Result
27
- def initialize(value, success, step, opts = {})
28
- super(value, success)
29
- @step = step
30
- @opts = opts
31
- end
32
-
33
- class << self
34
- alias :[] :new # Alias the default constructor to :new
35
- end
36
-
37
- attr_reader :opts, :step
38
- end
39
-
40
- result_constructor ->(value, success, step) {
41
- result.new(value, success, step, time: Time.now.to_i)
42
- }
43
- end
44
- end
45
-
46
- RSpec.describe Teckel::Chain do
47
- specify do
48
- result = TeckelChainResultTest::Chain.call(message: "Hello World!")
49
- expect(result).to be_successful
50
- expect(result.success).to eq("HELLO WORLD!")
51
- expect(result.opts).to include(time: kind_of(Integer))
52
- end
53
- end
data/spec/chain_spec.rb DELETED
@@ -1,180 +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
- it 'Chain input points to first step input' do
71
- expect(TeckelChainTest::Chain.input).to eq(TeckelChainTest::CreateUser.input)
72
- end
73
-
74
- it 'Chain output points to last steps output' do
75
- expect(TeckelChainTest::Chain.output).to eq(TeckelChainTest::AddFriend.output)
76
- end
77
-
78
- it 'Chain errors maps all step errors' do
79
- expect(TeckelChainTest::Chain.errors).to eq([
80
- TeckelChainTest::CreateUser.error,
81
- Teckel::Contracts::None,
82
- TeckelChainTest::AddFriend.error
83
- ])
84
- end
85
-
86
- context "success" do
87
- it "result matches" do
88
- result =
89
- TeckelChainTest::Chain.
90
- with(befriend: nil).
91
- call(name: "Bob", age: 23)
92
-
93
- expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
94
- end
95
- end
96
-
97
- context "failure" do
98
- it "returns a Result for invalid input" do
99
- result =
100
- TeckelChainTest::Chain.
101
- with(befriend: :fail).
102
- call(name: "Bob", age: 0)
103
-
104
- expect(result).to be_a(Teckel::Chain::Result)
105
- expect(result).to be_failure
106
- expect(result.step).to eq(:create)
107
- expect(result.value).to eq(errors: [{ age: "underage" }], message: "Could not save User")
108
- end
109
-
110
- it "returns a Result for failed step" do
111
- result =
112
- TeckelChainTest::Chain.
113
- with(befriend: :fail).
114
- call(name: "Bob", age: 23)
115
-
116
- expect(result).to be_a(Teckel::Chain::Result)
117
- expect(result).to be_failure
118
- expect(result.step).to eq(:befriend)
119
- expect(result.value).to eq(message: "Did not find a friend.")
120
- end
121
- end
122
-
123
- describe "#finalize!" do
124
- let(:frozen_error) do
125
- # different ruby versions raise different errors
126
- defined?(FrozenError) ? FrozenError : RuntimeError
127
- end
128
-
129
- subject { TeckelChainTest::Chain.dup }
130
-
131
- it "freezes the Chain class and operation classes" do
132
- subject.finalize!
133
-
134
- steps = subject.steps
135
- expect(steps).to be_frozen
136
- expect(steps).to all be_frozen
137
- end
138
-
139
- it "disallows adding new steps" do
140
- subject.class_eval do
141
- step :other, TeckelChainTest::AddFriend
142
- end
143
-
144
- subject.finalize!
145
-
146
- expect {
147
- subject.class_eval do
148
- step :yet_other, TeckelChainTest::AddFriend
149
- end
150
- }.to raise_error(frozen_error)
151
- end
152
-
153
- it "disallows changing around hook" do
154
- subject.class_eval do
155
- around ->{}
156
- end
157
-
158
- chain2 = TeckelChainTest::Chain.dup.finalize!
159
- expect {
160
- chain2.class_eval do
161
- around ->{}
162
- end
163
- }.to raise_error(frozen_error)
164
- end
165
-
166
- it "runs" do
167
- subject.finalize!
168
-
169
- result = subject.call(name: "Bob", age: 23)
170
- expect(result.success).to include(user: kind_of(User), friend: kind_of(User))
171
- end
172
-
173
- it "accepts mocks" do
174
- subject.finalize!
175
-
176
- allow(subject).to receive(:call) { :mocked }
177
- expect(subject.call).to eq(:mocked)
178
- end
179
- end
180
- 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,116 +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
- # Hack to get reliable stack traces
30
- eval <<~RUBY, binding, "operation_success_error.rb"
31
- module TeckelOperationContractTrace
32
- class OperationSuccessError < ApplicationOperation
33
- # Includes a deliberate bug while crating a success output
34
- def call(input)
35
- success!(incorrect_key: 1)
36
- end
37
- end
38
- end
39
- RUBY
40
-
41
- eval <<~RUBY, binding, "operation_simple_success_error.rb"
42
- module TeckelOperationContractTrace
43
- class OperationSimpleSuccessNil < ApplicationOperation
44
- # Includes a deliberate bug while crating a success output
45
- def call(input)
46
- return { incorrect_key: 1 }
47
- end
48
- end
49
- end
50
- RUBY
51
-
52
- eval <<~RUBY, binding, "operation_failure_error.rb"
53
- module TeckelOperationContractTrace
54
- class OperationFailureError < ApplicationOperation
55
- # Includes a deliberate bug while crating an error output
56
- def call(input)
57
- fail!(incorrect_key: 1)
58
- end
59
- end
60
- end
61
- RUBY
62
-
63
- eval <<~RUBY, binding, "operation_ok.rb"
64
- module TeckelOperationContractTrace
65
- class OperationOk < ApplicationOperation
66
- def call(input)
67
- success!(output_data: "all fine")
68
- end
69
- end
70
- end
71
- RUBY
72
-
73
- eval <<~RUBY, binding, "operation_input_error.rb"
74
- module TeckelOperationContractTrace
75
- def self.run_operation(operation)
76
- operation.call(error_input_data: "failure")
77
- end
78
- end
79
- RUBY
80
-
81
- RSpec.describe Teckel::Operation do
82
- context "contract errors include meaningful trace" do
83
- specify "incorrect success" do
84
- expect {
85
- TeckelOperationContractTrace::OperationSuccessError.call(input_data: "ok")
86
- }.to raise_error(Dry::Struct::Error) { |error|
87
- expect(error.backtrace).to include /^#{Regexp.escape("operation_success_error.rb:5:in `call'")}$/
88
- }
89
- end
90
-
91
- specify "incorrect success via simple return results in +nil+, but no meaningful trace" do
92
- expect(
93
- TeckelOperationContractTrace::OperationSimpleSuccessNil.call(input_data: "ok")
94
- ).to be_nil
95
- end
96
-
97
- specify "incorrect fail" do
98
- expect {
99
- TeckelOperationContractTrace::OperationFailureError.call(input_data: "ok")
100
- }.to raise_error(Dry::Struct::Error) { |error|
101
- expect(error.backtrace).to include /^#{Regexp.escape("operation_failure_error.rb:5:in `call'")}$/
102
- }
103
- end
104
-
105
- specify "incorrect input" do
106
- operation = TeckelOperationContractTrace::OperationOk
107
-
108
- expect(operation.call(input_data: "ok")).to eq(operation.output[output_data: "all fine"])
109
- expect {
110
- TeckelOperationContractTrace.run_operation(operation)
111
- }.to raise_error(Dry::Struct::Error) { |error|
112
- expect(error.backtrace).to include /^#{Regexp.escape("operation_input_error.rb:3:in `run_operation'")}$/
113
- }
114
- end
115
- end
116
- end