bcdd-result 0.6.0 → 0.8.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +31 -11
  3. data/CHANGELOG.md +148 -0
  4. data/README.md +849 -242
  5. data/Rakefile +9 -3
  6. data/Steepfile +1 -1
  7. data/lib/bcdd/result/config/constant_alias.rb +33 -0
  8. data/lib/bcdd/result/config/options.rb +26 -0
  9. data/lib/bcdd/result/config/switcher.rb +82 -0
  10. data/lib/bcdd/result/config.rb +71 -0
  11. data/lib/bcdd/result/context/expectations/mixin.rb +23 -0
  12. data/lib/bcdd/result/context/expectations.rb +25 -0
  13. data/lib/bcdd/result/context/failure.rb +9 -0
  14. data/lib/bcdd/result/context/mixin.rb +41 -0
  15. data/lib/bcdd/result/context/success.rb +15 -0
  16. data/lib/bcdd/result/context.rb +74 -0
  17. data/lib/bcdd/result/{expectations/contract → contract}/disabled.rb +2 -2
  18. data/lib/bcdd/result/{expectations → contract}/error.rb +5 -3
  19. data/lib/bcdd/result/{expectations/contract → contract}/evaluator.rb +2 -2
  20. data/lib/bcdd/result/{expectations/contract → contract}/for_types.rb +2 -2
  21. data/lib/bcdd/result/contract/for_types_and_values.rb +44 -0
  22. data/lib/bcdd/result/contract/interface.rb +21 -0
  23. data/lib/bcdd/result/{expectations → contract}/type_checker.rb +1 -1
  24. data/lib/bcdd/result/contract.rb +33 -0
  25. data/lib/bcdd/result/error.rb +7 -9
  26. data/lib/bcdd/result/expectations/mixin.rb +19 -12
  27. data/lib/bcdd/result/expectations.rb +51 -36
  28. data/lib/bcdd/result/failure/methods.rb +21 -0
  29. data/lib/bcdd/result/failure.rb +2 -16
  30. data/lib/bcdd/result/mixin.rb +26 -8
  31. data/lib/bcdd/result/success/methods.rb +21 -0
  32. data/lib/bcdd/result/success.rb +2 -16
  33. data/lib/bcdd/result/version.rb +1 -1
  34. data/lib/bcdd/result.rb +17 -4
  35. data/lib/bcdd-result.rb +3 -0
  36. data/sig/bcdd/result.rbs +340 -88
  37. metadata +27 -16
  38. data/lib/bcdd/result/expectations/contract/for_types_and_values.rb +0 -37
  39. data/lib/bcdd/result/expectations/contract/interface.rb +0 -21
  40. data/lib/bcdd/result/expectations/contract.rb +0 -25
  41. data/lib/result.rb +0 -5
data/Rakefile CHANGED
@@ -3,10 +3,16 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rake/testtask'
5
5
 
6
+ Rake::TestTask.new(:test_configuration) do |t|
7
+ t.libs += %w[lib test]
8
+
9
+ t.test_files = FileList.new('test/**/configuration_test.rb')
10
+ end
11
+
6
12
  Rake::TestTask.new(:test) do |t|
7
- t.libs << 'test'
8
- t.libs << 'lib'
9
- t.test_files = FileList['test/**/*_test.rb']
13
+ t.libs += %w[lib test]
14
+
15
+ t.test_files = FileList.new('test/**/*_test.rb')
10
16
  end
11
17
 
12
18
  require 'rubocop/rake_task'
data/Steepfile CHANGED
@@ -10,7 +10,7 @@ target :lib do
10
10
  # check 'app/models/**/*.rb' # Glob
11
11
  # ignore 'lib/templates/*.rb'
12
12
 
13
- # library 'pathname' # Standard libraries
13
+ library 'singleton' # Standard libraries
14
14
  # library 'strong_json' # Gems
15
15
 
16
16
  # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Config
