bcdd-result 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,6 +6,9 @@ class BCDD::Result
6
6
  require_relative 'context/success'
7
7
  require_relative 'context/mixin'
8
8
  require_relative 'context/expectations'
9
+ require_relative 'context/callable_and_then'
10
+
11
+ EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
9
12
 
10
13
  def self.Success(type, **value)
11
14
  Success.new(type: type, value: value)
@@ -15,7 +18,7 @@ class BCDD::Result
15
18
  Failure.new(type: type, value: value)
16
19
  end
17
20
 
18
- def initialize(type:, value:, subject: nil, expectations: nil, terminal: nil)
21
+ def initialize(type:, value:, source: nil, expectations: nil, terminal: nil)
19
22
  value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
20
23
 
21
24
  @acc = {}
@@ -23,8 +26,16 @@ class BCDD::Result
23
26
  super
24
27
  end
25
28
 
26
- def and_then(method_name = nil, **context_data, &block)
27
- super(method_name, context_data, &block)
29
+ def and_then(method_name = nil, **injected_value, &block)
30
+ super(method_name, injected_value, &block)
31
+ end
32
+
33
+ def and_then!(source, **injected_value)
34
+ _call = injected_value.delete(:_call)
35
+
36
+ acc.merge!(injected_value)
37
+
38
+ super(source, injected_value, _call: _call)
28
39
  end
29
40
 
30
41
  protected
@@ -33,20 +44,23 @@ class BCDD::Result
33
44
 
34
45
  private
35
46
 
36
- SubjectMethodArity = ->(method) do
47
+ SourceMethodArity = ->(method) do
37
48
  return 0 if method.arity.zero?
38
- return 1 if method.parameters.map(&:first).all?(/\Akey/)
49
+
50
+ parameters = method.parameters.map(&:first)
51
+
52
+ return 1 if !parameters.empty? && parameters.all?(/\Akey/)
39
53
 
40
54
  -1
41
55
  end
42
56
 
43
- def call_and_then_subject_method!(method, context_data)
44
- acc.merge!(value.merge(context_data))
57
+ def call_and_then_source_method!(method, injected_value)
58
+ acc.merge!(value.merge(injected_value))
45
59
 
46
- case SubjectMethodArity[method]
47
- when 0 then subject.send(method.name)
48
- when 1 then subject.send(method.name, **acc)
49
- else raise Error::InvalidSubjectMethodArity.build(subject: subject, method: method, max_arity: 1)
60
+ case SourceMethodArity[method]
61
+ when 0 then source.send(method.name)
62
+ when 1 then source.send(method.name, **acc)
63
+ else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 1)
50
64
  end
51
65
  end
52
66
 
@@ -56,20 +70,24 @@ class BCDD::Result
56
70
  block.call(acc)
57
71
  end
58
72
 
73
+ def call_and_then_callable!(source, value:, injected_value:, method_name:)
74
+ acc.merge!(value.merge(injected_value))
75
+
76
+ CallableAndThen::Caller.call(source, value: acc, injected_value: injected_value, method_name: method_name)
77
+ end
78
+
59
79
  def ensure_result_object(result, origin:)
60
80
  raise_unexpected_outcome_error(result, origin) unless result.is_a?(Context)
61
81
 
62
- return result.tap { _1.acc.merge!(acc) } if result.subject.equal?(subject)
82
+ return result.tap { _1.acc.merge!(acc) } if result.source.equal?(source)
63
83
 
64
- raise Error::InvalidResultSubject.build(given_result: result, expected_subject: subject)
84
+ raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
65
85
  end
66
86
 
67
- EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
68
-
69
87
  def raise_unexpected_outcome_error(result, origin)
70
88
  raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
71
89
  end
72
90
 
73
- private_constant :SubjectMethodArity, :EXPECTED_OUTCOME
91
+ private_constant :SourceMethodArity
74
92
  end
75
93
  end
@@ -9,7 +9,7 @@ class BCDD::Result::Error < StandardError
9
9
  end
10
10
 
11
11
  class MissingTypeArgument < self
12
- def initialize(_arg = nil)
12
+ def initialize(_message = nil)
13
13
  super('A type (argument) is required to invoke the #on/#on_type method')
14
14
  end
15
15
  end
@@ -22,29 +22,38 @@ class BCDD::Result::Error < StandardError
22
22
  end
23
23
  end
24
24
 
25
- class InvalidResultSubject < self
26
- def self.build(given_result:, expected_subject:)
25
+ class InvalidResultSource < self
26
+ def self.build(given_result:, expected_source:)
27
27
  message =
