bcdd-result 0.9.1 → 0.11.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/CHANGELOG.md +43 -16
  4. data/README.md +259 -40
  5. data/Rakefile +8 -2
  6. data/Steepfile +2 -2
  7. data/lib/bcdd/result/config/switchers/addons.rb +25 -0
  8. data/lib/bcdd/result/config/{constant_alias.rb → switchers/constant_aliases.rb} +4 -4
  9. data/lib/bcdd/result/config/switchers/features.rb +28 -0
  10. data/lib/bcdd/result/config/switchers/pattern_matching.rb +20 -0
  11. data/lib/bcdd/result/config.rb +8 -28
  12. data/lib/bcdd/result/context/expectations/mixin.rb +10 -2
  13. data/lib/bcdd/result/context/mixin.rb +13 -5
  14. data/lib/bcdd/result/context/success.rb +2 -2
  15. data/lib/bcdd/result/context.rb +10 -15
  16. data/lib/bcdd/result/expectations/mixin.rb +11 -5
  17. data/lib/bcdd/result/expectations.rb +6 -6
  18. data/lib/bcdd/result/mixin.rb +11 -5
  19. data/lib/bcdd/result/transitions/tracking/disabled.rb +17 -0
  20. data/lib/bcdd/result/transitions/tracking/enabled.rb +80 -0
  21. data/lib/bcdd/result/transitions/tracking.rb +20 -0
  22. data/lib/bcdd/result/transitions/tree.rb +95 -0
  23. data/lib/bcdd/result/transitions.rb +30 -0
  24. data/lib/bcdd/result/version.rb +1 -1
  25. data/lib/bcdd/result.rb +33 -22
  26. data/sig/bcdd/result/config.rbs +101 -0
  27. data/sig/bcdd/result/context.rbs +114 -0
  28. data/sig/bcdd/result/contract.rbs +119 -0
  29. data/sig/bcdd/result/data.rbs +16 -0
  30. data/sig/bcdd/result/error.rbs +31 -0
  31. data/sig/bcdd/result/expectations.rbs +71 -0
  32. data/sig/bcdd/result/handler.rbs +47 -0
  33. data/sig/bcdd/result/mixin.rbs +45 -0
  34. data/sig/bcdd/result/transitions.rbs +89 -0
  35. data/sig/bcdd/result/version.rbs +5 -0
  36. data/sig/bcdd/result.rbs +7 -519
  37. metadata +22 -4
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Config
5
+ module Features
6
+ OPTIONS = {
7
+ expectations: {
8
+ default: true,
9
+ affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
10
+ },
11
+ transitions: {
12
+ default: true,
13
+ affects: %w[BCDD::Result BCDD::Result::Context]
14
+ }
15
+ }.transform_values!(&:freeze).freeze
16
+
17
+ Listener = ->(option_name, _bool) do
18
+ Thread.current[Transitions::THREAD_VAR_NAME] = nil if option_name == :transitions
19
+ end
20
+
21
+ def self.switcher
22
+ Switcher.new(options: OPTIONS, listener: Listener)
23
+ end
24
+ end
25
+
26
+ private_constant :Features
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Config
5
+ module PatternMatching
6
+ OPTIONS = {
7
+ nil_as_valid_value_checking: {
8
+ default: false,
9
+ affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
10
+ }
11
+ }.transform_values!(&:freeze).freeze
12
+
13
+ def self.switcher
14
+ Switcher.new(options: OPTIONS)
15
+ end
16
+ end
17
+
18
+ private_constant :PatternMatching
19
+ end
20
+ end
@@ -4,40 +4,22 @@ require 'singleton'
4
4
 
5
5
  require_relative 'config/options'
6
6
  require_relative 'config/switcher'
7
- require_relative 'config/constant_alias'
7
+ require_relative 'config/switchers/addons'
8
+ require_relative 'config/switchers/constant_aliases'
9
+ require_relative 'config/switchers/features'
10
+ require_relative 'config/switchers/pattern_matching'
8
11
 