5
+ module ConstantAlias
6
+ RESULT = 'Result'
7
+
8
+ OPTIONS = {
9
+ RESULT => { default: false, affects: %w[Object] }
10
+ }.transform_values!(&:freeze).freeze
11
+
12
+ MAPPING = {
13
+ RESULT => { target: ::Object, name: :Result, value: ::BCDD::Result }
14
+ }.transform_values!(&:freeze).freeze
15
+
16
+ Listener = ->(option_name, boolean) do
17
+ mapping = MAPPING.fetch(option_name)
18
+
19
+ target, name, value = mapping.fetch_values(:target, :name, :value)
20
+
21
+ defined = target.const_defined?(name, false)
22
+
23
+ boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name)
24
+ end
25
+
26
+ def self.switcher
27
+ Switcher.new(options: OPTIONS, listener: Listener)
28
+ end
29
+ end
30
+
31
+ private_constant :ConstantAlias
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Config
5
+ module Options
6
+ def self.with_defaults(all_flags, config)
7
+ all_flags ||= {}
8
+
9
+ default_flags = Config.instance.to_h.fetch(config)
10
+
11
+ config_flags = all_flags.fetch(config, {})
12
+
13
+ default_flags.merge(config_flags).slice(*default_flags.keys)
14
+ end
15
+
16
+ def self.filter_map(all_flags, config:, from:)
17
+ with_defaults(all_flags, config)
18
+ .filter_map { |name, truthy| from[name] if truthy }
19
+ end
20
+
21
+ def self.addon(map:, from:)
22
+ filter_map(map, config: :addon, from: from)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Config
5
+ class Switcher
6
+ attr_reader :_options, :_affects, :listener
7
+
8
+ private :_options, :_affects, :listener
9
+
10
+ def initialize(options:, listener: nil)
11
+ @_options = options.transform_values { _1.fetch(:default) }
12
+ @_affects = options.transform_values { _1.fetch(:affects) }
13
+ @listener = listener
14
+ end
15
+
16
+ def inspect
17
+ "#<#{self.class.name} options=#{_options.inspect}>"
18
+ end
19
+
20
+ def freeze
21
+ _options.freeze
22
+ super
23
+ end
24
+
25
+ def to_h
26
+ _options.dup
27
+ end
28
+
29
+ def options
30
+ _affects.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] }
31
+ end
32
+
33
+ def enabled?(name)
34
+ _options[name] || false
35
+ end
36
+
37
+ def enable!(*names)
38
+ set_many(names, to: true)
39
+ end
40
+
41
+ def disable!(*names)
42
+ set_many(names, to: false)
43
+ end
44
+
45
+ private
46
+
47
+ def set_many(names, to:)
48
+ require_option!(names)
49
+
50
+ names.each do |name|
51
+ set_one(name, to)
52
+
53
+ listener&.call(name, to)
54
+ end
55
+
56
+ options.slice(*names)
57
+ end
58
+
59
+ def set_one(name, boolean)
60
+ validate_option!(name)
61
+
62
+ _options[name] = boolean
63
+ end
64
+
65
+ def require_option!(names)
66
+ raise ::ArgumentError, "One or more options required. #{available_options_message}" if names.empty?
67
+ end
68
+
69
+ def validate_option!(name)
70
+ return if _options.key?(name)
71
+
72
+ raise ::ArgumentError, "Invalid option: #{name.inspect}. #{available_options_message}"
73
+ end
74
+
75
+ def available_options_message
76
+ "Available options: #{_options.keys.map(&:inspect).join(', ')}"
77
+ end
78
+ end
79
+
80
+ private_constant :Switcher
81
+ end
82
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ require_relative 'config/options'
6
+ require_relative 'config/switcher'
7
+ require_relative 'config/constant_alias'
8
+
9
+ class BCDD::Result
10
+ class Config
11
+ include Singleton
12
+
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
+ attr_reader :addon, :feature, :constant_alias, :pattern_matching
35
+
36
+ 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)
41
+ end
42
+
43
+ def freeze
44
+ addon.freeze
45
+ feature.freeze
46
+ constant_alias.freeze
47
+ pattern_matching.freeze
48
+
49
+ super
50
+ end
51
+
52
+ def options
53
+ {
54
+ addon: addon,
55
+ feature: feature,
56
+ constant_alias: constant_alias,
57
+ pattern_matching: pattern_matching
58
+ }
59
+ end
60
+
61
+ def to_h
62
+ options.transform_values(&:to_h)
63
+ end
64
+
65
+ def inspect
66
+ "#<#{self.class.name} options=#{options.keys.sort.inspect}>"
67
+ end
68
+
69
+ private_constant :ADDON, :FEATURE, :PATTERN_MATCHING
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context
4
+ module Expectations::Mixin
5
+ Factory = BCDD::Result::Expectations::Mixin::Factory
6
+
7
+ METHODS = BCDD::Result::Expectations::Mixin::METHODS
8
+
9
+ module Addons
10
+ module Continuable
11
+ private def Continue(**value)
12
+ Success.new(type: :continued, value: value, subject: self)
13
+ end
14
+ end
15
+
16
+ OPTIONS = { continue: Continuable }.freeze
17
+
18
+ def self.options(config_flags)
19
+ ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context
4
+ class Expectations < BCDD::Result::Expectations
5
+ require_relative 'expectations/mixin'
6
+
7
+ def self.mixin_module
8
+ Mixin
9
+ end
10
+
11
+ def self.result_factory_without_expectations
12
+ ::BCDD::Result::Context
13
+ end
14
+
15
+ private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
16
+
17
+ def Success(type, **value)
18
+ Success.new(type: type, value: value, subject: subject, expectations: contract)
19
+ end
20
+
21
+ def Failure(type, **value)
22
+ Failure.new(type: type, value: value, subject: subject, expectations: contract)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context::Failure < BCDD::Result::Context
4
+ include BCDD::Result::Failure::Methods
5
+
6
+ def and_expose(_type, _keys)
7
+ self
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context
4
+ module Mixin
5
+ Factory = BCDD::Result::Mixin::Factory
6
+
7
+ module Methods
8
+ def Success(type, **value)
9
+ Success.new(type: type, value: value, subject: self)
10
+ end
11
+
12
+ def Failure(type, **value)
13
+ Failure.new(type: type, value: value, subject: self)
14
+ end
15
+ end
16
+
17
+ module Addons
18
+ module Continuable
19
+ private def Continue(**value)
20
+ Success.new(type: :continued, value: value, subject: self)
21
+ end
22
+ end
23
+
24
+ OPTIONS = { continue: Continuable }.freeze
25
+
26
+ def self.options(config_flags)
27
+ ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.mixin_module
33
+ Mixin
34
+ end
35
+
36
+ def self.result_factory
37
+ ::BCDD::Result::Context
38
+ end
39
+
40
+ private_class_method :mixin_module, :result_factory
41
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context::Success < BCDD::Result::Context
4
+ include ::BCDD::Result::Success::Methods
5
+
6
+ def and_expose(type, keys)
7
+ unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
8
+ raise ::ArgumentError, 'keys must be an Array of Symbols'
9
+ end
10
+
11
+ exposed_value = acc.merge(value).slice(*keys)
12
+
13
+ self.class.new(type: type, value: exposed_value, subject: subject)
14
+ end
15
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Context < self
5
+ require_relative 'context/failure'
6
+ require_relative 'context/success'
7
+ require_relative 'context/mixin'
8
+ require_relative 'context/expectations'
9
+
10
+ def self.Success(type, **value)
11
+ Success.new(type: type, value: value)
12
+ end
13
+
14
+ def self.Failure(type, **value)
15
+ Failure.new(type: type, value: value)
16
+ end
17
+
18
+ def initialize(type:, value:, subject: nil, expectations: nil)
19
+ value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
20
+
21
+ @acc = {}
22
+
23
+ super
24
+ end
25
+
26
+ def and_then(method_name = nil, **context_data, &block)
27
+ super(method_name, context_data, &block)
28
+ end
29
+
30
+ protected
31
+
32
+ attr_reader :acc
33
+
34
+ private
35
+
36
+ SubjectMethodArity = ->(method) do
37
+ return 0 if method.arity.zero?
38
+ return 1 if method.parameters.map(&:first).all?(/\Akey/)
39
+
40
+ -1
41
+ end
42
+
43
+ def call_subject_method(method_name, context)
44
+ method = subject.method(method_name)
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)
56
+ end
57
+
58
+ def ensure_result_object(result, origin:)
59
+ raise_unexpected_outcome_error(result, origin) unless result.is_a?(Context)
60
+
61
+ return result.tap { _1.acc.merge!(acc) } if result.subject.equal?(subject)
62
+
63
+ raise Error::InvalidResultSubject.build(given_result: result, expected_subject: subject)
64
+ end
65
+
66
+ EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
67
+
68
+ def raise_unexpected_outcome_error(result, origin)
69
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
70
+ end
71
+
72
+ private_constant :SubjectMethodArity, :EXPECTED_OUTCOME
73
+ end
74
+ end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  module Contract::Disabled
5
5
  extend Contract::Interface
