bcdd-result 0.7.0 → 0.9.0

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