teckel 0.5.0 → 0.6.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.
@@ -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