28
- "You cannot call #and_then and return a result that does not belong to the subject!\n" \
29
- "Expected subject: #{expected_subject.inspect}\n" \
30
- "Given subject: #{given_result.send(:subject).inspect}\n" \
28
+ "You cannot call #and_then and return a result that does not belong to the same source!\n" \
29
+ "Expected source: #{expected_source.inspect}\n" \
30
+ "Given source: #{given_result.send(:source).inspect}\n" \
31
31
  "Given result: #{given_result.inspect}"
32
32
 
33
33
  new(message)
34
34
  end
35
35
  end
36
36
 
37
- class InvalidSubjectMethodArity < self
38
- def self.build(subject:, method:, max_arity:)
39
- new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0..#{max_arity}")
37
+ class InvalidSourceMethodArity < self
38
+ def self.build(source:, method:, max_arity:)
39
+ new("#{source.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0..#{max_arity}")
40
40
  end
41
41
  end
42
42
 
43
43
  class UnhandledTypes < self
44
44
  def self.build(types:)
45
- subject = types.size == 1 ? 'This was' : 'These were'
45
+ source = types.size == 1 ? 'This was' : 'These were'
46
46
 
47
- new("You must handle all cases. #{subject} not handled: #{types.map(&:inspect).join(', ')}")
47
+ new("You must handle all cases. #{source} not handled: #{types.map(&:inspect).join(', ')}")
48
+ end
49
+ end
50
+
51
+ class CallableAndThenDisabled < self
52
+ def initialize(_message = nil)
53
+ super(
54
+ 'You cannot use #and_then! as the feature is disabled. ' \
55
+ 'Please use BCDD::Result.config.feature.enable!(:and_then!) to enable it.'
56
+ )
48
57
  end
49
58
  end
50
59
  end
@@ -24,7 +24,7 @@ class BCDD::Result
24
24
 
25
25
  FACTORY = <<~RUBY
26
26
  private def _Result
27
- @_Result ||= Result.with(subject: self, terminal: %<terminal>s)
27
+ @_Result ||= Result.with(source: self, terminal: %<terminal>s)
28
28
  end
29
29
  RUBY
30
30
 
@@ -38,13 +38,13 @@ class BCDD::Result
38
38
  module Addons
39
39
  module Continue
40
40
  private def Continue(value)
41
- Success.new(type: :continued, value: value, subject: self)
41
+ Success.new(type: :continued, value: value, source: self)
42
42
  end
43
43
  end
44
44
 
45
45
  module Given
46
46
  private def Given(value)
47
- Success.new(type: :given, value: value, subject: self)
47
+ Success.new(type: :given, value: value, source: self)
48
48
  end
49
49
  end
50
50
 
@@ -38,10 +38,10 @@ class BCDD::Result
38
38
 
39
39
  private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
40
40
 
41
- def initialize(subject: nil, contract: nil, terminal: nil, **options)
41
+ def initialize(source: nil, contract: nil, terminal: nil, **options)
42
42
  @terminal = terminal
43
43
 
44
- @subject = subject
44
+ @source = source
45
45
 
46
46
  @contract = contract if contract.is_a?(Contract::Evaluator)
47
47
 
@@ -60,16 +60,16 @@ class BCDD::Result
60
60
  _ResultAs(Failure, type, value)
61
61
  end
62
62
 
63
- def with(subject:, terminal: nil)
64
- self.class.new(subject: subject, terminal: terminal, contract: contract)
63
+ def with(source:, terminal: nil)
64
+ self.class.new(source: source, terminal: terminal, contract: contract)
65
65
  end
66
66
 
67
67
  private
68
68
 
69
69
  def _ResultAs(kind_class, type, value)
70
- kind_class.new(type: type, value: value, subject: subject, expectations: contract, terminal: terminal)
70
+ kind_class.new(type: type, value: value, source: source, expectations: contract, terminal: terminal)
71
71
  end
72
72
 
73
- attr_reader :subject, :terminal, :contract
73
+ attr_reader :source, :terminal, :contract
74
74
  end
75
75
  end
@@ -21,7 +21,7 @@ class BCDD::Result
21
21
  end
22
22
 
23
23
  private def _ResultAs(kind_class, type, value, terminal: nil)
24
- kind_class.new(type: type, value: value, subject: self, terminal: terminal)
24
+ kind_class.new(type: type, value: value, source: self, terminal: terminal)
25
25
  end
26
26
  end
27
27
 
@@ -2,16 +2,26 @@
2
2
 
3
3
  module BCDD::Result::Transitions
4
4
  module Tracking::Disabled
