bcdd-result 0.7.0 → 0.9.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.
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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Config
5
+ module ConstantAlias
6
+ MAPPING = {
7
+ 'Result' => { target: ::Object, name: :Result, value: ::BCDD::Result },
8
+ 'Context' => { target: ::Object, name: :Context, value: ::BCDD::Result::Context },
9
+ 'BCDD::Context' => { target: ::BCDD, name: :Context, value: ::BCDD::Result::Context }
10
+ }.transform_values!(&:freeze).freeze
11
+
12
+ OPTIONS = MAPPING.to_h do |option_name, mapping|
13
+ affects = mapping.fetch(:target).name.freeze
14
+
15
+ [option_name, { default: false, affects: [affects].freeze }]
16
+ end.freeze
17
+
18
+ Listener = ->(option_name, boolean) do
19
+ mapping = MAPPING.fetch(option_name)
20
+
21
+ target, name, value = mapping.fetch_values(:target, :name, :value)
22
+
23
+ defined = target.const_defined?(name, false)
24
+
25
+ boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name)
26
+ end
27
+
28
+ def self.switcher
29
+ Switcher.new(options: OPTIONS, listener: Listener)
30
+ end
31
+ end
32
+
33
+ private_constant :ConstantAlias
34
+ end
35
+ end
@@ -0,0 +1,27 @@
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.select(all_flags, config:, from:)
17
+ with_defaults(all_flags, config)
18
+ .filter_map { |name, truthy| [name, from[name]] if truthy }
19
+ .to_h
20
+ end
21
+
22
+ def self.addon(map:, from:)
23
+ select(map, config: :addon, from: from)
24
+ end
25
+ end
26
+ end
27
+ 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
@@ -2,21 +2,9 @@
2
2
 
3
3
  class BCDD::Result::Context
4
4
  module Expectations::Mixin
5
- METHODS = <<~RUBY
6
- def Success(...)
7
- _Result::Success(...)
8
- end
9
-
10
- def Failure(...)
11
- _Result::Failure(...)
12
- end
5
+ Factory = BCDD::Result::Expectations::Mixin::Factory
13
6
 
14
- private
15
-
16
- def _Result
17
- @_Result ||= Result.with(subject: self)
18
- end
19
- RUBY
7
+ Methods = BCDD::Result::Expectations::Mixin::Methods
20
8
 
21
9
  module Addons
22
10
  module Continuable
@@ -25,10 +13,10 @@ class BCDD::Result::Context
25
13
  end
26
14
  end
27
15
 
28
- OPTIONS = { Continue: Continuable }.freeze
16
+ OPTIONS = { continue: Continuable }.freeze
29
17
 
30
- def self.options(names)
31
- Array(names).filter_map { |name| OPTIONS[name] }
18
+ def self.options(config_flags)
19
+ ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
32
20
  end
33
21
  end
34
22
  end
@@ -1,41 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class BCDD::Result::Context
4
- class Expectations
4
+ class Expectations < BCDD::Result::Expectations
5
5
  require_relative 'expectations/mixin'
6
6
 
7
- def self.mixin(success: nil, failure: nil, with: nil)
8
- addons = Mixin::Addons.options(with)
9
-
10
- mod = ::BCDD::Result::Expectations::Mixin.module!
11
- mod.const_set(:Result, new(success: success, failure: failure).freeze)
12
- mod.module_eval(Mixin::METHODS)
13
- mod.send(:include, *addons) unless addons.empty?
14
- mod
7
+ def self.mixin_module
8
+ Mixin
15
9
  end
16
10
 
17
- def initialize(subject: nil, success: nil, failure: nil, contract: nil)
18
- @subject = subject
19
-
20
- @contract = contract if contract.is_a?(::BCDD::Result::Contract::Evaluator)
21
-
22
- @contract ||= ::BCDD::Result::Contract.new(success: success, failure: failure).freeze
11
+ def self.result_factory_without_expectations
12
+ ::BCDD::Result::Context
23
13
  end
24
14
 
15
+ private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
16
+
25
17
  def Success(type, **value)
