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.
data/lib/teckel/result.rb CHANGED
@@ -47,16 +47,18 @@ module Teckel
47
47
  end
48
48
 
49
49
  def deconstruct_keys(keys)
50
- {}.tap do |e|
51
- e[:success] = successful? if keys.include?(:success)
52
- e[:value] = value if keys.include?(:value)
53
- end
50
+ e = {}
51
+ e[:success] = successful? if keys.include?(:success)
52
+ e[:value] = value if keys.include?(:value)
53
+ e
54
54
  end
55
55
  end
56
56
 
57
57
  def self.included(receiver)
58
- receiver.extend ClassMethods
59
- receiver.send :include, InstanceMethods
58
+ receiver.class_eval do
59
+ extend ClassMethods
60
+ include InstanceMethods
61
+ end
60
62
  end
61
63
  end
62
64
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teckel
4
- VERSION = "0.4.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/teckel.rb CHANGED
@@ -19,4 +19,3 @@ require_relative "teckel/config"
19
19
  require_relative "teckel/contracts"
20
20
  require_relative "teckel/result"
21
21
  require_relative "teckel/operation"
22
- require_relative "teckel/chain"
@@ -0,0 +1,100 @@
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
@@ -0,0 +1,39 @@
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
@@ -3,72 +3,72 @@
3
3
  require 'support/dry_base'
4
4
  require 'support/fake_models'
5
5
 
6
- RSpec.describe Teckel::Chain do
7
- module TeckelChainDefaultsViaBaseClass
8
- LOG = [] # rubocop:disable Style/MutableConstant
9
-
10
- class LoggingChain
11
- include Teckel::Chain
6
+ module TeckelChainDefaultsViaBaseClass
7
+ LOG = [] # rubocop:disable Style/MutableConstant
12
8
 
13
- around do |chain, input|
14
- require 'benchmark'
15
- result = nil
16
- LOG << Benchmark.measure { result = chain.call(input) }
17
- result
18
- end
9
+ class LoggingChain
10
+ include Teckel::Chain
19
11
 
20
- freeze
12
+ around do |chain, input|
13
+ require 'benchmark'
14
+ result = nil
15
+ LOG << Benchmark.measure { result = chain.call(input) }
16
+ result
21
17
  end
22
18
 
23
- class OperationA
24
- include Teckel::Operation
19
+ freeze
20
+ end
25
21
 
26
- result!
22
+ class OperationA
23
+ include Teckel::Operation
27
24
 
28
- input none
29
- output Types::Integer
30
- error none
25
+ result!
31
26
 
32
- def call(_)
33
- rand(1000)
34
- end
27
+ input none
28
+ output Types::Integer
29
+ error none
35
30
 
36
- finalize!
31
+ def call(_)
32
+ success! rand(1000)
37
33
  end
38
34
 
39
- class OperationB
40
- include Teckel::Operation
35
+ finalize!
36
+ end
41
37
 
42
- result!
38
+ class OperationB
39
+ include Teckel::Operation
43
40
 
44
- input none
45
- output Types::String
46
- error none
41
+ result!
47
42
 
48
- def call(_)
49
- ("a".."z").to_a.sample
50
- end
43
+ input none
44
+ output Types::String
45
+ error none
51
46
 
52
- finalize!
47
+ def call(_)
48
+ success! ("a".."z").to_a.sample
53
49
  end
54
50
 
55
- class ChainA < LoggingChain
56
- step :roll, OperationA
51
+ finalize!
52
+ end
57
53
 
58
- finalize!
59
- end
54
+ class ChainA < LoggingChain
55
+ step :roll, OperationA
60
56
 
61
- class ChainB < LoggingChain
62
- step :say, OperationB
57
+ finalize!
58
+ end
63
59
 
64
- finalize!
65
- end
60
+ class ChainB < LoggingChain
61
+ step :say, OperationB
66
62
 
67
- class ChainC < ChainB
68
- finalize!
69
- end
63
+ finalize!
64
+ end
65
+
66
+ class ChainC < ChainB
67
+ finalize!
70
68
  end
69
+ end
71
70
 
71
+ RSpec.describe Teckel::Chain do
72
72
  before do
73
73
  TeckelChainDefaultsViaBaseClass::LOG.clear
74
74
  end
@@ -0,0 +1,36 @@
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
@@ -3,47 +3,47 @@
3
3
  require 'support/dry_base'
4
4
  require 'support/fake_models'
5
5
 
6
- RSpec.describe Teckel::Chain do
7
- module TeckelChainResultTest
8
- class Message
9
- include ::Teckel::Operation
6
+ module TeckelChainResultTest
7
+ class Message
8
+ include ::Teckel::Operation
10
9
 
11
- result!
10
+ result!
12
11
 
13
- input Types::Hash.schema(message: Types::String)
14
- error none
15
- output Types::String
12
+ input Types::Hash.schema(message: Types::String)
13
+ error none
14
+ output Types::String
16
15
 
17
- def call(input)
18
- input[:message].upcase
19
- end
16
+ def call(input)
17
+ success! input[:message].upcase
20
18
  end
19
+ end
21
20
 
22
- class Chain
23
- include Teckel::Chain
24
-
25
- step :message, Message
21
+ class Chain
22
+ include Teckel::Chain
26
23
 