6
6
 
7
- EMPTY_SET = Set.new.freeze
7
+ EMPTY_SET = ::Set.new.freeze
8
8
 
9
9
  def self.allowed_types
10
10
  EMPTY_SET
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations::Error < BCDD::Result::Error
3
+ class BCDD::Result::Contract::Error < BCDD::Result::Error
4
4
  class UnexpectedType < self
5
5
  def self.build(type:, allowed_types:)
6
6
  new("type :#{type} is not allowed. Allowed types: #{allowed_types.map(&:inspect).join(', ')}")
@@ -8,8 +8,10 @@ class BCDD::Result::Expectations::Error < BCDD::Result::Error
8
8
  end
9
9
 
10
10
  class UnexpectedValue < self
11
- def self.build(type:, value:)
12
- new("value #{value.inspect} is not allowed for :#{type} type")
11
+ def self.build(type:, value:, cause: nil)
12
+ cause_message = " (#{cause.message})" if cause
13
+
14
+ new("value #{value.inspect} is not allowed for :#{type} type#{cause_message}")
13
15
  end
14
16
  end
15
17
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  class Contract::Evaluator
5
5
  include Contract::Interface
6
6
 
@@ -25,7 +25,7 @@ class BCDD::Result::Expectations
25
25
  def type!(type)