26
- Success.new(type: type, value: value, subject: subject, expectations: contract)
18
+ _ResultAs(Success, type, value)
27
19
  end
28
20
 
29
21
  def Failure(type, **value)
30
- Failure.new(type: type, value: value, subject: subject, expectations: contract)
22
+ _ResultAs(Failure, type, value)
31
23
  end
32
-
33
- def with(subject:)
34
- self.class.new(subject: subject, contract: contract)
35
- end
36
-
37
- private
38
-
39
- attr_reader :subject, :contract
40
24
  end
41
25
  end
@@ -2,37 +2,48 @@
2
2
 
3
3
  class BCDD::Result::Context
4
4
  module Mixin
5
+ Factory = BCDD::Result::Mixin::Factory
6
+
5
7
  module Methods
6
8
  def Success(type, **value)
7
- Success.new(type: type, value: value, subject: self)
9
+ _ResultAs(Success, type, value)
8
10
  end
9
11
 
10
12
  def Failure(type, **value)
11
- Failure.new(type: type, value: value, subject: self)
13
+ _ResultAs(Failure, type, value)
14
+ end
15
+
16
+ private def _ResultAs(kind_class, type, value, halted: nil)
17
+ kind_class.new(type: type, value: value, subject: self, halted: halted)
12
18
  end
13
19
  end
14
20
 
15
21
  module Addons
16
22
  module Continuable
23
+ def Success(type, **value)
24
+ _ResultAs(Success, type, value, halted: true)
25
+ end
26
+
17
27
  private def Continue(**value)
18
- Success.new(type: :continued, value: value, subject: self)
28
+ _ResultAs(Success, :continued, value)
19
29
  end
20
30
  end
21
31
 
22
- OPTIONS = { Continue: Continuable }.freeze
32
+ OPTIONS = { continue: Continuable }.freeze
23
33
 
24
- def self.options(names)
25
- Array(names).filter_map { |name| OPTIONS[name] }
34
+ def self.options(config_flags)
35
+ ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
26
36
  end
27
37
  end
28
38
  end
29
39
 
30
- def self.mixin(with: nil)
31
- addons = Mixin::Addons.options(with)
40
+ def self.mixin_module
41
+ Mixin
42
+ end
32
43
 
33
- mod = ::BCDD::Result::Mixin.module!
34
- mod.send(:include, Mixin::Methods)
35
- mod.send(:include, *addons) unless addons.empty?
36
- mod
44
+ def self.result_factory
45
+ ::BCDD::Result::Context
37
46
  end
47
+
48
+ private_class_method :mixin_module, :result_factory
38
49
  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)
18
+ def initialize(type:, value:, subject: nil, expectations: nil, halted: nil)
19
19
  value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
20
20
 
21
21
  @acc = {}
@@ -35,7 +35,7 @@ class BCDD::Result
35
35
  private
36
36
 
37
37
  def for(data)
38
- case data.name
38
+ case data.kind
39
39
  when :unknown then Contract::Disabled
40
40
  when :success then success
41
41
  else failure
@@ -4,7 +4,12 @@ class BCDD::Result
4
4
  class Contract::ForTypesAndValues
5
5
  include Contract::Interface
6
6
 
7
- def initialize(types_and_values)
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
+
8
13
  @types_and_values = types_and_values.transform_keys(&:to_sym)
9
14
 
10
15
  @types_contract = Contract::ForTypes.new(@types_and_values.keys)
@@ -29,7 +34,7 @@ class BCDD::Result
29
34
 
30
35
  checking_result = value_checking === value
31
36
 
32
- return value if checking_result || (Contract.nil_as_valid_value_checking? && checking_result.nil?)
37
+ return value if checking_result || (checking_result.nil? && @nil_as_valid_value_checking)
33
38
 
34
39
  raise Contract::Error::UnexpectedValue.build(type: type, value: value)
35
40
  rescue ::NoMatchingPatternError => e
@@ -19,24 +19,14 @@ module BCDD::Result::Contract
19
19
  TypeChecker.new(data.type, expectations: contract)