9
12
  class BCDD::Result
10
13
  class Config
11
14
  include Singleton
12
15
 
13
- ADDON = {
14
- continue: {
15
- default: false,
16
- affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
17
- }
18
- }.transform_values!(&:freeze).freeze
19
-
20
- FEATURE = {
21
- expectations: {
22
- default: true,
23
- affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
24
- }
25
- }.transform_values!(&:freeze).freeze
26
-
27
- PATTERN_MATCHING = {
28
- nil_as_valid_value_checking: {
29
- default: false,
30
- affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
31
- }
32
- }.transform_values!(&:freeze).freeze
33
-
34
16
  attr_reader :addon, :feature, :constant_alias, :pattern_matching
35
17
 
36
18
  def initialize
37
- @addon = Switcher.new(options: ADDON)
38
- @feature = Switcher.new(options: FEATURE)
39
- @constant_alias = ConstantAlias.switcher
40
- @pattern_matching = Switcher.new(options: PATTERN_MATCHING)
19
+ @addon = Addons.switcher
20
+ @feature = Features.switcher
21
+ @constant_alias = ConstantAliases.switcher
22
+ @pattern_matching = PatternMatching.switcher
41
23
  end
42
24
 
43
25
  def freeze
@@ -65,7 +47,5 @@ class BCDD::Result
65
47
  def inspect
66
48
  "#<#{self.class.name} options=#{options.keys.sort.inspect}>"
67
49
  end
68
-
69
- private_constant :ADDON, :FEATURE, :PATTERN_MATCHING
70
50
  end
71
51
  end
@@ -7,13 +7,21 @@ class BCDD::Result::Context
7
7
  Methods = BCDD::Result::Expectations::Mixin::Methods
8
8
 
9
9
  module Addons
10
- module Continuable
10
+ module Continue
11
11
  private def Continue(**value)
12
12
  Success.new(type: :continued, value: value, subject: self)
13
13
  end
14
14
  end
15
15
 
16
- OPTIONS = { continue: Continuable }.freeze
16
+ module Given
17
+ private def Given(*values)
18
+ value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
19
+
20
+ Success.new(type: :given, value: value, subject: self)
21
+ end
22
+ end
23
+
24
+ OPTIONS = { continue: Continue, given: Given }.freeze
17
25
 
18
26
  def self.options(config_flags)
19
27
  ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
@@ -13,15 +13,15 @@ class BCDD::Result::Context
13
13
  _ResultAs(Failure, type, value)
14
14
  end
15
15
 
16
- private def _ResultAs(kind_class, type, value, halted: nil)
17
- kind_class.new(type: type, value: value, subject: self, halted: halted)
16
+ private def _ResultAs(kind_class, type, value, terminal: nil)
17
+ kind_class.new(type: type, value: value, subject: self, terminal: terminal)
18
18
  end
19
19
  end
20
20
 
21
21
  module Addons
22
- module Continuable
22
+ module Continue
23
23
  def Success(type, **value)
24
- _ResultAs(Success, type, value, halted: true)
24
+ _ResultAs(Success, type, value, terminal: true)
25
25
  end
26
26
 
27
27
  private def Continue(**value)
@@ -29,7 +29,15 @@ class BCDD::Result::Context
29
29
  end
30
30
  end
31
31
 
32
- OPTIONS = { continue: Continuable }.freeze
32
+ module Given
33
+ private def Given(*values)
34
+ value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
35
+
36
+ _ResultAs(Success, :given, value)
37
+ end
38
+ end
39
+
40
+ OPTIONS = { continue: Continue, given: Given }.freeze
33
41
 
34
42
  def self.options(config_flags)
35
43
  ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
@@ -3,13 +3,13 @@
3
3
  class BCDD::Result::Context::Success < BCDD::Result::Context
4
4
  include ::BCDD::Result::Success::Methods
5
5
 