5
- def self.start(name:, desc:); end
6
-
7
- def self.finish(result:); end
5
+ def self.exec(_name, _desc)
6
+ EnsureResult[yield]
7
+ end
8
8
 
9
9
  def self.reset!; end
10
10
 
11
11
  def self.record(result); end
12
12
 
13
- def self.record_and_then(_type, _data, _subject)
13
+ def self.record_and_then(_type, _data, _source)
14
14
  yield
15
15
  end
16
+
17
+ def self.reset_and_then!; end
18
+
19
+ class << self
20
+ private
21
+
22
+ def start(name, desc); end
23
+
24
+ def finish(result); end
25
+ end
16
26
  end
17
27
  end
@@ -6,26 +6,18 @@ module BCDD::Result::Transitions
6
6
 
7
7
  private :tree, :tree=, :records, :records=, :root_started_at, :root_started_at=
8
8
 
9
- def start(name:, desc:)
10
- name_and_desc = [name, desc]
11
-
12
- tree.frozen? ? root_start(name_and_desc) : tree.insert!(name_and_desc)
13
- end
14
-
15
- def finish(result:)
16
- node = tree.current
9
+ def exec(name, desc)
10
+ start(name, desc)
17
11
 
18
- tree.move_up!
12
+ transition_node = tree.current
19
13
 
20
- return unless node.root?
14
+ result = EnsureResult[yield]
21
15
 
22
- duration = (now_in_milliseconds - root_started_at)
16
+ tree.move_to_root! if transition_node.root?
23
17
 
24
- metadata = { duration: duration, tree_map: tree.nested_ids }
18
+ finish(result)
25
19
 
26
- result.send(:transitions=, version: Tracking::VERSION, records: records, metadata: metadata)
27
-
28
- reset!
20
+ result
29
21
  end
30
22
 
31
23
  def reset!
@@ -38,11 +30,11 @@ module BCDD::Result::Transitions
38
30
  track(result, time: ::Time.now.getutc)
39
31
  end
40
32
 
41
- def record_and_then(type_arg, arg, subject)
33
+ def record_and_then(type_arg, arg, source)
42
34
  type = type_arg.instance_of?(::Method) ? :method : type_arg
43
35
 
44
36
  unless tree.frozen?
45
- current_and_then = { type: type, arg: arg, subject: subject }
37
+ current_and_then = { type: type, arg: arg, source: source }
46
38
  current_and_then[:method_name] = type_arg.name if type == :method
47
39
 
48
40
  tree.current.value[1] = current_and_then
@@ -51,8 +43,36 @@ module BCDD::Result::Transitions
51
43
  yield
52
44
  end
53
45
 
46
+ def reset_and_then!
47
+ return if tree.frozen?
48
+
49
+ tree.current.value[1] = Tracking::EMPTY_HASH
50
+ end
51
+
54
52
  private
55
53
 
54
+ def start(name, desc)
55
+ name_and_desc = [name, desc]
56
+
57
+ tree.frozen? ? root_start(name_and_desc) : tree.insert!(name_and_desc)
58
+ end
59
+
60
+ def finish(result)
61
+ node = tree.current
62
+
63
+ tree.move_up!
64
+
65
+ return unless node.root?
66
+
67
+ duration = (now_in_milliseconds - root_started_at)
68
+
69
+ metadata = { duration: duration, tree_map: tree.nested_ids }
70
+
71
+ result.send(:transitions=, version: Tracking::VERSION, records: records, metadata: metadata)
72
+
73
+ reset!
74
+ end
75
+
56
76
  TreeNodeValueNormalizer = ->(id, (nam, des)) { [{ id: id, name: nam, desc: des }, Tracking::EMPTY_HASH] }
57
77
 
58
78
  def root_start(name_and_desc)
@@ -74,7 +94,7 @@ module BCDD::Result::Transitions
74
94
  end
75
95
 
76
96
  def now_in_milliseconds
77
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
97
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
78
98
  end
79
99
  end
80
100
  end
@@ -46,9 +46,9 @@ class BCDD::Result
46
46
  def initialize(value, normalizer: ->(_id, val) { val })
47
47
  @size = 0
48
48
 
49
- @root = Node.new(value, parent: nil, id: @size, normalizer: normalizer)
49
+ @root = Node.new(value, parent: nil, id: size, normalizer: normalizer)
50
50
 
51
- @current = @root
51
+ @current = root
52
52
  end
53
53
 
54
54
  def root_value
@@ -70,19 +70,23 @@ class BCDD::Result
70
70
  end
71
71
 
72
72
  def insert!(value)