26
26
  return type if type?(type)
27
27
 
28
- raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
28
+ raise Contract::Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
29
29
  end
30
30
 
31
31
  def type_and_value!(data)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  class Contract::ForTypes
5
5
  include Contract::Interface
6
6
 
@@ -17,7 +17,7 @@ class BCDD::Result::Expectations
17
17
  def type!(type)
18
18
  return type if type?(type)
19
19
 
20
- raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
20
+ raise Contract::Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
21
21
  end
22
22
 
23
23
  def type_and_value!(data)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Contract::ForTypesAndValues
5
+ include Contract::Interface
6
+
7
+ def initialize(types_and_values, config)
8
+ @nil_as_valid_value_checking =
9
+ Config::Options
10
+ .with_defaults(config, :pattern_matching)
11
+ .fetch(:nil_as_valid_value_checking)
12
+
13
+ @types_and_values = types_and_values.transform_keys(&:to_sym)
14
+
15
+ @types_contract = Contract::ForTypes.new(@types_and_values.keys)
16
+ end
17
+
18
+ def allowed_types
19
+ @types_contract.allowed_types
20
+ end
21
+
22
+ def type?(type)
23
+ @types_contract.type?(type)
24
+ end
25
+
26
+ def type!(type)
27
+ @types_contract.type!(type)
28
+ end
29
+
30
+ def type_and_value!(data)
31
+ type, value = data.type, data.value
32
+
33
+ value_checking = @types_and_values[type!(type)]
34
+
35
+ checking_result = value_checking === value
36
+
37
+ return value if checking_result || (checking_result.nil? && @nil_as_valid_value_checking)
38
+
39
+ raise Contract::Error::UnexpectedValue.build(type: type, value: value)
40
+ rescue ::NoMatchingPatternError => e
41
+ raise Contract::Error::UnexpectedValue.build(type: data.type, value: data.value, cause: e)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module Contract::Interface
5
+ def allowed_types
6
+ raise Error::NotImplemented
7
+ end
8
+
9
+ def type?(_type)
10
+ raise Error::NotImplemented
11
+ end
12
+
13
+ def type!(_type)
14
+ raise Error::NotImplemented
15
+ end
16
+
17
+ def type_and_value!(_data)
18
+ raise Error::NotImplemented
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ module BCDD::Result::Contract
4
4
  class TypeChecker
5
5
  attr_reader :result_type, :expectations
6
6
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::Contract
4
+ require_relative 'contract/error'
5
+ require_relative 'contract/type_checker'
6
+ require_relative 'contract/interface'
7
+ require_relative 'contract/evaluator'
8
+ require_relative 'contract/disabled'
9
+ require_relative 'contract/for_types'
10
+ require_relative 'contract/for_types_and_values'
11
+
12
+ NONE = Evaluator.new(Disabled, Disabled).freeze
13
+
14
+ def self.evaluate(data, contract)
15
+ contract ||= NONE
16
+
17
+ contract.type_and_value!(data)
18
+
19
+ TypeChecker.new(data.type, expectations: contract)
20
+ end
21
+
22
+ ToEnsure = ->(spec, config) do
23
+ return Disabled if spec.nil?
24
+
25
+ spec.is_a?(::Hash) ? ForTypesAndValues.new(spec, config) : ForTypes.new(Array(spec))
26
+ end
27
+
28
+ def self.new(success:, failure:, config:)
29
+ Evaluator.new(ToEnsure[success, config], ToEnsure[failure, config])
30
+ end
31
+
32
+ private_constant :ToEnsure
33
+ end
@@ -15,16 +15,14 @@ class BCDD::Result::Error < StandardError
15
15
  end
16
16
 
17
17
  class UnexpectedOutcome < self
18
- def self.build(outcome:, origin:)
19
- message =
20
- "Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by " \
21
- 'BCDD::Result::Success or BCDD::Result::Failure'
18
+ def self.build(outcome:, origin:, expected: nil)
19
+ expected ||= 'BCDD::Result::Success or BCDD::Result::Failure'
22
20
 
23
- new(message)
21
+ new("Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by #{expected}")
24
22
  end
25
23
  end
26
24
 
27
- class WrongResultSubject < self
25
+ class InvalidResultSubject < self
28
26
  def self.build(given_result:, expected_subject:)
29
27
  message =
30
28
  "You cannot call #and_then and return a result that does not belong to the subject!\n" \
@@ -36,9 +34,9 @@ class BCDD::Result::Error < StandardError
36
34
  end
37
35
  end
38
36
 
39
- class WrongSubjectMethodArity < self
40
- def self.build(subject:, method:)
41
- new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0, 1 or 2.")
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}")
42
40
  end
43
41
  end
44
42