6
- def and_expose(type, keys, halted: true)
6
+ def and_expose(type, keys, terminal: true)
7
7
  unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
8
8
  raise ::ArgumentError, 'keys must be an Array of Symbols'
9
9
  end
10
10
 
11
11
  exposed_value = acc.merge(value).slice(*keys)
12
12
 
13
- self.class.new(type: type, value: exposed_value, subject: subject, halted: halted)
13
+ self.class.new(type: type, value: exposed_value, subject: subject, terminal: terminal)
14
14
  end
15
15
  end
@@ -15,7 +15,7 @@ class BCDD::Result
15
15
  Failure.new(type: type, value: value)
16
16
  end
17
17
 
18
- def initialize(type:, value:, subject: nil, expectations: nil, halted: nil)
18
+ def initialize(type:, value:, subject: nil, expectations: nil, terminal: nil)
19
19
  value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
20
20
 
21
21
  @acc = {}
@@ -40,25 +40,20 @@ class BCDD::Result
40
40
  -1
41
41
  end
42
42
 
43
- def call_and_then_subject_method(method_name, context)
44
- method = subject.method(method_name)
43
+ def call_and_then_subject_method!(method, context_data)
44
+ acc.merge!(value.merge(context_data))
45
45
 
46
- acc.merge!(value.merge(context))
47
-
48
- result =
49
- case SubjectMethodArity[method]
50
- when 0 then subject.send(method_name)
51
- when 1 then subject.send(method_name, **acc)
52
- else raise Error::InvalidSubjectMethodArity.build(subject: subject, method: method, max_arity: 1)
53
- end
54
-
55
- ensure_result_object(result, origin: :method)
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)
50
+ end
56
51
  end
57
52
 
58
- def call_and_then_block(block)
53
+ def call_and_then_block!(block)
59
54
  acc.merge!(value)
60
55
 
61
- call_and_then_block!(block, acc)
56
+ block.call(acc)
62
57
  end
63
58
 
64
59
  def ensure_result_object(result, origin:)
@@ -24,25 +24,31 @@ class BCDD::Result
24
24
 
25
25
  FACTORY = <<~RUBY
26
26
  private def _Result
27
- @_Result ||= Result.with(subject: self, halted: %<halted>s)
27
+ @_Result ||= Result.with(subject: self, terminal: %<terminal>s)
28
28
  end
29
29
  RUBY
30
30
 
31
31
  def self.to_eval(addons)
32
- halted = addons.key?(:continue) ? 'true' : 'nil'
32
+ terminal = addons.key?(:continue) ? 'true' : 'nil'
33
33
 
34
- "#{BASE}\n#{format(FACTORY, halted: halted)}"
34
+ "#{BASE}\n#{format(FACTORY, terminal: terminal)}"
35
35
  end
36
36
  end
37
37
 
38
38
  module Addons
39
- module Continuable
39
+ module Continue
40
40
  private def Continue(value)
41
41
  Success.new(type: :continued, value: value, subject: self)
42
42
  end
43
43
  end
44
44
 
45
- OPTIONS = { continue: Continuable }.freeze
45
+ module Given
46
+ private def Given(value)
47
+ Success.new(type: :given, value: value, subject: self)
48
+ end
49
+ end
50
+
51
+ OPTIONS = { continue: Continue, given: Given }.freeze
46
52
 
47
53
  def self.options(config_flags)
48
54
  Config::Options.addon(map: config_flags, from: OPTIONS)
@@ -38,8 +38,8 @@ 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, halted: nil, **options)
42
- @halted = halted
41
+ def initialize(subject: nil, contract: nil, terminal: nil, **options)
42
+ @terminal = terminal
43
43
 
44
44
  @subject = subject
45
45
 
@@ -60,16 +60,16 @@ class BCDD::Result
60
60
  _ResultAs(Failure, type, value)
61
61
  end
62
62
 