73
- @current = insert(value)
73
+ move_to! insert(value)
74
+ end
75
+
76
+ def move_to!(node)
77
+ tap { @current = node }
74
78
  end
75
79
 
76
80
  def move_up!(level = 1)
77
- tap { level.times { @current = current.parent || root } }
81
+ tap { level.times { move_to!(current.parent || root) } }
78
82
  end
79
83
 
80
84
  def move_down!(level = 1, index: -1)
81
- tap { level.times { current.children[index].then { |child| @current = child if child } } }
85
+ tap { level.times { current.children[index].then { |child| move_to!(child) if child } } }
82
86
  end
83
87
 
84
88
  def move_to_root!
85
- tap { @current = root }
89
+ move_to!(root)
86
90
  end
87
91
 
88
92
  NestedIds = ->(node) { [node.id, node.children.map(&NestedIds)] }
@@ -7,21 +7,19 @@ class BCDD::Result
7
7
 
8
8
  THREAD_VAR_NAME = :bcdd_result_transitions_tracking
9
9
 
10
+ EnsureResult = ->(result) do
11
+ return result if result.is_a?(::BCDD::Result)
12
+
13
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: :transitions)
14
+ end
15
+
10
16
  def self.tracking
11
17
  Thread.current[THREAD_VAR_NAME] ||= Tracking.instance
12
18
  end
13
19
  end
14
20
 
15
- def self.transitions(name: nil, desc: nil)
16
- Transitions.tracking.start(name: name, desc: desc)
17
-
18
- result = yield
19
-
20
- result.is_a?(::BCDD::Result) or raise Error::UnexpectedOutcome.build(outcome: result, origin: :transitions)
21
-
22
- Transitions.tracking.finish(result: result)
23
-
24
- result
21
+ def self.transitions(name: nil, desc: nil, &block)
22
+ Transitions.tracking.exec(name, desc, &block)
25
23
  rescue ::Exception => e
26
24
  Transitions.tracking.reset!
27
25
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BCDD
4
4
  class Result
5
- VERSION = '0.11.0'
5
+ VERSION = '0.12.0'
6
6
  end
7
7
  end
data/lib/bcdd/result.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+
3
5
  require_relative 'result/version'
4
- require_relative 'result/transitions'
5
6
  require_relative 'result/error'
7
+ require_relative 'result/transitions'
8
+ require_relative 'result/callable_and_then'
6
9
  require_relative 'result/data'
7
10
  require_relative 'result/handler'
8
11
  require_relative 'result/failure'
@@ -16,9 +19,9 @@ require_relative 'result/config'
16
19
  class BCDD::Result
17
20
  attr_accessor :unknown, :transitions
18
21
 
19
- attr_reader :subject, :data, :type_checker, :terminal
22
+ attr_reader :source, :data, :type_checker, :terminal
20
23
 
21
- protected :subject
24
+ protected :source
22
25
 
23
26
  private :unknown, :unknown=, :type_checker, :transitions=
24
27
 
@@ -32,11 +35,11 @@ class BCDD::Result
32
35
  config.freeze
33
36
  end
34
37
 
35
- def initialize(type:, value:, subject: nil, expectations: nil, terminal: nil)
38
+ def initialize(type:, value:, source: nil, expectations: nil, terminal: nil)
36
39
  data = Data.new(kind, type, value)
37
40
 
38
41
  @type_checker = Contract.evaluate(data, expectations)
39
- @subject = subject
42
+ @source = source
40
43
  @terminal = terminal || kind == :failure
41
44
  @data = data
42
45
 
@@ -88,12 +91,20 @@ class BCDD::Result
88
91
  tap { yield(value, type) if unknown }
89
92
  end
90
93
 
91
- def and_then(method_name = nil, context = nil, &block)
94
+ def and_then(method_name = nil, injected_value = nil, &block)
92
95
  return self if terminal?
93
96
 
94
97
  method_name && block and raise ::ArgumentError, 'method_name and block are mutually exclusive'
95
98
 
96
- method_name ? call_and_then_subject_method(method_name, context) : call_and_then_block(block)
99
+ method_name ? call_and_then_source_method(method_name, injected_value) : call_and_then_block(block)
100
+ end
101
+
102
+ def and_then!(source, injected_value = nil, _call: nil)
103
+ raise Error::CallableAndThenDisabled unless Config.instance.feature.enabled?(:and_then!)
104
+
105
+ return self if terminal?
106
+
107
+ call_and_then_callable!(source, value: value, injected_value: injected_value, method_name: _call)
97
108
  end
98
109
 
99
110
  def handle
@@ -139,27 +150,27 @@ class BCDD::Result
139
150
  block.call(value, type)