20
20
  end
21
21
 
22
- ToEnsure = ->(spec) do
22
+ ToEnsure = ->(spec, config) do
23
23
  return Disabled if spec.nil?
24
24
 
25
- spec.is_a?(::Hash) ? ForTypesAndValues.new(spec) : ForTypes.new(Array(spec))
25
+ spec.is_a?(::Hash) ? ForTypesAndValues.new(spec, config) : ForTypes.new(Array(spec))
26
26
  end
27
27
 
28
- def self.new(success:, failure:)
29
- Evaluator.new(ToEnsure[success], ToEnsure[failure])
30
- end
31
-
32
- @nil_as_valid_value_checking = false
33
-
34
- def self.nil_as_valid_value_checking!(enabled: true)
35
- @nil_as_valid_value_checking = enabled
36
- end
37
-
38
- def self.nil_as_valid_value_checking?
39
- @nil_as_valid_value_checking
28
+ def self.new(success:, failure:, config:)
29
+ Evaluator.new(ToEnsure[success, config], ToEnsure[failure, config])
40
30
  end
41
31
 
42
32
  private_constant :ToEnsure
@@ -2,26 +2,26 @@
2
2
 
3
3
  class BCDD::Result
4
4
  class Data
5
- attr_reader :name, :type, :value
5
+ attr_reader :kind, :type, :value
6
6
 
7
- def initialize(name, type, value)
8
- @name = name
7
+ def initialize(kind, type, value)
8
+ @kind = kind
9
9
  @type = type.to_sym
10
10
  @value = value
11
11
  end
12
12
 
13
13
  def to_h
14
- { name: name, type: type, value: value }
14
+ { kind: kind, type: type, value: value }
15
15
  end
16
16
 
17
17
  def to_a
18
- [name, type, value]
18
+ [kind, type, value]
19
19
  end
20
20
 
21
21
  def inspect
22
22
  format(
23
- '#<%<class_name>s name=%<name>p type=%<type>p value=%<value>p>',
24
- class_name: self.class.name, name: name, type: type, value: value
23
+ '#<%<class_name>s kind=%<kind>p type=%<type>p value=%<value>p>',
24
+ class_name: self.class.name, kind: kind, type: type, value: value
25
25
  )
26
26
  end
27
27
 
@@ -2,21 +2,38 @@
2
2
 
3
3
  class BCDD::Result
4
4
  module Expectations::Mixin
5
- METHODS = <<~RUBY
6
- def Success(...)
7
- _Result.Success(...)
5
+ module Factory
6
+ def self.module!
7
+ ::Module.new do
8
+ def self.included(base); base.const_set(:ResultExpectationsMixin, self); end
9
+ def self.extended(base); base.const_set(:ResultExpectationsMixin, self); end
10
+ end
8
11
  end
12
+ end
9
13
 
10
- def Failure(...)
11
- _Result.Failure(...)
12
- end
14
+ module Methods
15
+ BASE = <<~RUBY
16
+ def Success(...)
17
+ _Result.Success(...)
18
+ end
19
+
20
+ def Failure(...)
21
+ _Result.Failure(...)
22
+ end
23
+ RUBY
13
24
 
14
- private
25
+ FACTORY = <<~RUBY
26
+ private def _Result
27
+ @_Result ||= Result.with(subject: self, halted: %<halted>s)
28
+ end
29
+ RUBY
30
+
31
+ def self.to_eval(addons)
32
+ halted = addons.key?(:continue) ? 'true' : 'nil'
15
33
 
16
- def _Result
17
- @_Result ||= Result.with(subject: self)
34
+ "#{BASE}\n#{format(FACTORY, halted: halted)}"
18
35
  end
19
- RUBY
36
+ end
20
37
 
21
38
  module Addons
22
39
  module Continuable
@@ -25,17 +42,10 @@ class BCDD::Result
25
42
  end
26
43
  end
27
44
 
28
- OPTIONS = { Continue: Continuable }.freeze
29
-
30
- def self.options(names)
31
- Array(names).filter_map { |name| OPTIONS[name] }
32
- end
33
- end
45
+ OPTIONS = { continue: Continuable }.freeze
34
46
 