63
- def with(subject:, halted: nil)
64
- self.class.new(subject: subject, halted: halted, contract: contract)
63
+ def with(subject:, terminal: nil)
64
+ self.class.new(subject: subject, 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, halted: halted)
70
+ kind_class.new(type: type, value: value, subject: subject, expectations: contract, terminal: terminal)
71
71
  end
72
72
 
73
- attr_reader :subject, :halted, :contract
73
+ attr_reader :subject, :terminal, :contract
74
74
  end
75
75
  end
@@ -20,15 +20,15 @@ class BCDD::Result
20
20
  _ResultAs(Failure, type, value)
21
21
  end
22
22
 
23
- private def _ResultAs(kind_class, type, value, halted: nil)
24
- kind_class.new(type: type, value: value, subject: self, halted: halted)
23
+ private def _ResultAs(kind_class, type, value, terminal: nil)
24
+ kind_class.new(type: type, value: value, subject: self, terminal: terminal)
25
25
  end
26
26
  end
27
27
 
28
28
  module Addons
29
- module Continuable
29
+ module Continue
30
30
  def Success(type, value = nil)
31
- _ResultAs(Success, type, value, halted: true)
31
+ _ResultAs(Success, type, value, terminal: true)
32
32
  end
33
33
 
34
34
  private def Continue(value)
@@ -36,7 +36,13 @@ class BCDD::Result
36
36
  end
37
37
  end
38
38
 
39
- OPTIONS = { continue: Continuable }.freeze
39
+ module Given
40
+ private def Given(value)
41
+ _ResultAs(Success, :given, value)
42
+ end
43
+ end
44
+
45
+ OPTIONS = { continue: Continue, given: Given }.freeze
40
46
 
41
47
  def self.options(config_flags)
42
48
  Config::Options.addon(map: config_flags, from: OPTIONS)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::Transitions
