bcdd-result 0.9.1 → 0.11.0

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