27
- class Result < Teckel::Operation::Result
28
- def initialize(value, success, step, opts = {})
29
- super(value, success)
30
- @step = step
31
- @opts = opts
32
- end
24
+ step :message, Message
33
25
 
34
- class << self
35
- alias :[] :new # Alias the default constructor to :new
36
- end
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
37
32
 
38
- attr_reader :opts, :step
33
+ class << self
34
+ alias :[] :new # Alias the default constructor to :new
39
35
  end
40
36
 
41
- result_constructor ->(value, success, step) {
42
- result.new(value, success, step, time: Time.now.to_i)
43
- }
37
+ attr_reader :opts, :step
44
38
  end
39
+
40
+ result_constructor ->(value, success, step) {
41
+ result.new(value, success, step, time: Time.now.to_i)
42
+ }
45
43
  end
44
+ end
46
45
 
46
+ RSpec.describe Teckel::Chain do
47
47
  specify do
48
48
  result = TeckelChainResultTest::Chain.call(message: "Hello World!")
49
49
  expect(result).to be_successful
data/spec/chain_spec.rb CHANGED
@@ -3,68 +3,73 @@
3
3
  require 'support/dry_base'
4
4
  require 'support/fake_models'
5
5
 
6
- RSpec.describe Teckel::Chain do
7
- module TeckelChainTest
8
- class CreateUser
9
- include ::Teckel::Operation
10
- result!
11
-
12
- input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
13
- output Types.Instance(User)
14
- error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
15
-
16
- def call(input)
17
- user = User.new(name: input[:name], age: input[:age])
18
- if user.save
19
- success!(user)
20
- else
21
- fail!(message: "Could not save User", errors: user.errors)
22
- end
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)
23
21
  end
24
22
  end
23
+ end
25
24
 
26
- class LogUser
27
- include ::Teckel::Operation
25
+ class LogUser
26
+ include ::Teckel::Operation
28
27
 
29
- result!
28
+ result!
30
29
 
31
- input Types.Instance(User)
32
- error none
33
- output input
30
+ input Types.Instance(User)
31
+ error none
32
+ output input
34
33
 
35
- def call(usr)
36
- Logger.new(File::NULL).info("User #{usr.name} created")
37
- usr
38
- end
34
+ def call(usr)
35
+ Logger.new(File::NULL).info("User #{usr.name} created")
36
+ success! usr
39
37
  end
38
+ end
40
39
 
41
- class AddFriend
42
- include ::Teckel::Operation
40
+ class AddFriend
41
+ include ::Teckel::Operation
43
42
 
44
- result!
43
+ result!
45
44
 
46
- settings Struct.new(:fail_befriend)
45
+ settings Struct.new(:fail_befriend)
47
46
 
48
- input Types.Instance(User)
49
- output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
50
- error Types::Hash.schema(message: Types::String)
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)
51
50
 
52
- def call(user)
53
- if settings&.fail_befriend
54
- fail!(message: "Did not find a friend.")
55
- else
56
- { user: user, friend: User.new(name: "A friend", age: 42) }
57
- end
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)
58
56
  end
59
57
  end
58
+ end
60
59
 
61
- class Chain
62
- include Teckel::Chain
60
+ class Chain
61
+ include Teckel::Chain
63
62
 
64
- step :create, CreateUser
65
- step :log, LogUser
66
- step :befriend, AddFriend
67
- end
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
68
73
  end
69
74
 
70
75
  it 'Chain input points to first step input' do
@@ -85,8 +90,7 @@ RSpec.describe Teckel::Chain do
85
90
 
86
91
  context "success" do
87
92
  it "result matches" do
88
- result =
89
- TeckelChainTest::Chain.
93
+ result = TeckelChainTest::Chain.
90
94
  with(befriend: nil).
91
95
  call(name: "Bob", age: 23)
92
96
 
@@ -96,8 +100,7 @@ RSpec.describe Teckel::Chain do
96
100
 
97
101
  context "failure" do
98
102
  it "returns a Result for invalid input" do
99
- result =
100
- TeckelChainTest::Chain.
103
+ result = TeckelChainTest::Chain.
101
104
  with(befriend: :fail).
102
105
  call(name: "Bob", age: 0)
103
106
 
@@ -108,8 +111,7 @@ RSpec.describe Teckel::Chain do
108
111
  end
109
112
 
110
113
  it "returns a Result for failed step" do
111
- result =
112
- TeckelChainTest::Chain.
114
+ result = TeckelChainTest::Chain.
113
115
  with(befriend: :fail).
114
116
  call(name: "Bob", age: 23)
115
117
 
@@ -121,11 +123,6 @@ RSpec.describe Teckel::Chain do
121
123
  end
122
124
 
123
125
  describe "#finalize!" do
124
- let(:frozen_error) do
125
- # different ruby versions raise different errors
126
- defined?(FrozenError) ? FrozenError : RuntimeError
127
- end
128
-
129
126
  subject { TeckelChainTest::Chain.dup }
130
127
 
131
128
  it "freezes the Chain class and operation classes" do
@@ -177,4 +174,82 @@ RSpec.describe Teckel::Chain do
177
174
  expect(subject.call).to eq(:mocked)
178
175
  end
179
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
180
255
  end
@@ -5,3 +5,4 @@ require_relative 'support/fake_db'
5
5
  require_relative 'support/fake_models'
6
6
 
7
7
  require "teckel"
8
+ require "teckel/chain"