teckel 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,77 +4,77 @@ require 'support/dry_base'
4
4
  require 'support/fake_db'
5
5
  require 'support/fake_models'
6
6
 
7
- RSpec.describe Teckel::Chain do
8
- module TeckelChainAroundHookTest
9
- class CreateUser
10
- include ::Teckel::Operation
11
-
12
- result!
13
-
14
- input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
15
- output Types.Instance(User)
16
- error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
17
-
18
- def call(input)
19
- user = User.new(name: input[:name], age: input[:age])
20
- if user.save
21
- success!(user)
22
- else
23
- fail!(message: "Could not safe User", errors: user.errors)
24
- end
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)
25
23
  end
26
24
  end
25
+ end
27
26
 
28
- class AddFriend
29
- include ::Teckel::Operation
27
+ class AddFriend
28
+ include ::Teckel::Operation
30
29
 
31
- result!
30
+ result!
32
31
 
33
- settings Struct.new(:fail_befriend)
32
+ settings Struct.new(:fail_befriend)
34
33
 
35
- input Types.Instance(User)
36
- output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
37
- error Types::Hash.schema(message: Types::String)
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)
38
37
 
39
- def call(user)
40
- if settings&.fail_befriend
41
- fail!(message: "Did not find a friend.")
42
- else
43
- { user: user, friend: User.new(name: "A friend", age: 42) }
44
- end
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)
45
43
  end
46
44
  end
45
+ end
47
46
 
48
- @stack = []
49
- def self.stack
50
- @stack
51
- end
52
-
53
- class Chain
54
- include Teckel::Chain
47
+ @stack = []
48
+ def self.stack
49
+ @stack
50
+ end
55
51
 
56
- around ->(chain, input) {
57
- result = nil
58
- begin
59
- TeckelChainAroundHookTest.stack << :before
52
+ class Chain
53
+ include Teckel::Chain
60
54
 
61
- FakeDB.transaction do
62
- result = chain.call(input)
63
- raise FakeDB::Rollback if result.failure?
64
- end
55
+ around ->(chain, input) {
56
+ result = nil
57
+ begin
58
+ TeckelChainAroundHookTest.stack << :before
65
59
 
66
- TeckelChainAroundHookTest.stack << :after
67
- result
68
- rescue FakeDB::Rollback
69
- result
60
+ FakeDB.transaction do
61
+ result = chain.call(input)
62
+ raise FakeDB::Rollback if result.failure?
70
63
  end
71
- }
72
64
 
73
- step :create, CreateUser
74
- step :befriend, AddFriend
75
- end
65
+ TeckelChainAroundHookTest.stack << :after
66
+ result
67
+ rescue FakeDB::Rollback
68
+ result
69
+ end
70
+ }
71
+
72
+ step :create, CreateUser
73
+ step :befriend, AddFriend
76
74
  end
75
+ end
77
76
 
77
+ RSpec.describe Teckel::Chain do
78
78
  before { TeckelChainAroundHookTest.stack.clear }
79
79
 
80
80
  context "success" do
@@ -3,70 +3,70 @@
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
68
66
  end
67
+ end
69
68
 
69
+ RSpec.describe Teckel::Chain do
70
70
  it 'Chain input points to first step input' do
71
71
  expect(TeckelChainTest::Chain.input).to eq(TeckelChainTest::CreateUser.input)
72
72
  end
@@ -0,0 +1,116 @@
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
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.describe Teckel::Operation do
4
- context "default settings" do
5
- module TeckelOperationDefaultSettings
6
- class BaseOperation
7
- include ::Teckel::Operation
3
+ module TeckelOperationDefaultSettings
4
+ class BaseOperation
5
+ include ::Teckel::Operation
8
6
 
9
- input none
10
- output Symbol
11
- error none
7
+ input none
8
+ output Symbol
9
+ error none
12
10
 
13
- def call(_input)
14
- success! settings.injected
15
- end
16
- end
11
+ def call(_input)
12
+ success! settings.injected
17
13
  end
14
+ end
15
+ end
18
16
 
17
+ RSpec.describe Teckel::Operation do
18
+ context "default settings" do
19
19
  shared_examples "operation with default settings" do |operation|
20
20
  subject { operation }
21
21
 
@@ -1,57 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.describe Teckel::Operation do
4
- context "default settings via base class" do
5
- module TeckelOperationDefaultsViaBaseClass
6
- DefaultError = Struct.new(:message, :status_code)
7
- Settings = Struct.new(:fail_it)
8
-
9
- class ApplicationOperation
10
- include Teckel::Operation
3
+ module TeckelOperationDefaultsViaBaseClass
4
+ DefaultError = Struct.new(:message, :status_code)
5
+ Settings = Struct.new(:fail_it)
11
6
 
12
- settings Settings
13
- settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
7
+ class ApplicationOperation
8
+ include Teckel::Operation
14
9
 
15
- error DefaultError
16
- error_constructor ->(data) { error.new(*data.values_at(*error.members)) }
10
+ settings Settings
11
+ settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
17
12
 
18
- result!
13
+ error DefaultError
14
+ error_constructor ->(data) { error.new(*data.values_at(*error.members)) }
19
15
 
20
- # Freeze the base class to make sure it's inheritable configuration is not altered
21
- freeze
22
- end
16
+ result!
23
17
 
24
- class OperationA < ApplicationOperation
25
- input Struct.new(:input_data_a)
26
- output Struct.new(:output_data_a)
18
+ # Freeze the base class to make sure it's inheritable configuration is not altered
19
+ freeze
20
+ end
27
21
 
28
- def call(input)
29
- if settings&.fail_it
30
- fail!(message: settings.fail_it, status_code: 400)
31
- else
32
- input.input_data_a * 2
33
- end
34
- end
22
+ class OperationA < ApplicationOperation
23
+ input Struct.new(:input_data_a)
24
+ output Struct.new(:output_data_a)
35
25
 
36
- finalize!
26
+ def call(input)
27
+ if settings&.fail_it
28
+ fail!(message: settings.fail_it, status_code: 400)
29
+ else
30
+ success!(input.input_data_a * 2)
37
31
  end
32
+ end
38
33
 
39
- class OperationB < ApplicationOperation
40
- input Struct.new(:input_data_b)
41
- output Struct.new(:output_data_b)
34
+ finalize!
35
+ end
42
36
 
43
- def call(input)
44
- if settings&.fail_it
45
- fail!(message: settings.fail_it, status_code: 500)
46
- else
47
- input.input_data_b * 4
48
- end
49
- end
37
+ class OperationB < ApplicationOperation
38
+ input Struct.new(:input_data_b)
39
+ output Struct.new(:output_data_b)
50
40
 
51
- finalize!
41
+ def call(input)
42
+ if settings&.fail_it
43
+ fail!(message: settings.fail_it, status_code: 500)
44
+ else
45
+ success!(input.input_data_b * 4)
52
46
  end
53
47
  end
54
48
 
49
+ finalize!
50
+ end
51
+ end
52
+
53
+ RSpec.describe Teckel::Operation do
54
+ context "default settings via base class" do
55
55
  let(:operation_a) { TeckelOperationDefaultsViaBaseClass::OperationA }
56
56
  let(:operation_b) { TeckelOperationDefaultsViaBaseClass::OperationB }
57
57