teckel 0.4.0 → 0.8.0

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