35
- def self.module!
36
- ::Module.new do
37
- def self.included(base); base.const_set(:ResultExpectationsMixin, self); end
38
- def self.extended(base); base.const_set(:ResultExpectationsMixin, self); end
47
+ def self.options(config_flags)
48
+ Config::Options.addon(map: config_flags, from: OPTIONS)
39
49
  end
40
50
  end
41
51
  end
@@ -4,38 +4,72 @@ class BCDD::Result
4
4
  class Expectations
5
5
  require_relative 'expectations/mixin'
6
6
 
7
- def self.mixin(success: nil, failure: nil, with: nil)
8
- addons = Mixin::Addons.options(with)
7
+ def self.mixin(**options)
8
+ return mixin!(**options) if Config.instance.feature.enabled?(:expectations)
9
9
 
10
- mod = Mixin.module!
11
- mod.const_set(:Result, new(success: success, failure: failure).freeze)
12
- mod.module_eval(Mixin::METHODS)
13
- mod.send(:include, *addons) unless addons.empty?
10
+ result_factory_without_expectations.mixin(**options.slice(:config))
11
+ end
12
+
13
+ def self.mixin!(success: nil, failure: nil, config: nil)
14
+ addons = mixin_module::Addons.options(config)
15
+
16
+ mod = mixin_module::Factory.module!
17
+ mod.const_set(:Result, new(success: success, failure: failure, config: config).freeze)
18
+ mod.module_eval(mixin_module::Methods.to_eval(addons), __FILE__, __LINE__ + 1)
19
+ mod.send(:include, *addons.values) unless addons.empty?
14
20
  mod
15
21
  end
16
22
 
17
- def initialize(subject: nil, success: nil, failure: nil, contract: nil)
23
+ def self.mixin_module
24
+ Mixin
25
+ end
26
+
27
+ def self.result_factory_without_expectations
28
+ ::BCDD::Result
29
+ end
30
+
31
+ def self.new(...)
32
+ return result_factory_without_expectations unless Config.instance.feature.enabled?(:expectations)
33
+
34
+ instance = allocate
35
+ instance.send(:initialize, ...)
36
+ instance
37
+ end
38
+
39
+ private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
40
+
41
+ def initialize(subject: nil, contract: nil, halted: nil, **options)
42
+ @halted = halted
43
+
18
44
  @subject = subject
19
45
 
20
46
  @contract = contract if contract.is_a?(Contract::Evaluator)
21
47
 
22
- @contract ||= Contract.new(success: success, failure: failure).freeze
48
+ @contract ||= Contract.new(
49
+ success: options[:success],
50
+ failure: options[:failure],
51
+ config: options[:config]
52
+ ).freeze
23
53
  end
24
54
 
25
55
  def Success(type, value = nil)
26
- Success.new(type: type, value: value, subject: subject, expectations: contract)
56
+ _ResultAs(Success, type, value)
27
57
  end
28
58
 
29
59
  def Failure(type, value = nil)
30
- Failure.new(type: type, value: value, subject: subject, expectations: contract)
60
+ _ResultAs(Failure, type, value)
31
61
  end
32
62
 
33
- def with(subject:)
34
- self.class.new(subject: subject, contract: contract)
63
+ def with(subject:, halted: nil)
64
+ self.class.new(subject: subject, halted: halted, contract: contract)
35
65
  end
36
66
 
37
67
  private
38
68
 
39
- attr_reader :subject, :contract
69
+ def _ResultAs(kind_class, type, value)
70
+ kind_class.new(type: type, value: value, subject: subject, expectations: contract, halted: halted)
71
+ end
72
+
73
+ attr_reader :subject, :halted, :contract
40
74
  end
41
75
  end
@@ -15,7 +15,7 @@ module BCDD::Result::Failure::Methods
15
15
 
16
16
  private
17
17
 
18
- def name
18
+ def kind
19
19
  :failure
20
20
  end
21
21
  end