4
+ module Tracking::Disabled
5
+ def self.start(name:, desc:); end
6
+
7
+ def self.finish(result:); end
8
+
9
+ def self.reset!; end
10
+
11
+ def self.record(result); end
12
+
13
+ def self.record_and_then(_type, _data, _subject)
14
+ yield
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::Transitions
4
+ class Tracking::Enabled
5
+ attr_accessor :tree, :records, :root_started_at
6
+
7
+ private :tree, :tree=, :records, :records=, :root_started_at, :root_started_at=
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
17
+
18
+ tree.move_up!
19
+
20
+ return unless node.root?
21
+
22
+ duration = (now_in_milliseconds - root_started_at)
23
+
24
+ metadata = { duration: duration, tree_map: tree.nested_ids }
25
+
26
+ result.send(:transitions=, version: Tracking::VERSION, records: records, metadata: metadata)
27
+
28
+ reset!
29
+ end
30
+
31
+ def reset!
32
+ self.tree = Tracking::EMPTY_TREE
33
+ end
34
+
35
+ def record(result)
36
+ return if tree.frozen?
37
+
38
+ track(result, time: ::Time.now.getutc)
39
+ end
40
+
41
+ def record_and_then(type_arg, arg, subject)
42
+ type = type_arg.instance_of?(::Method) ? :method : type_arg
43
+
44
+ unless tree.frozen?
45
+ current_and_then = { type: type, arg: arg, subject: subject }
46
+ current_and_then[:method_name] = type_arg.name if type == :method
47
+
48
+ tree.current.value[1] = current_and_then
49
+ end
50
+
51
+ yield
52
+ end
53
+
54
+ private
55
+
56
+ TreeNodeValueNormalizer = ->(id, (nam, des)) { [{ id: id, name: nam, desc: des }, Tracking::EMPTY_HASH] }
57
+
58
+ def root_start(name_and_desc)
59
+ self.root_started_at = now_in_milliseconds
60
+
61
+ self.records = []
62
+
63
+ self.tree = Tree.new(name_and_desc, normalizer: TreeNodeValueNormalizer)
64
+ end
65
+
66
+ def track(result, time:)
67
+ result = result.data.to_h
68
+
69
+ root, = tree.root_value
70
+ parent, = tree.parent_value
71
+ current, and_then = tree.current_value
72
+
73
+ records << { root: root, parent: parent, current: current, result: result, and_then: and_then, time: time }
74
+ end
75
+
76
+ def now_in_milliseconds
77
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module Transitions
5
+ module Tracking
6
+ require_relative 'tracking/enabled'
7
+ require_relative 'tracking/disabled'
8
+
9
+ EMPTY_ARRAY = [].freeze
10
+ EMPTY_HASH = {}.freeze
11
+ EMPTY_TREE = Tree.new(nil).freeze
12
+ VERSION = 1
13
+ EMPTY = { version: VERSION, records: EMPTY_ARRAY, metadata: { duration: 0, tree_map: EMPTY_ARRAY } }.freeze
14
+
15
+ def self.instance
16
+ Config.instance.feature.enabled?(:transitions) ? Tracking::Enabled.new : Tracking::Disabled
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module Transitions
5
+ class Tree
6
+ class Node
7
+ attr_reader :id, :value, :parent, :normalizer, :children
8
+
9
+ def initialize(value, parent:, id:, normalizer:)
10
+ @normalizer = normalizer
11
+
12
+ @id = id
13
+ @value = normalizer.call(id, value)
14
+ @parent = parent
15
+
16
+ @children = []
17
+ end
18
+
19
+ def insert(value, id:)
20
+ node = self.class.new(value, parent: self, id: id, normalizer: normalizer)
21
+
22
+ @children << node
23
+
24
+ node
25
+ end
26
+
27
+ def root?
28
+ parent.nil?
29
+ end
30
+
31
+ def leaf?
32
+ children.empty?
33
+ end
34
+
35
+ def node?
36
+ !leaf?
37
+ end
38
+
39
+ def inspect
40
+ "#<#{self.class.name} id=#{id} children.size=#{children.size}>"
41
+ end
42
+ end
43
+
44
+ attr_reader :size, :root, :current
45
+
46
+ def initialize(value, normalizer: ->(_id, val) { val })
47
+ @size = 0
48
+
49
+ @root = Node.new(value, parent: nil, id: @size, normalizer: normalizer)
50
+
51
+ @current = @root
52
+ end
53
+
54
+ def root_value
55
+ root.value
56
+ end
57
+
58
+ def parent_value
59
+ current.parent&.value || root_value
60
+ end
61
+
62
+ def current_value
63
+ current.value
64
+ end
65
+
66
+ def insert(value)
67
+ @size += 1
68
+
69
+ current.insert(value, id: size)
70
+ end
71
+
72
+ def insert!(value)
73
+ @current = insert(value)
74
+ end
75
+
76
+ def move_up!(level = 1)
77
+ tap { level.times { @current = current.parent || root } }
78
+ end
79
+
80
+ def move_down!(level = 1, index: -1)
81
+ tap { level.times { current.children[index].then { |child| @current = child if child } } }
82
+ end
83
+
84
+ def move_to_root!
85
+ tap { @current = root }
86
+ end
87
+
88
+ NestedIds = ->(node) { [node.id, node.children.map(&NestedIds)] }
89
+
90
+ def nested_ids
91
+ NestedIds[root]
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module Transitions
5
+ require_relative 'transitions/tree'
6
+ require_relative 'transitions/tracking'
7
+
8
+ THREAD_VAR_NAME = :bcdd_result_transitions_tracking
9
+
10
+ def self.tracking
11
+ Thread.current[THREAD_VAR_NAME] ||= Tracking.instance
12
+ end
13
+ end
14
+
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
25
+ rescue ::Exception => e
26
+ Transitions.tracking.reset!
27
+
28
+ raise e
29
+ end
30
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BCDD
4
4
  class Result
5
- VERSION = '0.9.1'
5
+ VERSION = '0.11.0'
6
6
  end
7
7
  end