140
151
  end
141
152
 
142
- def call_and_then_subject_method(method_name, context_data)
143
- method = subject.method(method_name)
153
+ def call_and_then_source_method(method_name, injected_value)
154
+ method = source.method(method_name)
144
155
 
145
- Transitions.tracking.record_and_then(method, context_data, subject) do
146
- result = call_and_then_subject_method!(method, context_data)
156
+ Transitions.tracking.record_and_then(method, injected_value, source) do
157
+ result = call_and_then_source_method!(method, injected_value)
147
158
 
148
159
  ensure_result_object(result, origin: :method)
149
160
  end
150
161
  end
151
162
 
152
- def call_and_then_subject_method!(method, context_data)
163
+ def call_and_then_source_method!(method, injected_value)
153
164
  case method.arity
154
- when 0 then subject.send(method.name)
155
- when 1 then subject.send(method.name, value)
156
- when 2 then subject.send(method.name, value, context_data)
157
- else raise Error::InvalidSubjectMethodArity.build(subject: subject, method: method, max_arity: 2)
165
+ when 0 then source.send(method.name)
166
+ when 1 then source.send(method.name, value)
167
+ when 2 then source.send(method.name, value, injected_value)
168
+ else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 2)
158
169
  end
159
170
  end
160
171
 
161
172
  def call_and_then_block(block)
162
- Transitions.tracking.record_and_then(:block, nil, subject) do
173
+ Transitions.tracking.record_and_then(:block, nil, source) do
163
174
  result = call_and_then_block!(block)
164
175
 
165
176
  ensure_result_object(result, origin: :block)
@@ -170,11 +181,15 @@ class BCDD::Result
170
181
  block.call(value)
171
182
  end
172
183
 
184
+ def call_and_then_callable!(source, value:, injected_value:, method_name:)
185
+ CallableAndThen::Caller.call(source, value: value, injected_value: injected_value, method_name: method_name)
186
+ end
187
+
173
188
  def ensure_result_object(result, origin:)
174
189
  raise Error::UnexpectedOutcome.build(outcome: result, origin: origin) unless result.is_a?(::BCDD::Result)
175
190
 
176
- return result if result.subject.equal?(subject)
191
+ return result if result.source.equal?(source)
177
192
 
178
- raise Error::InvalidResultSubject.build(given_result: result, expected_subject: subject)
193
+ raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
179
194
  end
180
195
  end
@@ -0,0 +1,60 @@
1
+ module BCDD::Result::CallableAndThen
2
+ class Config
3
+ attr_accessor default_method_name_to_call: Symbol
4
+
5
+ def initialize: -> void
6
+
7
+ def options: () -> Hash[Symbol, untyped]
8
+ end
9
+
10
+ class Error < BCDD::Result::Error
11
+ end
12
+
13
+ class Error::InvalidArity < Error
14
+ def self.build: (
15
+ source: untyped,
16
+ method: Symbol,
17
+ arity: String
18
+ ) -> Error::InvalidArity
19
+ end
20
+
21
+ class Caller
22
+ def self.call: (
23
+ untyped source,
24
+ value: untyped,
25
+ injected_value: untyped,
26
+ method_name: (Symbol | nil)
27
+ ) -> BCDD::Result
28
+
29
+ private
30
+
31
+ def self.call_proc!: (
32
+ untyped source,
33
+ untyped value,
34
+ untyped injected_value
35
+ ) -> BCDD::Result
36
+
37
+ def self.call_method!: (
38
+ untyped source,
39
+ Method method,
40
+ untyped value,
41
+ untyped injected_value
42
+ ) -> BCDD::Result
43
+
44
+ def self.callable_method: (
45
+ untyped source,
46
+ (Symbol | nil) method_name
47
+ ) -> ::Method
48
+
49
+ def self.ensure_result_object: (
50
+ untyped source,
51
+ untyped value,
52
+ BCDD::Result result
53
+ ) -> BCDD::Result
54
+
55
+ def self.expected_result_object: () -> singleton(BCDD::Result)
56
+
57
+ def self.expected_outcome: () -> String
58
+ end
59
+ end
60
+
@@ -14,6 +14,8 @@ class BCDD::Result::Config
14
14
 
15
15
  def initialize: -> void
16
16
 
17
+ def and_then!: () -> BCDD::Result::CallableAndThen::Config
18
+
17
19
  def freeze: -> BCDD::Result::Config
18
20
  def options: -> Hash[Symbol, BCDD::Result::Config::Switcher]
19
21
  def to_h: -> Hash[Symbol, Hash[Symbol | String, bool]]