From f7fd6b8d54bf3489173fc5d6795469a49ae2b188 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Sun, 5 Nov 2023 22:02:16 -0300 Subject: [PATCH 01/30] Add BCDD::Result::Config --- Steepfile | 2 +- lib/bcdd/result/config.rb | 40 +++++++++++++ lib/bcdd/result/config/exposable.rb | 90 +++++++++++++++++++++++++++++ sig/bcdd/result.rbs | 58 +++++++++++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 lib/bcdd/result/config.rb create mode 100644 lib/bcdd/result/config/exposable.rb diff --git a/Steepfile b/Steepfile index 79d2fb8..cd52064 100644 --- a/Steepfile +++ b/Steepfile @@ -10,7 +10,7 @@ target :lib do # check 'app/models/**/*.rb' # Glob # ignore 'lib/templates/*.rb' - # library 'pathname' # Standard libraries + library 'singleton' # Standard libraries # library 'strong_json' # Gems # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default) diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb new file mode 100644 index 0000000..94bac1a --- /dev/null +++ b/lib/bcdd/result/config.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'singleton' + +class BCDD::Result + class Config + include Singleton + + require_relative 'config/exposable' + + attr_reader :_exposable + + private :_exposable + + def initialize + @_exposable = Exposable.new + end + + def freeze + _exposable.freeze + super + end + + def exposable + _exposable.options + end + + def exposed?(option) + _exposable.enabled?(option) + end + + def expose!(*options) + _exposable.enable!(options) + end + + def unexpose!(*options) + _exposable.disable!(options) + end + end +end diff --git a/lib/bcdd/result/config/exposable.rb b/lib/bcdd/result/config/exposable.rb new file mode 100644 index 0000000..e8b2226 --- /dev/null +++ b/lib/bcdd/result/config/exposable.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +class BCDD::Result + class Config + class Exposable + module Option + LIST = [ + RESULT = 'Result' + ].freeze + + AFFECTS = { + RESULT => %w[Object] + }.transform_values!(&:freeze).freeze + + MAPPING = { + RESULT => { target: ::Object, name: :Result, value: ::BCDD::Result } + }.transform_values!(&:freeze).freeze + end + + attr_reader :_options + + private :_options + + def initialize + @_options = Option::LIST.to_h { [_1, false] } + end + + def freeze + _options.freeze + super + end + + def options + Option::AFFECTS.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] } + end + + def enabled?(name) + _options[name] || false + end + + def enable!(names) + enable_many(names, to: true) + end + + def disable!(names) + enable_many(names, to: false) + end + + private + + def enable_many(names, to:) + option_required!(names) + + names.each { |name| enable_one(name, to) } + + options.slice(*names) + end + + def enable_one(option_name, boolean) + mapping = Option::MAPPING.fetch(option_name) + + target, name, value = mapping.fetch_values(:target, :name, :value) + + enable_one!(boolean, target: target, name: name, value: value) + + _options[name] = boolean + rescue ::KeyError + option_invalid!(name) + end + + def enable_one!(boolean, target:, name:, value:) + defined = target.const_defined?(name, false) + + boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name) + end + + AVAILABLE_OPTIONS = "Available options: #{Option::LIST.map(&:inspect).join(', ')}" + + def option_required!(names) + raise ::ArgumentError, "One or more options required. #{AVAILABLE_OPTIONS}" if names.empty? + end + + def option_invalid!(name) + raise ::ArgumentError, "Invalid option: #{name.inspect}. #{AVAILABLE_OPTIONS}" + end + end + + private_constant :Exposable + end +end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index f126124..96e36f7 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -481,3 +481,61 @@ module BCDD::Result::Context::Expectations::Mixin def self.options: (Symbol) -> Array[Module] end end + +class BCDD::Result::Config + include Singleton + + private attr_reader _exposable: BCDD::Result::Config::Exposable + + def self.instance: -> BCDD::Result::Config + + def initialize: -> void + + def freeze: -> BCDD::Result::Config + + def exposable: -> Hash[String, Hash[Symbol, untyped]] + + def exposed?: (String) -> bool + + def expose!: (*String) -> Hash[String, Hash[Symbol, untyped]] + + def unexpose!: (*String) -> Hash[String, Hash[Symbol, untyped]] +end + +class BCDD::Result::Config::Exposable + module Option + RESULT: String + + LIST: Array[String] + AFFECTS: Hash[String, Array[String]] + MAPPING: Hash[String, Hash[Symbol, untyped]] + end + + AVAILABLE_OPTIONS: String + + private attr_reader _options: Hash[String, bool] + + def initialize: -> void + + def freeze: -> BCDD::Result::Config::Exposable + + def options: -> Hash[String, Hash[Symbol, untyped]] + + def enabled?: (String) -> bool + + def enable!: (Array[String]) -> Hash[String, Hash[Symbol, untyped]] + + def disable!: (Array[String]) -> Hash[String, Hash[Symbol, untyped]] + + private + + def enable_many: (Array[String], to: bool) -> Hash[String, Hash[Symbol, untyped]] + + def enable_one: (String, bool) -> void + + def enable_one!: (bool, target: untyped, name: Symbol, value: untyped) -> bool + + def option_required!: (Array[String]) -> void + + def option_invalid!: (String) -> void +end From fd8dcbdaad6d94badba1cd89aeee673b6d400122 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Sun, 5 Nov 2023 22:02:45 -0300 Subject: [PATCH 02/30] Add BCDD::Result.config and configure methods --- CHANGELOG.md | 25 +++++++++++++++++++++++++ lib/bcdd/result.rb | 11 +++++++++++ sig/bcdd/result.rbs | 4 ++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e99485d..a6b2fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ ## [Unreleased] +- Add `BCDD::Result.config` + - **Exposable** + ```ruby + BCDD::Result.config.exposable + + BCDD::Result.config.exposed?('Result') + + BCDD::Result.config.expose!('Result') + + BCDD::Result.config.unexpose!('Result') + ``` + +- Add `BCDD::Result.configuration`. It freezes the configuration, disallowing methods that promote changes but allowing the query ones. You can use this feature to ensure integrity in your configuration. + ```ruby + BCDD::Result.config.exposed?('Result') # false + + BCDD::Result.configuration do |config| + config.expose!('Result') + end + + BCDD::Result.config.exposed?('Result') # true + + BCDD::Result.config.unexpose!('Result') # raises FrozenError + ``` + ## [0.7.0] - 2023-10-27 ### Added diff --git a/lib/bcdd/result.rb b/lib/bcdd/result.rb index fd57f4e..42645b6 100644 --- a/lib/bcdd/result.rb +++ b/lib/bcdd/result.rb @@ -10,6 +10,7 @@ require_relative 'result/contract' require_relative 'result/expectations' require_relative 'result/context' +require_relative 'result/config' class BCDD::Result attr_accessor :unknown @@ -20,6 +21,16 @@ class BCDD::Result private :unknown, :unknown=, :type_checker + def self.config + Config.instance + end + + def self.configuration + yield(config) + + config.freeze + end + def initialize(type:, value:, subject: nil, expectations: nil) data = Data.new(name, type, value) diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 96e36f7..e413e47 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -11,6 +11,10 @@ class BCDD::Result attr_reader data: BCDD::Result::Data attr_reader subject: untyped + def self.config: -> BCDD::Result::Config + + def self.configuration: { (BCDD::Result::Config) -> void } -> BCDD::Result::Config + def initialize: ( type: Symbol, value: untyped, From f6556e6bd826f1754d7ffdc3791eee75f83e8df5 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Sun, 5 Nov 2023 22:03:03 -0300 Subject: [PATCH 03/30] Refactor lib/result with BCDD::Result.config --- lib/result.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/result.rb b/lib/result.rb index e287cae..d1b7b7c 100644 --- a/lib/result.rb +++ b/lib/result.rb @@ -2,4 +2,4 @@ require_relative 'bcdd/result' -Object.const_set(:Result, BCDD::Result) +BCDD::Result.config.expose!('Result') From 3be3c3e5f6e3106d49b8d9cfb199b3b8d885cc35 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Sun, 5 Nov 2023 22:59:52 -0300 Subject: [PATCH 04/30] Add BCDD::Result.config.pattern_matching --- CHANGELOG.md | 34 +++++--- lib/bcdd/result/config.rb | 32 +++----- .../{exposable.rb => constant_alias.rb} | 22 ++--- lib/bcdd/result/config/pattern_matching.rb | 80 +++++++++++++++++++ lib/bcdd/result/contract.rb | 10 --- .../result/contract/for_types_and_values.rb | 8 +- lib/result.rb | 2 +- sig/bcdd/result.rbs | 67 +++++++++++----- .../nil_as_valid_value_checking_test.rb | 8 +- 9 files changed, 185 insertions(+), 78 deletions(-) rename lib/bcdd/result/config/{exposable.rb => constant_alias.rb} (79%) create mode 100644 lib/bcdd/result/config/pattern_matching.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b2fc8..ce27609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +1,46 @@ ## [Unreleased] +### Added + - Add `BCDD::Result.config` - - **Exposable** + - **Constant Aliases** + ```ruby + BCDD::Result.config.constant_alias.options + + BCDD::Result.config.constant_alias.enabled?('Result') + + BCDD::Result.config.constant_alias.enable!('Result') + + BCDD::Result.config.constant_alias.disable!('Result') + ``` + - **Default Add-ons** ```ruby - BCDD::Result.config.exposable + BCDD::Result.config.pattern_matching.options - BCDD::Result.config.exposed?('Result') + BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking) - BCDD::Result.config.expose!('Result') + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - BCDD::Result.config.unexpose!('Result') + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) ``` - Add `BCDD::Result.configuration`. It freezes the configuration, disallowing methods that promote changes but allowing the query ones. You can use this feature to ensure integrity in your configuration. ```ruby - BCDD::Result.config.exposed?('Result') # false + BCDD::Result.config.constant_alias.enabled?('Result') # false BCDD::Result.configuration do |config| - config.expose!('Result') + config.constant_alias.enable!('Result') end - BCDD::Result.config.exposed?('Result') # true + BCDD::Result.config.constant_alias.enabled?('Result') # true - BCDD::Result.config.unexpose!('Result') # raises FrozenError + BCDD::Result.config.constant_alias.disable!('Result') # raises FrozenError ``` +### Changed + +- **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking!` with `BCDD::Result.config`. + ## [0.7.0] - 2023-10-27 ### Added diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index 94bac1a..8296d99 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -4,37 +4,23 @@ class BCDD::Result class Config - include Singleton - - require_relative 'config/exposable' + require_relative 'config/constant_alias' + require_relative 'config/pattern_matching' - attr_reader :_exposable + include Singleton - private :_exposable + attr_reader :pattern_matching, :constant_alias def initialize - @_exposable = Exposable.new + @constant_alias = ConstantAlias.new + @pattern_matching = PatternMatching.new end def freeze - _exposable.freeze - super - end - - def exposable - _exposable.options - end - - def exposed?(option) - _exposable.enabled?(option) - end + constant_alias.freeze + pattern_matching.freeze - def expose!(*options) - _exposable.enable!(options) - end - - def unexpose!(*options) - _exposable.disable!(options) + super end end end diff --git a/lib/bcdd/result/config/exposable.rb b/lib/bcdd/result/config/constant_alias.rb similarity index 79% rename from lib/bcdd/result/config/exposable.rb rename to lib/bcdd/result/config/constant_alias.rb index e8b2226..0355d5b 100644 --- a/lib/bcdd/result/config/exposable.rb +++ b/lib/bcdd/result/config/constant_alias.rb @@ -2,7 +2,7 @@ class BCDD::Result class Config - class Exposable + class ConstantAlias module Option LIST = [ RESULT = 'Result' @@ -38,37 +38,37 @@ def enabled?(name) _options[name] || false end - def enable!(names) - enable_many(names, to: true) + def enable!(*names) + set_many(names, to: true) end - def disable!(names) - enable_many(names, to: false) + def disable!(*names) + set_many(names, to: false) end private - def enable_many(names, to:) + def set_many(names, to:) option_required!(names) - names.each { |name| enable_one(name, to) } + names.each { |name| set_one(name, to) } options.slice(*names) end - def enable_one(option_name, boolean) + def set_one(option_name, boolean) mapping = Option::MAPPING.fetch(option_name) target, name, value = mapping.fetch_values(:target, :name, :value) - enable_one!(boolean, target: target, name: name, value: value) + set_one!(boolean, target: target, name: name, value: value) _options[name] = boolean rescue ::KeyError option_invalid!(name) end - def enable_one!(boolean, target:, name:, value:) + def set_one!(boolean, target:, name:, value:) defined = target.const_defined?(name, false) boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name) @@ -85,6 +85,6 @@ def option_invalid!(name) end end - private_constant :Exposable + private_constant :ConstantAlias end end diff --git a/lib/bcdd/result/config/pattern_matching.rb b/lib/bcdd/result/config/pattern_matching.rb new file mode 100644 index 0000000..48410e0 --- /dev/null +++ b/lib/bcdd/result/config/pattern_matching.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +class BCDD::Result + class Config + class PatternMatching + module Option + LIST = [ + NIL_AS_VALID_VALUE_CHECKING = :nil_as_valid_value_checking + ].freeze + + AFFECTS = { + NIL_AS_VALID_VALUE_CHECKING => %w[ + BCDD::Result::Expectations + BCDD::Result::Context::Expectations + ] + }.transform_values!(&:freeze).freeze + end + + attr_reader :_options + + private :_options + + def initialize + @_options = Option::LIST.to_h { [_1, false] } + end + + def freeze + _options.freeze + super + end + + def options + Option::AFFECTS.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] } + end + + def enabled?(name) + _options[name] || false + end + + def enable!(*names) + set_many(names, to: true) + end + + def disable!(*names) + set_many(names, to: false) + end + + private + + def set_many(names, to:) + option_required!(names) + + names.each { |name| set_one(name, to) } + + options.slice(*names) + end + + def set_one(name, boolean) + case name + when Option::NIL_AS_VALID_VALUE_CHECKING + _options[name] = boolean + else + option_invalid!(name) + end + end + + AVAILABLE_OPTIONS = "Available options: #{Option::LIST.map(&:inspect).join(', ')}" + + def option_required!(names) + raise ::ArgumentError, "One or more options required. #{AVAILABLE_OPTIONS}" if names.empty? + end + + def option_invalid!(name) + raise ::ArgumentError, "Invalid option: #{name.inspect}. #{AVAILABLE_OPTIONS}" + end + end + + private_constant :PatternMatching + end +end diff --git a/lib/bcdd/result/contract.rb b/lib/bcdd/result/contract.rb index 21a8c96..7a182f7 100644 --- a/lib/bcdd/result/contract.rb +++ b/lib/bcdd/result/contract.rb @@ -29,15 +29,5 @@ def self.new(success:, failure:) Evaluator.new(ToEnsure[success], ToEnsure[failure]) end - @nil_as_valid_value_checking = false - - def self.nil_as_valid_value_checking!(enabled: true) - @nil_as_valid_value_checking = enabled - end - - def self.nil_as_valid_value_checking? - @nil_as_valid_value_checking - end - private_constant :ToEnsure end diff --git a/lib/bcdd/result/contract/for_types_and_values.rb b/lib/bcdd/result/contract/for_types_and_values.rb index 9cf8a4f..44a659e 100644 --- a/lib/bcdd/result/contract/for_types_and_values.rb +++ b/lib/bcdd/result/contract/for_types_and_values.rb @@ -29,11 +29,17 @@ def type_and_value!(data) checking_result = value_checking === value - return value if checking_result || (Contract.nil_as_valid_value_checking? && checking_result.nil?) + return value if checking_result || (nil_as_valid_value_checking? && checking_result.nil?) raise Contract::Error::UnexpectedValue.build(type: type, value: value) rescue ::NoMatchingPatternError => e raise Contract::Error::UnexpectedValue.build(type: data.type, value: data.value, cause: e) end + + private + + def nil_as_valid_value_checking? + Config.instance.pattern_matching.enabled?(:nil_as_valid_value_checking) + end end end diff --git a/lib/result.rb b/lib/result.rb index d1b7b7c..9723a63 100644 --- a/lib/result.rb +++ b/lib/result.rb @@ -2,4 +2,4 @@ require_relative 'bcdd/result' -BCDD::Result.config.expose!('Result') +BCDD::Result.config.constant_alias.enable!('Result') diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index e413e47..f6a3f0c 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -236,9 +236,6 @@ module BCDD::Result::Contract success: Hash[Symbol, untyped] | Array[Symbol], failure: Hash[Symbol, untyped] | Array[Symbol] ) -> BCDD::Result::Contract::Evaluator - - def self.nil_as_valid_value_checking!: (?enabled: bool) -> void - def self.nil_as_valid_value_checking?: -> bool end module BCDD::Result::Contract @@ -314,6 +311,10 @@ module BCDD::Result::Contract include Interface def initialize: (Hash[Symbol, untyped]) -> void + + private + + def nil_as_valid_value_checking?: -> bool end end @@ -489,24 +490,17 @@ end class BCDD::Result::Config include Singleton - private attr_reader _exposable: BCDD::Result::Config::Exposable + attr_reader constant_alias: BCDD::Result::Config::ConstantAlias + attr_reader pattern_matching: BCDD::Result::Config::PatternMatching def self.instance: -> BCDD::Result::Config def initialize: -> void def freeze: -> BCDD::Result::Config - - def exposable: -> Hash[String, Hash[Symbol, untyped]] - - def exposed?: (String) -> bool - - def expose!: (*String) -> Hash[String, Hash[Symbol, untyped]] - - def unexpose!: (*String) -> Hash[String, Hash[Symbol, untyped]] end -class BCDD::Result::Config::Exposable +class BCDD::Result::Config::ConstantAlias module Option RESULT: String @@ -521,25 +515,60 @@ class BCDD::Result::Config::Exposable def initialize: -> void - def freeze: -> BCDD::Result::Config::Exposable + def freeze: -> BCDD::Result::Config::ConstantAlias def options: -> Hash[String, Hash[Symbol, untyped]] def enabled?: (String) -> bool - def enable!: (Array[String]) -> Hash[String, Hash[Symbol, untyped]] + def enable!: (*String) -> Hash[String, Hash[Symbol, untyped]] - def disable!: (Array[String]) -> Hash[String, Hash[Symbol, untyped]] + def disable!: (*String) -> Hash[String, Hash[Symbol, untyped]] private - def enable_many: (Array[String], to: bool) -> Hash[String, Hash[Symbol, untyped]] + def set_many: (Array[String], to: bool) -> Hash[String, Hash[Symbol, untyped]] - def enable_one: (String, bool) -> void + def set_one: (String, bool) -> void - def enable_one!: (bool, target: untyped, name: Symbol, value: untyped) -> bool + def set_one!: (bool, target: untyped, name: Symbol, value: untyped) -> bool def option_required!: (Array[String]) -> void def option_invalid!: (String) -> void end + +class BCDD::Result::Config::PatternMatching + module Option + NIL_AS_VALID_VALUE_CHECKING: Symbol + + LIST: Array[Symbol] + AFFECTS: Hash[Symbol, Array[String]] + end + + AVAILABLE_OPTIONS: String + + private attr_reader _options: Hash[Symbol, bool] + + def initialize: -> void + + def freeze: -> BCDD::Result::Config::PatternMatching + + def options: -> Hash[Symbol, Hash[Symbol, untyped]] + + def enabled?: (Symbol) -> bool + + def enable!: (*Symbol) -> Hash[Symbol, Hash[Symbol, untyped]] + + def disable!: (*Symbol) -> Hash[Symbol, Hash[Symbol, untyped]] + + private + + def set_many: (Array[Symbol], to: bool) -> Hash[Symbol, Hash[Symbol, untyped]] + + def set_one: (Symbol, bool) -> void + + def option_required!: (Array[Symbol]) -> void + + def option_invalid!: (Symbol) -> void +end diff --git a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb index cc034a8..1c31e92 100644 --- a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb +++ b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb @@ -19,14 +19,14 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test _Result::Success(:ok, 1) end - Contract.nil_as_valid_value_checking! + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) result = _Result::Success(:ok, 1) assert result.success?(:ok) assert_equal(1, result.value) ensure - Contract.nil_as_valid_value_checking!(enabled: false) + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) end test 'BCDD::Result::Context::Expectations' do @@ -44,14 +44,14 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test _Result::Success(:ok, number: 1) end - Contract.nil_as_valid_value_checking! + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) result = _Result::Success(:ok, number: 1) assert result.success?(:ok) assert_equal({ number: 1 }, result.value) ensure - Contract.nil_as_valid_value_checking!(enabled: false) + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) end end end From aab0e53afb432338930ae583ae2bc5faa4800b1f Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 6 Nov 2023 23:36:07 -0300 Subject: [PATCH 05/30] Add BCDD::Result::Config::Switcher --- lib/bcdd/result/config/switcher.rb | 78 ++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/bcdd/result/config/switcher.rb diff --git a/lib/bcdd/result/config/switcher.rb b/lib/bcdd/result/config/switcher.rb new file mode 100644 index 0000000..a7d7fc4 --- /dev/null +++ b/lib/bcdd/result/config/switcher.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +class BCDD::Result + class Config + class Switcher + attr_reader :_options, :_affects, :listener + + private :_options, :_affects, :listener + + def self.factory(options:, listener: nil) + -> { new(options: options, listener: listener) } + end + + def initialize(options:, listener:) + @_affects = options + @_options = options.keys.to_h { [_1, false] } + @listener = listener + end + + def freeze + _options.freeze + super + end + + def options + _affects.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] } + end + + def enabled?(name) + _options[name] || false + end + + def enable!(*names) + set_many(names, to: true) + end + + def disable!(*names) + set_many(names, to: false) + end + + private + + def set_many(names, to:) + require_option!(names) + + names.each do |name| + set_one(name, to) + + listener&.call(name, to) + end + + options.slice(*names) + end + + def set_one(name, boolean) + validate_option!(name) + + _options[name] = boolean + end + + def require_option!(names) + raise ::ArgumentError, "One or more options required. #{available_options_message}" if names.empty? + end + + def validate_option!(name) + return if _options.key?(name) + + raise ::ArgumentError, "Invalid option: #{name.inspect}. #{available_options_message}" + end + + def available_options_message + "Available options: #{_options.keys.map(&:inspect).join(', ')}" + end + end + + private_constant :Switcher + end +end From 6779e140c06333bced8e65ac12d1faf74a5b790c Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 6 Nov 2023 23:36:26 -0300 Subject: [PATCH 06/30] Refactor BCDD::Result::Config --- lib/bcdd/result/config.rb | 26 +++++-- lib/bcdd/result/config/constant_alias.rb | 81 +++----------------- lib/bcdd/result/config/pattern_matching.rb | 80 -------------------- sig/bcdd/result.rbs | 88 ++++++++-------------- 4 files changed, 62 insertions(+), 213 deletions(-) delete mode 100644 lib/bcdd/result/config/pattern_matching.rb diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index 8296d99..d7c56dc 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -2,18 +2,32 @@ require 'singleton' +require_relative 'config/switcher' +require_relative 'config/constant_alias' + class BCDD::Result class Config - require_relative 'config/constant_alias' - require_relative 'config/pattern_matching' - include Singleton - attr_reader :pattern_matching, :constant_alias + CONSTANT_ALIAS = Switcher.factory( + options: ConstantAlias::OPTIONS, + listener: ConstantAlias::Listener + ) + + PATTERN_MATCHING = Switcher.factory( + options: { + nil_as_valid_value_checking: %w[ + BCDD::Result::Expectations + BCDD::Result::Context::Expectations + ] + } + ) + + attr_reader :constant_alias, :pattern_matching def initialize - @constant_alias = ConstantAlias.new - @pattern_matching = PatternMatching.new + @constant_alias = CONSTANT_ALIAS.call + @pattern_matching = PATTERN_MATCHING.call end def freeze diff --git a/lib/bcdd/result/config/constant_alias.rb b/lib/bcdd/result/config/constant_alias.rb index 0355d5b..6178d32 100644 --- a/lib/bcdd/result/config/constant_alias.rb +++ b/lib/bcdd/result/config/constant_alias.rb @@ -2,87 +2,26 @@ class BCDD::Result class Config - class ConstantAlias - module Option - LIST = [ - RESULT = 'Result' - ].freeze + module ConstantAlias + RESULT = 'Result' - AFFECTS = { - RESULT => %w[Object] - }.transform_values!(&:freeze).freeze + OPTIONS = { + RESULT => %w[Object] + }.transform_values!(&:freeze).freeze - MAPPING = { - RESULT => { target: ::Object, name: :Result, value: ::BCDD::Result } - }.transform_values!(&:freeze).freeze - end - - attr_reader :_options - - private :_options - - def initialize - @_options = Option::LIST.to_h { [_1, false] } - end - - def freeze - _options.freeze - super - end - - def options - Option::AFFECTS.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] } - end - - def enabled?(name) - _options[name] || false - end - - def enable!(*names) - set_many(names, to: true) - end - - def disable!(*names) - set_many(names, to: false) - end - - private + MAPPING = { + RESULT => { target: ::Object, name: :Result, value: ::BCDD::Result } + }.transform_values!(&:freeze).freeze - def set_many(names, to:) - option_required!(names) - - names.each { |name| set_one(name, to) } - - options.slice(*names) - end - - def set_one(option_name, boolean) - mapping = Option::MAPPING.fetch(option_name) + Listener = ->(option_name, boolean) do + mapping = MAPPING.fetch(option_name) target, name, value = mapping.fetch_values(:target, :name, :value) - set_one!(boolean, target: target, name: name, value: value) - - _options[name] = boolean - rescue ::KeyError - option_invalid!(name) - end - - def set_one!(boolean, target:, name:, value:) defined = target.const_defined?(name, false) boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name) end - - AVAILABLE_OPTIONS = "Available options: #{Option::LIST.map(&:inspect).join(', ')}" - - def option_required!(names) - raise ::ArgumentError, "One or more options required. #{AVAILABLE_OPTIONS}" if names.empty? - end - - def option_invalid!(name) - raise ::ArgumentError, "Invalid option: #{name.inspect}. #{AVAILABLE_OPTIONS}" - end end private_constant :ConstantAlias diff --git a/lib/bcdd/result/config/pattern_matching.rb b/lib/bcdd/result/config/pattern_matching.rb deleted file mode 100644 index 48410e0..0000000 --- a/lib/bcdd/result/config/pattern_matching.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -class BCDD::Result - class Config - class PatternMatching - module Option - LIST = [ - NIL_AS_VALID_VALUE_CHECKING = :nil_as_valid_value_checking - ].freeze - - AFFECTS = { - NIL_AS_VALID_VALUE_CHECKING => %w[ - BCDD::Result::Expectations - BCDD::Result::Context::Expectations - ] - }.transform_values!(&:freeze).freeze - end - - attr_reader :_options - - private :_options - - def initialize - @_options = Option::LIST.to_h { [_1, false] } - end - - def freeze - _options.freeze - super - end - - def options - Option::AFFECTS.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] } - end - - def enabled?(name) - _options[name] || false - end - - def enable!(*names) - set_many(names, to: true) - end - - def disable!(*names) - set_many(names, to: false) - end - - private - - def set_many(names, to:) - option_required!(names) - - names.each { |name| set_one(name, to) } - - options.slice(*names) - end - - def set_one(name, boolean) - case name - when Option::NIL_AS_VALID_VALUE_CHECKING - _options[name] = boolean - else - option_invalid!(name) - end - end - - AVAILABLE_OPTIONS = "Available options: #{Option::LIST.map(&:inspect).join(', ')}" - - def option_required!(names) - raise ::ArgumentError, "One or more options required. #{AVAILABLE_OPTIONS}" if names.empty? - end - - def option_invalid!(name) - raise ::ArgumentError, "Invalid option: #{name.inspect}. #{AVAILABLE_OPTIONS}" - end - end - - private_constant :PatternMatching - end -end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index f6a3f0c..c3d20ef 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -490,8 +490,11 @@ end class BCDD::Result::Config include Singleton - attr_reader constant_alias: BCDD::Result::Config::ConstantAlias - attr_reader pattern_matching: BCDD::Result::Config::PatternMatching + CONSTANT_ALIAS: Proc + PATTERN_MATCHING: Proc + + attr_reader constant_alias: BCDD::Result::Config::Switcher + attr_reader pattern_matching: BCDD::Result::Config::Switcher def self.instance: -> BCDD::Result::Config @@ -500,75 +503,48 @@ class BCDD::Result::Config def freeze: -> BCDD::Result::Config end -class BCDD::Result::Config::ConstantAlias - module Option - RESULT: String - - LIST: Array[String] - AFFECTS: Hash[String, Array[String]] - MAPPING: Hash[String, Hash[Symbol, untyped]] - end - - AVAILABLE_OPTIONS: String +class BCDD::Result::Config::Switcher + private attr_reader _affects: Hash[Symbol | String, Array[String]] + private attr_reader _options: Hash[Symbol | String, bool] + private attr_reader listener: Proc - private attr_reader _options: Hash[String, bool] + def self.factory:( + options: Hash[Symbol | String, Array[String]], + ?listener: Proc + ) -> Proc - def initialize: -> void + def initialize: ( + options: Hash[Symbol | String, Array[String]], + listener: Proc + ) -> void - def freeze: -> BCDD::Result::Config::ConstantAlias + def freeze: -> BCDD::Result::Config::Switcher - def options: -> Hash[String, Hash[Symbol, untyped]] + def options: -> Hash[Symbol | String, Hash[Symbol, untyped]] - def enabled?: (String) -> bool + def enabled?: (Symbol | String) -> bool - def enable!: (*String) -> Hash[String, Hash[Symbol, untyped]] + def enable!: (*(Symbol | String)) -> Hash[Symbol | String, Hash[Symbol, untyped]] - def disable!: (*String) -> Hash[String, Hash[Symbol, untyped]] + def disable!: (*(Symbol | String)) -> Hash[Symbol | String, Hash[Symbol, untyped]] private - def set_many: (Array[String], to: bool) -> Hash[String, Hash[Symbol, untyped]] + def set_many: (Array[Symbol | String], to: bool) -> Hash[Symbol | String, Hash[Symbol, untyped]] - def set_one: (String, bool) -> void + def set_one: (Symbol | String, bool) -> void - def set_one!: (bool, target: untyped, name: Symbol, value: untyped) -> bool + def require_option!: (Array[Symbol | String]) -> void - def option_required!: (Array[String]) -> void + def validate_option!: (Symbol | String) -> void - def option_invalid!: (String) -> void + def available_options_message: -> String end -class BCDD::Result::Config::PatternMatching - module Option - NIL_AS_VALID_VALUE_CHECKING: Symbol - - LIST: Array[Symbol] - AFFECTS: Hash[Symbol, Array[String]] - end - - AVAILABLE_OPTIONS: String - - private attr_reader _options: Hash[Symbol, bool] - - def initialize: -> void - - def freeze: -> BCDD::Result::Config::PatternMatching - - def options: -> Hash[Symbol, Hash[Symbol, untyped]] - - def enabled?: (Symbol) -> bool - - def enable!: (*Symbol) -> Hash[Symbol, Hash[Symbol, untyped]] - - def disable!: (*Symbol) -> Hash[Symbol, Hash[Symbol, untyped]] - - private - - def set_many: (Array[Symbol], to: bool) -> Hash[Symbol, Hash[Symbol, untyped]] - - def set_one: (Symbol, bool) -> void - - def option_required!: (Array[Symbol]) -> void +module BCDD::Result::Config::ConstantAlias + RESULT: String - def option_invalid!: (Symbol) -> void + OPTIONS: Hash[String, Array[String]] + MAPPING: Hash[String, Hash[Symbol, untyped]] + Listener: Proc end From bbb4cabc1fb18511435103ce70f16bcf8d3aa49a Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Tue, 7 Nov 2023 08:06:56 -0300 Subject: [PATCH 07/30] Replace BCDD::Result.config with BCDD::Result::Config --- CHANGELOG.md | 33 +++++++++++-------- lib/bcdd/result.rb | 8 ++--- lib/bcdd/result/config.rb | 15 +++++++++ lib/bcdd/result/config/switcher.rb | 4 +++ .../result/contract/for_types_and_values.rb | 2 +- lib/result.rb | 5 --- sig/bcdd/result.rbs | 9 +++-- .../nil_as_valid_value_checking_test.rb | 8 ++--- test/result_test.rb | 12 ------- 9 files changed, 51 insertions(+), 45 deletions(-) delete mode 100644 lib/result.rb delete mode 100644 test/result_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index ce27609..7d4bde7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,44 +2,49 @@ ### Added -- Add `BCDD::Result.config` +- Add `BCDD::Result::Config` - **Constant Aliases** ```ruby - BCDD::Result.config.constant_alias.options + BCDD::Result::Config.constant_alias.options - BCDD::Result.config.constant_alias.enabled?('Result') + BCDD::Result::Config.constant_alias.enabled?('Result') - BCDD::Result.config.constant_alias.enable!('Result') + BCDD::Result::Config.constant_alias.enable!('Result') - BCDD::Result.config.constant_alias.disable!('Result') + BCDD::Result::Config.constant_alias.disable!('Result') ``` - **Default Add-ons** ```ruby - BCDD::Result.config.pattern_matching.options + BCDD::Result::Config.pattern_matching.options - BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.enabled?(:nil_as_valid_value_checking) - BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking) - BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.disable!(:nil_as_valid_value_checking) ``` -- Add `BCDD::Result.configuration`. It freezes the configuration, disallowing methods that promote changes but allowing the query ones. You can use this feature to ensure integrity in your configuration. +- Add `BCDD::Result::configuration`. It freezes the configuration, disallowing methods that promote changes but allowing the query ones. You can use this feature to ensure integrity in your configuration. ```ruby - BCDD::Result.config.constant_alias.enabled?('Result') # false + BCDD::Result::Config.constant_alias.enabled?('Result') # false BCDD::Result.configuration do |config| config.constant_alias.enable!('Result') end - BCDD::Result.config.constant_alias.enabled?('Result') # true + BCDD::Result::Config.constant_alias.enabled?('Result') # true - BCDD::Result.config.constant_alias.disable!('Result') # raises FrozenError + BCDD::Result::Config.constant_alias.disable!('Result') # raises FrozenError ``` ### Changed -- **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking!` with `BCDD::Result.config`. +- **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking!` with `BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking)`. +- **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking?` with `BCDD::Result::Config.pattern_matching.enabled?(:nil_as_valid_value_checking)`. + +### Removed + +- **(BREAKING)** Remove the `lib/result` file. Now you can define `Result` as an alias for `BCDD::Result` using `BCDD::Result::Config.constant_alias.enable!('Result')`. ## [0.7.0] - 2023-10-27 diff --git a/lib/bcdd/result.rb b/lib/bcdd/result.rb index 42645b6..896f57b 100644 --- a/lib/bcdd/result.rb +++ b/lib/bcdd/result.rb @@ -21,14 +21,10 @@ class BCDD::Result private :unknown, :unknown=, :type_checker - def self.config - Config.instance - end - def self.configuration - yield(config) + yield(Config) - config.freeze + Config.freeze end def initialize(type:, value:, subject: nil, expectations: nil) diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index d7c56dc..c3e594b 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'singleton' +require 'forwardable' require_relative 'config/switcher' require_relative 'config/constant_alias' @@ -36,5 +37,19 @@ def freeze super end + + def self.freeze; instance.freeze; end + def self.constant_alias; instance.constant_alias; end + def self.pattern_matching; instance.pattern_matching; end + + def self.options + %i[constant_alias pattern_matching] + end + + def self.inspect + "#<#{name} options=#{options.inspect}>" + end + + private_class_method :instance end end diff --git a/lib/bcdd/result/config/switcher.rb b/lib/bcdd/result/config/switcher.rb index a7d7fc4..5eec29e 100644 --- a/lib/bcdd/result/config/switcher.rb +++ b/lib/bcdd/result/config/switcher.rb @@ -17,6 +17,10 @@ def initialize(options:, listener:) @listener = listener end + def inspect + "#<#{self.class.name} options=#{_options.inspect}>" + end + def freeze _options.freeze super diff --git a/lib/bcdd/result/contract/for_types_and_values.rb b/lib/bcdd/result/contract/for_types_and_values.rb index 44a659e..d69cc6d 100644 --- a/lib/bcdd/result/contract/for_types_and_values.rb +++ b/lib/bcdd/result/contract/for_types_and_values.rb @@ -39,7 +39,7 @@ def type_and_value!(data) private def nil_as_valid_value_checking? - Config.instance.pattern_matching.enabled?(:nil_as_valid_value_checking) + Config.pattern_matching.enabled?(:nil_as_valid_value_checking) end end end diff --git a/lib/result.rb b/lib/result.rb deleted file mode 100644 index 9723a63..0000000 --- a/lib/result.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -require_relative 'bcdd/result' - -BCDD::Result.config.constant_alias.enable!('Result') diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index c3d20ef..bbef38d 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -11,9 +11,7 @@ class BCDD::Result attr_reader data: BCDD::Result::Data attr_reader subject: untyped - def self.config: -> BCDD::Result::Config - - def self.configuration: { (BCDD::Result::Config) -> void } -> BCDD::Result::Config + def self.configuration: { (singleton(BCDD::Result::Config)) -> void } -> BCDD::Result::Config def initialize: ( type: Symbol, @@ -501,6 +499,11 @@ class BCDD::Result::Config def initialize: -> void def freeze: -> BCDD::Result::Config + + def self.freeze: -> BCDD::Result::Config + def self.constant_alias: -> BCDD::Result::Config::Switcher + def self.pattern_matching: -> BCDD::Result::Config::Switcher + def self.options: -> Array[Symbol] end class BCDD::Result::Config::Switcher diff --git a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb index 1c31e92..c68947e 100644 --- a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb +++ b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb @@ -19,14 +19,14 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test _Result::Success(:ok, 1) end - BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking) result = _Result::Success(:ok, 1) assert result.success?(:ok) assert_equal(1, result.value) ensure - BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.disable!(:nil_as_valid_value_checking) end test 'BCDD::Result::Context::Expectations' do @@ -44,14 +44,14 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test _Result::Success(:ok, number: 1) end - BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking) result = _Result::Success(:ok, number: 1) assert result.success?(:ok) assert_equal({ number: 1 }, result.value) ensure - BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) + BCDD::Result::Config.pattern_matching.disable!(:nil_as_valid_value_checking) end end end diff --git a/test/result_test.rb b/test/result_test.rb deleted file mode 100644 index 3d80d62..0000000 --- a/test/result_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' -require 'result' - -class ResultTest < Minitest::Test - test 'Result is defined' do - assert defined?(Result) - - assert_same BCDD::Result, Result - end -end From 9e4989f8652d3ae903f870f846ecb1db7df9aa7d Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Tue, 7 Nov 2023 08:17:53 -0300 Subject: [PATCH 08/30] Replace `mixin(with:)` with `mixin(config:)` keyword argument --- CHANGELOG.md | 3 +++ README.md | 10 +++++----- lib/bcdd/result/context/expectations.rb | 4 ++-- lib/bcdd/result/context/expectations/mixin.rb | 2 +- lib/bcdd/result/context/mixin.rb | 6 +++--- lib/bcdd/result/contract/for_types_and_values.rb | 2 +- lib/bcdd/result/expectations.rb | 4 ++-- lib/bcdd/result/expectations/mixin.rb | 2 +- lib/bcdd/result/mixin.rb | 6 +++--- sig/bcdd/result.rbs | 8 ++++---- .../and_then/with_subject/continue_instance_test.rb | 2 +- .../and_then/with_subject/continue_singleton_test.rb | 2 +- .../and_then/with_subject/continue_instance_test.rb | 2 +- .../and_then/with_subject/continue_singleton_test.rb | 2 +- .../context/expectations/with_subject/continue_test.rb | 6 +++--- .../result/expectations/with_subject/continue_test.rb | 6 +++--- .../result_mixin/singleton_test.rb | 2 +- 17 files changed, 36 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4bde7..12fab19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,8 +40,11 @@ ### Changed - **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking!` with `BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking)`. + - **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking?` with `BCDD::Result::Config.pattern_matching.enabled?(:nil_as_valid_value_checking)`. +- **(BREAKING)** Replace `mixin(with:)` with `mixin(config:)` keyword argument. + ### Removed - **(BREAKING)** Remove the `lib/result` file. Now you can define `Result` as an alias for `BCDD::Result` using `BCDD::Result::Config.constant_alias.enable!('Result')`. diff --git a/README.md b/README.md index 3f809e7..6ae5eaa 100644 --- a/README.md +++ b/README.md @@ -838,7 +838,7 @@ This addon will create the `Continue(value)` method, which will know how to prod ```ruby module Divide - extend self, BCDD::Result.mixin(with: :Continue) + extend self, BCDD::Result.mixin(config: :continue) def call(arg1, arg2) validate_numbers(arg1, arg2) @@ -1274,12 +1274,12 @@ The `BCDD::Result::Expectations.mixin` also accepts the `with:` argument. It is **Continue** -It is similar to `BCDD::Result.mixin(with: :Continue)`, the key difference is that the `Continue(value)` will be ignored by the expectations. This is extremely useful when you want to use `Continue(value)` to chain operations, but you don't want to declare N success types in the expectations. +It is similar to `BCDD::Result.mixin(config: :continue)`, the key difference is that the `Continue(value)` will be ignored by the expectations. This is extremely useful when you want to use `Continue(value)` to chain operations, but you don't want to declare N success types in the expectations. ```ruby class Divide include BCDD::Result::Expectations.mixin( - with: :Continue, + config: :continue, success: :division_completed, failure: %i[invalid_arg division_by_zero] ) @@ -1581,7 +1581,7 @@ The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin **Continue** -The `BCDD::Result::Context.mixin(with: :Continue)` or `BCDD::Result::Context::Expectations.mixin(with: :Continue)` adds a `Continue(**input)` that will be ignored by the expectations. This is extremely useful when you want to use `Continue()` to chain operations, but you don't want to declare N success types in the expectations. +The `BCDD::Result::Context.mixin(config: :continue)` or `BCDD::Result::Context::Expectations.mixin(config: :continue)` adds a `Continue(**input)` that will be ignored by the expectations. This is extremely useful when you want to use `Continue()` to chain operations, but you don't want to declare N success types in the expectations. Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on: @@ -1596,7 +1596,7 @@ module Divide require 'logger' extend self, BCDD::Result::Context::Expectations.mixin( - with: :Continue, + config: :continue, success: { division_completed: ->(value) { value => { number: Numeric } } }, diff --git a/lib/bcdd/result/context/expectations.rb b/lib/bcdd/result/context/expectations.rb index 9c189ed..a6d844a 100644 --- a/lib/bcdd/result/context/expectations.rb +++ b/lib/bcdd/result/context/expectations.rb @@ -4,8 +4,8 @@ class BCDD::Result::Context class Expectations require_relative 'expectations/mixin' - def self.mixin(success: nil, failure: nil, with: nil) - addons = Mixin::Addons.options(with) + def self.mixin(success: nil, failure: nil, config: nil) + addons = Mixin::Addons.options(config) mod = ::BCDD::Result::Expectations::Mixin.module! mod.const_set(:Result, new(success: success, failure: failure).freeze) diff --git a/lib/bcdd/result/context/expectations/mixin.rb b/lib/bcdd/result/context/expectations/mixin.rb index f2f3572..ea189f5 100644 --- a/lib/bcdd/result/context/expectations/mixin.rb +++ b/lib/bcdd/result/context/expectations/mixin.rb @@ -25,7 +25,7 @@ module Continuable end end - OPTIONS = { Continue: Continuable }.freeze + OPTIONS = { continue: Continuable }.freeze def self.options(names) Array(names).filter_map { |name| OPTIONS[name] } diff --git a/lib/bcdd/result/context/mixin.rb b/lib/bcdd/result/context/mixin.rb index 34d404f..9bf9f37 100644 --- a/lib/bcdd/result/context/mixin.rb +++ b/lib/bcdd/result/context/mixin.rb @@ -19,7 +19,7 @@ module Continuable end end - OPTIONS = { Continue: Continuable }.freeze + OPTIONS = { continue: Continuable }.freeze def self.options(names) Array(names).filter_map { |name| OPTIONS[name] } @@ -27,8 +27,8 @@ def self.options(names) end end - def self.mixin(with: nil) - addons = Mixin::Addons.options(with) + def self.mixin(config: nil) + addons = Mixin::Addons.options(config) mod = ::BCDD::Result::Mixin.module! mod.send(:include, Mixin::Methods) diff --git a/lib/bcdd/result/contract/for_types_and_values.rb b/lib/bcdd/result/contract/for_types_and_values.rb index d69cc6d..4549f30 100644 --- a/lib/bcdd/result/contract/for_types_and_values.rb +++ b/lib/bcdd/result/contract/for_types_and_values.rb @@ -29,7 +29,7 @@ def type_and_value!(data) checking_result = value_checking === value - return value if checking_result || (nil_as_valid_value_checking? && checking_result.nil?) + return value if checking_result || (checking_result.nil? && nil_as_valid_value_checking?) raise Contract::Error::UnexpectedValue.build(type: type, value: value) rescue ::NoMatchingPatternError => e diff --git a/lib/bcdd/result/expectations.rb b/lib/bcdd/result/expectations.rb index 28f9d57..ccfa563 100644 --- a/lib/bcdd/result/expectations.rb +++ b/lib/bcdd/result/expectations.rb @@ -4,8 +4,8 @@ class BCDD::Result class Expectations require_relative 'expectations/mixin' - def self.mixin(success: nil, failure: nil, with: nil) - addons = Mixin::Addons.options(with) + def self.mixin(success: nil, failure: nil, config: nil) + addons = Mixin::Addons.options(config) mod = Mixin.module! mod.const_set(:Result, new(success: success, failure: failure).freeze) diff --git a/lib/bcdd/result/expectations/mixin.rb b/lib/bcdd/result/expectations/mixin.rb index 63d765b..173e4e0 100644 --- a/lib/bcdd/result/expectations/mixin.rb +++ b/lib/bcdd/result/expectations/mixin.rb @@ -25,7 +25,7 @@ module Continuable end end - OPTIONS = { Continue: Continuable }.freeze + OPTIONS = { continue: Continuable }.freeze def self.options(names) Array(names).filter_map { |name| OPTIONS[name] } diff --git a/lib/bcdd/result/mixin.rb b/lib/bcdd/result/mixin.rb index ba46f99..efcd5f5 100644 --- a/lib/bcdd/result/mixin.rb +++ b/lib/bcdd/result/mixin.rb @@ -19,7 +19,7 @@ module Continuable end end - OPTIONS = { Continue: Continuable }.freeze + OPTIONS = { continue: Continuable }.freeze def self.options(names) Array(names).filter_map { |name| OPTIONS[name] } @@ -34,8 +34,8 @@ def self.extended(base); base.const_set(:ResultMixin, self); end end end - def self.mixin(with: nil) - addons = Mixin::Addons.options(with) + def self.mixin(config: nil) + addons = Mixin::Addons.options(config) mod = Mixin.module! mod.send(:include, Mixin::Methods) diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index bbef38d..d05667f 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -119,7 +119,7 @@ class BCDD::Result def self.module!: -> Module end - def self.mixin: (?with: Array[Symbol]) -> Module + def self.mixin: (?config: Array[Symbol]) -> Module end class BCDD::Result @@ -337,7 +337,7 @@ end class BCDD::Result::Expectations def self.mixin: ( - ?with: Symbol, + ?config: Symbol, ?success: Hash[Symbol, untyped] | Array[Symbol], ?failure: Hash[Symbol, untyped] | Array[Symbol] ) -> Module @@ -443,12 +443,12 @@ class BCDD::Result::Context end end - def self.mixin: (?with: Array[Symbol]) -> Module + def self.mixin: (?config: Array[Symbol]) -> Module end class BCDD::Result::Context::Expectations def self.mixin: ( - ?with: Symbol, + ?config: Symbol, ?success: Hash[Symbol, untyped] | Array[Symbol], ?failure: Hash[Symbol, untyped] | Array[Symbol] ) -> Module diff --git a/test/bcdd/result/and_then/with_subject/continue_instance_test.rb b/test/bcdd/result/and_then/with_subject/continue_instance_test.rb index e23d0f9..2a3c747 100644 --- a/test/bcdd/result/and_then/with_subject/continue_instance_test.rb +++ b/test/bcdd/result/and_then/with_subject/continue_instance_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::AndThenWithSubjectContinueInstanceTest < Minitest::Test class Divide - include BCDD::Result.mixin(with: :Continue) + include BCDD::Result.mixin(config: :continue) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb b/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb index 89d5619..5de8c9e 100644 --- a/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb +++ b/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::AndThenWithSubjectContinueSingletonTest < Minitest::Test module Divide - extend self, BCDD::Result.mixin(with: :Continue) + extend self, BCDD::Result.mixin(config: :continue) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb b/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb index 36b8a4c..ed8afc6 100644 --- a/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb +++ b/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::Context::AndThenWithSubjectContinueInstanceTest < Minitest::Test class Divide - include BCDD::Result::Context.mixin(with: :Continue) + include BCDD::Result::Context.mixin(config: :continue) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb b/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb index 7f33683..3d129d6 100644 --- a/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb +++ b/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::Context::AndThenWithSubjectContinueSingletonTest < Minitest::Test module Divide - extend self, BCDD::Result::Context.mixin(with: :Continue) + extend self, BCDD::Result::Context.mixin(config: :continue) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/context/expectations/with_subject/continue_test.rb b/test/bcdd/result/context/expectations/with_subject/continue_test.rb index e5b6479..a8ba554 100644 --- a/test/bcdd/result/context/expectations/with_subject/continue_test.rb +++ b/test/bcdd/result/context/expectations/with_subject/continue_test.rb @@ -5,7 +5,7 @@ class BCDD::Result::Context::ExpectationsWithSubjectSuccessTypeTest < Minitest::Test class DivideType include BCDD::Result::Context::Expectations.mixin( - with: :Continue, + config: :continue, success: :ok, failure: :err ) @@ -36,7 +36,7 @@ def divide(number1:, number2:) class DivideTypes include BCDD::Result::Context::Expectations.mixin( - with: :Continue, + config: :continue, success: :division_completed, failure: %i[invalid_arg division_by_zero] ) @@ -67,7 +67,7 @@ def divide(number1:, number2:) module DivideTypeAndValue extend self, BCDD::Result::Context::Expectations.mixin( - with: :Continue, + config: :continue, success: { division_completed: ->(value) { case value diff --git a/test/bcdd/result/expectations/with_subject/continue_test.rb b/test/bcdd/result/expectations/with_subject/continue_test.rb index 698787e..1247cfe 100644 --- a/test/bcdd/result/expectations/with_subject/continue_test.rb +++ b/test/bcdd/result/expectations/with_subject/continue_test.rb @@ -5,7 +5,7 @@ class BCDD::Result::ExpectationsWithSubjectSuccessTypeTest < Minitest::Test class DivideType include BCDD::Result::Expectations.mixin( - with: :Continue, + config: :continue, success: :ok, failure: :err ) @@ -38,7 +38,7 @@ def divide((number1, number2)) class DivideTypes include BCDD::Result::Expectations.mixin( - with: :Continue, + config: :continue, success: :division_completed, failure: %i[invalid_arg division_by_zero] ) @@ -71,7 +71,7 @@ def divide((number1, number2)) module DivideTypeAndValue extend self, BCDD::Result::Expectations.mixin( - with: :Continue, + config: :continue, success: { division_completed: Numeric }, failure: { invalid_arg: String, division_by_zero: String } ) diff --git a/test/railway_oriented_programming/result_mixin/singleton_test.rb b/test/railway_oriented_programming/result_mixin/singleton_test.rb index 254c07e..f388d56 100644 --- a/test/railway_oriented_programming/result_mixin/singleton_test.rb +++ b/test/railway_oriented_programming/result_mixin/singleton_test.rb @@ -4,7 +4,7 @@ class BCDD::RailwayOrientedProgrammingResultMixinSingletonTest < Minitest::Test module Divide - extend self, BCDD::Result.mixin(with: :Continue) + extend self, BCDD::Result.mixin(config: :continue) def call(arg1, arg2) validate_numbers(arg1, arg2) From 37d68322ee2065a951a538640c425509bf2f7906 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Tue, 7 Nov 2023 08:38:26 -0300 Subject: [PATCH 09/30] Refactor expectations --- lib/bcdd/result/context/expectations.rb | 28 +++-------------- lib/bcdd/result/context/expectations/mixin.rb | 16 ++-------- lib/bcdd/result/expectations.rb | 12 +++++-- lib/bcdd/result/expectations/mixin.rb | 16 +++++----- sig/bcdd/result.rbs | 31 ++++++------------- 5 files changed, 33 insertions(+), 70 deletions(-) diff --git a/lib/bcdd/result/context/expectations.rb b/lib/bcdd/result/context/expectations.rb index a6d844a..cf294ae 100644 --- a/lib/bcdd/result/context/expectations.rb +++ b/lib/bcdd/result/context/expectations.rb @@ -1,26 +1,14 @@ # frozen_string_literal: true class BCDD::Result::Context - class Expectations + class Expectations < BCDD::Result::Expectations require_relative 'expectations/mixin' - def self.mixin(success: nil, failure: nil, config: nil) - addons = Mixin::Addons.options(config) - - mod = ::BCDD::Result::Expectations::Mixin.module! - mod.const_set(:Result, new(success: success, failure: failure).freeze) - mod.module_eval(Mixin::METHODS) - mod.send(:include, *addons) unless addons.empty? - mod + def self.mixin_module + Mixin end - def initialize(subject: nil, success: nil, failure: nil, contract: nil) - @subject = subject - - @contract = contract if contract.is_a?(::BCDD::Result::Contract::Evaluator) - - @contract ||= ::BCDD::Result::Contract.new(success: success, failure: failure).freeze - end + private_class_method :mixin_module def Success(type, **value) Success.new(type: type, value: value, subject: subject, expectations: contract) @@ -29,13 +17,5 @@ def Success(type, **value) def Failure(type, **value) Failure.new(type: type, value: value, subject: subject, expectations: contract) end - - def with(subject:) - self.class.new(subject: subject, contract: contract) - end - - private - - attr_reader :subject, :contract end end diff --git a/lib/bcdd/result/context/expectations/mixin.rb b/lib/bcdd/result/context/expectations/mixin.rb index ea189f5..aa0f67b 100644 --- a/lib/bcdd/result/context/expectations/mixin.rb +++ b/lib/bcdd/result/context/expectations/mixin.rb @@ -2,21 +2,9 @@ class BCDD::Result::Context module Expectations::Mixin - METHODS = <<~RUBY - def Success(...) - _Result::Success(...) - end - - def Failure(...) - _Result::Failure(...) - end + Factory = BCDD::Result::Expectations::Mixin::Factory - private - - def _Result - @_Result ||= Result.with(subject: self) - end - RUBY + METHODS = BCDD::Result::Expectations::Mixin::METHODS module Addons module Continuable diff --git a/lib/bcdd/result/expectations.rb b/lib/bcdd/result/expectations.rb index ccfa563..082b340 100644 --- a/lib/bcdd/result/expectations.rb +++ b/lib/bcdd/result/expectations.rb @@ -5,15 +5,21 @@ class Expectations require_relative 'expectations/mixin' def self.mixin(success: nil, failure: nil, config: nil) - addons = Mixin::Addons.options(config) + addons = mixin_module::Addons.options(config) - mod = Mixin.module! + mod = mixin_module::Factory.module! mod.const_set(:Result, new(success: success, failure: failure).freeze) - mod.module_eval(Mixin::METHODS) + mod.module_eval(mixin_module::METHODS) mod.send(:include, *addons) unless addons.empty? mod end + def self.mixin_module + Mixin + end + + private_class_method :mixin_module + def initialize(subject: nil, success: nil, failure: nil, contract: nil) @subject = subject diff --git a/lib/bcdd/result/expectations/mixin.rb b/lib/bcdd/result/expectations/mixin.rb index 173e4e0..0cab1bf 100644 --- a/lib/bcdd/result/expectations/mixin.rb +++ b/lib/bcdd/result/expectations/mixin.rb @@ -2,6 +2,15 @@ class BCDD::Result module Expectations::Mixin + module Factory + def self.module! + ::Module.new do + def self.included(base); base.const_set(:ResultExpectationsMixin, self); end + def self.extended(base); base.const_set(:ResultExpectationsMixin, self); end + end + end + end + METHODS = <<~RUBY def Success(...) _Result.Success(...) @@ -31,12 +40,5 @@ def self.options(names) Array(names).filter_map { |name| OPTIONS[name] } end end - - def self.module! - ::Module.new do - def self.included(base); base.const_set(:ResultExpectationsMixin, self); end - def self.extended(base); base.const_set(:ResultExpectationsMixin, self); end - end - end end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index d05667f..02b94cd 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -342,6 +342,8 @@ class BCDD::Result::Expectations ?failure: Hash[Symbol, untyped] | Array[Symbol] ) -> Module + def self.mixin_module: -> singleton(BCDD::Result::Expectations::Mixin) + def initialize: ( ?subject: untyped, ?success: Hash[Symbol, untyped] | Array[Symbol], @@ -361,6 +363,10 @@ class BCDD::Result::Expectations end module BCDD::Result::Expectations::Mixin + module Factory + def self.module!: -> Module + end + METHODS: String module Addons @@ -372,8 +378,6 @@ module BCDD::Result::Expectations::Mixin def self.options: (Symbol) -> Array[Module] end - - def self.module!: -> Module end class BCDD::Result::Context < BCDD::Result @@ -446,33 +450,16 @@ class BCDD::Result::Context def self.mixin: (?config: Array[Symbol]) -> Module end -class BCDD::Result::Context::Expectations - def self.mixin: ( - ?config: Symbol, - ?success: Hash[Symbol, untyped] | Array[Symbol], - ?failure: Hash[Symbol, untyped] | Array[Symbol] - ) -> Module - - def initialize: ( - ?subject: untyped, - ?success: Hash[Symbol, untyped] | Array[Symbol], - ?failure: Hash[Symbol, untyped] | Array[Symbol], - ?contract: BCDD::Result::Contract::Evaluator - ) -> void +class BCDD::Result::Context::Expectations < BCDD::Result::Expectations + def self.mixin_module: -> singleton(BCDD::Result::Context::Expectations::Mixin) def Success: (Symbol, **untyped) -> BCDD::Result::Context::Success def Failure: (Symbol, **untyped) -> BCDD::Result::Context::Failure - - def with: (subject: untyped) -> BCDD::Result::Context::Expectations - - private - - attr_reader subject: untyped - attr_reader contract: BCDD::Result::Contract::Evaluator end module BCDD::Result::Context::Expectations::Mixin METHODS: String + Factory: singleton(BCDD::Result::Expectations::Mixin::Factory) module Addons module Continuable From 65c80aea56d3b943df1a0aec1fc2eeaaf28aab6f Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Tue, 7 Nov 2023 08:48:43 -0300 Subject: [PATCH 10/30] Refactor result and result context mixins --- lib/bcdd/result/context/mixin.rb | 13 ++++++------- lib/bcdd/result/mixin.rb | 28 ++++++++++++++++++---------- sig/bcdd/result.rbs | 12 +++++++++--- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/lib/bcdd/result/context/mixin.rb b/lib/bcdd/result/context/mixin.rb index 9bf9f37..69e317e 100644 --- a/lib/bcdd/result/context/mixin.rb +++ b/lib/bcdd/result/context/mixin.rb @@ -2,6 +2,8 @@ class BCDD::Result::Context module Mixin + Factory = BCDD::Result::Mixin::Factory + module Methods def Success(type, **value) Success.new(type: type, value: value, subject: self) @@ -27,12 +29,9 @@ def self.options(names) end end - def self.mixin(config: nil) - addons = Mixin::Addons.options(config) - - mod = ::BCDD::Result::Mixin.module! - mod.send(:include, Mixin::Methods) - mod.send(:include, *addons) unless addons.empty? - mod + def self.mixin_module + Mixin end + + private_class_method :mixin_module end diff --git a/lib/bcdd/result/mixin.rb b/lib/bcdd/result/mixin.rb index efcd5f5..acee0f2 100644 --- a/lib/bcdd/result/mixin.rb +++ b/lib/bcdd/result/mixin.rb @@ -2,6 +2,15 @@ class BCDD::Result module Mixin + module Factory + def self.module! + ::Module.new do + def self.included(base); base.const_set(:ResultMixin, self); end + def self.extended(base); base.const_set(:ResultMixin, self); end + end + end + end + module Methods def Success(type, value = nil) Success.new(type: type, value: value, subject: self) @@ -25,21 +34,20 @@ def self.options(names) Array(names).filter_map { |name| OPTIONS[name] } end end - - def self.module! - ::Module.new do - def self.included(base); base.const_set(:ResultMixin, self); end - def self.extended(base); base.const_set(:ResultMixin, self); end - end - end end def self.mixin(config: nil) - addons = Mixin::Addons.options(config) + addons = mixin_module::Addons.options(config) - mod = Mixin.module! - mod.send(:include, Mixin::Methods) + mod = mixin_module::Factory.module! + mod.send(:include, mixin_module::Methods) mod.send(:include, *addons) unless addons.empty? mod end + + def self.mixin_module + Mixin + end + + private_class_method :mixin_module end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 02b94cd..8a665a6 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -96,6 +96,10 @@ end class BCDD::Result module Mixin + module Factory + def self.module!: -> Module + end + module Methods def Success: (Symbol type, ?untyped value) -> BCDD::Result::Success @@ -115,11 +119,11 @@ class BCDD::Result def self.options: (Array[Symbol]) -> Array[Module] end - - def self.module!: -> Module end def self.mixin: (?config: Array[Symbol]) -> Module + + def self.mixin_module: -> singleton(BCDD::Result::Mixin) end class BCDD::Result @@ -426,6 +430,8 @@ end class BCDD::Result::Context module Mixin + Factory: singleton(BCDD::Result::Mixin::Factory) + module Methods def Success: (Symbol, **untyped) -> BCDD::Result::Context::Success @@ -447,7 +453,7 @@ class BCDD::Result::Context end end - def self.mixin: (?config: Array[Symbol]) -> Module + def self.mixin_module: -> singleton(BCDD::Result::Context::Mixin) end class BCDD::Result::Context::Expectations < BCDD::Result::Expectations From 1c137f123ec0a467ae9cc8131aedc3f07bbfdfee Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Tue, 7 Nov 2023 09:29:37 -0300 Subject: [PATCH 11/30] Allow using a hash to configure addons --- CHANGELOG.md | 14 ++++++++++++++ lib/bcdd/result.rb | 2 +- lib/bcdd/result/config.rb | 2 +- lib/bcdd/result/config/options.rb | 13 +++++++++++++ lib/bcdd/result/context/expectations/mixin.rb | 2 +- lib/bcdd/result/context/mixin.rb | 2 +- lib/bcdd/result/expectations/mixin.rb | 2 +- lib/bcdd/result/mixin.rb | 2 +- sig/bcdd/result.rbs | 4 ++++ 9 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 lib/bcdd/result/config/options.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 12fab19..99ba33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,20 @@ - **(BREAKING)** Replace `mixin(with:)` with `mixin(config:)` keyword argument. +- **(BREAKING)** Change the addons definition. + - **From** + ```ruby + BCDD::Result.mixin(with: :Continue) + BCDD::Result.mixin(with: [:Continue]) + ``` + - **To** + ```ruby + BCDD::Result.mixin(with: :continue) + BCDD::Result.mixin(with: [:continue]) + BCDD::Result.mixin(with: { continue: true }) + ``` + - These examples are valid to all kinds of mixins (`BCDD::Result.mixin`, `BCDD::Result::Context.mixin`, `BCDD::Result::Expectations.mixin`, `BCDD::Result::Context::Expectations.mixin`) + ### Removed - **(BREAKING)** Remove the `lib/result` file. Now you can define `Result` as an alias for `BCDD::Result` using `BCDD::Result::Config.constant_alias.enable!('Result')`. diff --git a/lib/bcdd/result.rb b/lib/bcdd/result.rb index 896f57b..25a18d4 100644 --- a/lib/bcdd/result.rb +++ b/lib/bcdd/result.rb @@ -3,6 +3,7 @@ require_relative 'result/version' require_relative 'result/error' require_relative 'result/data' +require_relative 'result/config' require_relative 'result/handler' require_relative 'result/failure' require_relative 'result/success' @@ -10,7 +11,6 @@ require_relative 'result/contract' require_relative 'result/expectations' require_relative 'result/context' -require_relative 'result/config' class BCDD::Result attr_accessor :unknown diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index c3e594b..1579dd0 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require 'singleton' -require 'forwardable' +require_relative 'config/options' require_relative 'config/switcher' require_relative 'config/constant_alias' diff --git a/lib/bcdd/result/config/options.rb b/lib/bcdd/result/config/options.rb new file mode 100644 index 0000000..5fecdc7 --- /dev/null +++ b/lib/bcdd/result/config/options.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class BCDD::Result + class Config + module Options + def self.filter(config, all_options) + config = Array(config).to_h { [_1, true] } unless config.is_a?(::Hash) + + config.filter_map { |name, boolean| all_options[name] if boolean } + end + end + end +end diff --git a/lib/bcdd/result/context/expectations/mixin.rb b/lib/bcdd/result/context/expectations/mixin.rb index aa0f67b..dea44c7 100644 --- a/lib/bcdd/result/context/expectations/mixin.rb +++ b/lib/bcdd/result/context/expectations/mixin.rb @@ -16,7 +16,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(names) - Array(names).filter_map { |name| OPTIONS[name] } + ::BCDD::Result::Config::Options.filter(names, OPTIONS) end end end diff --git a/lib/bcdd/result/context/mixin.rb b/lib/bcdd/result/context/mixin.rb index 69e317e..1e55ecc 100644 --- a/lib/bcdd/result/context/mixin.rb +++ b/lib/bcdd/result/context/mixin.rb @@ -24,7 +24,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(names) - Array(names).filter_map { |name| OPTIONS[name] } + ::BCDD::Result::Config::Options.filter(names, OPTIONS) end end end diff --git a/lib/bcdd/result/expectations/mixin.rb b/lib/bcdd/result/expectations/mixin.rb index 0cab1bf..7bbfb10 100644 --- a/lib/bcdd/result/expectations/mixin.rb +++ b/lib/bcdd/result/expectations/mixin.rb @@ -37,7 +37,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(names) - Array(names).filter_map { |name| OPTIONS[name] } + Config::Options.filter(names, OPTIONS) end end end diff --git a/lib/bcdd/result/mixin.rb b/lib/bcdd/result/mixin.rb index acee0f2..1e18b47 100644 --- a/lib/bcdd/result/mixin.rb +++ b/lib/bcdd/result/mixin.rb @@ -31,7 +31,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(names) - Array(names).filter_map { |name| OPTIONS[name] } + Config::Options.filter(names, OPTIONS) end end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 8a665a6..f717ce3 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -544,3 +544,7 @@ module BCDD::Result::Config::ConstantAlias MAPPING: Hash[String, Hash[Symbol, untyped]] Listener: Proc end + +module BCDD::Result::Config::Options + def self.filter: (untyped, Hash[untyped, untyped]) -> Array[Module] +end From ba888a8bb06fa1058d817de8d99fa116fae20897 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 09:24:57 -0300 Subject: [PATCH 12/30] Put back BCDD::Result.config --- CHANGELOG.md | 24 +++++++++---------- lib/bcdd/result.rb | 8 +++++-- .../result/contract/for_types_and_values.rb | 2 +- sig/bcdd/result.rbs | 3 ++- .../nil_as_valid_value_checking_test.rb | 8 +++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ba33e..88b03ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,39 +2,39 @@ ### Added -- Add `BCDD::Result::Config` +- Add `BCDD::Result.config` - **Constant Aliases** ```ruby - BCDD::Result::Config.constant_alias.options + BCDD::Result.config.constant_alias.options - BCDD::Result::Config.constant_alias.enabled?('Result') + BCDD::Result.config.constant_alias.enabled?('Result') - BCDD::Result::Config.constant_alias.enable!('Result') + BCDD::Result.config.constant_alias.enable!('Result') - BCDD::Result::Config.constant_alias.disable!('Result') + BCDD::Result.config.constant_alias.disable!('Result') ``` - **Default Add-ons** ```ruby - BCDD::Result::Config.pattern_matching.options + BCDD::Result.config.pattern_matching.options - BCDD::Result::Config.pattern_matching.enabled?(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking) - BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - BCDD::Result::Config.pattern_matching.disable!(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) ``` - Add `BCDD::Result::configuration`. It freezes the configuration, disallowing methods that promote changes but allowing the query ones. You can use this feature to ensure integrity in your configuration. ```ruby - BCDD::Result::Config.constant_alias.enabled?('Result') # false + BCDD::Result.config.constant_alias.enabled?('Result') # false BCDD::Result.configuration do |config| config.constant_alias.enable!('Result') end - BCDD::Result::Config.constant_alias.enabled?('Result') # true + BCDD::Result.config.constant_alias.enabled?('Result') # true - BCDD::Result::Config.constant_alias.disable!('Result') # raises FrozenError + BCDD::Result.config.constant_alias.disable!('Result') # raises FrozenError ``` ### Changed diff --git a/lib/bcdd/result.rb b/lib/bcdd/result.rb index 25a18d4..47b3a4a 100644 --- a/lib/bcdd/result.rb +++ b/lib/bcdd/result.rb @@ -21,10 +21,14 @@ class BCDD::Result private :unknown, :unknown=, :type_checker + def self.config + Config.instance + end + def self.configuration - yield(Config) + yield(config) - Config.freeze + config.freeze end def initialize(type:, value:, subject: nil, expectations: nil) diff --git a/lib/bcdd/result/contract/for_types_and_values.rb b/lib/bcdd/result/contract/for_types_and_values.rb index 4549f30..39b3ced 100644 --- a/lib/bcdd/result/contract/for_types_and_values.rb +++ b/lib/bcdd/result/contract/for_types_and_values.rb @@ -39,7 +39,7 @@ def type_and_value!(data) private def nil_as_valid_value_checking? - Config.pattern_matching.enabled?(:nil_as_valid_value_checking) + Config.instance.pattern_matching.enabled?(:nil_as_valid_value_checking) end end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index f717ce3..68dc63d 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -11,7 +11,8 @@ class BCDD::Result attr_reader data: BCDD::Result::Data attr_reader subject: untyped - def self.configuration: { (singleton(BCDD::Result::Config)) -> void } -> BCDD::Result::Config + def self.config: -> BCDD::Result::Config + def self.configuration: { (BCDD::Result::Config) -> void } -> BCDD::Result::Config def initialize: ( type: Symbol, diff --git a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb index c68947e..1c31e92 100644 --- a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb +++ b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb @@ -19,14 +19,14 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test _Result::Success(:ok, 1) end - BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) result = _Result::Success(:ok, 1) assert result.success?(:ok) assert_equal(1, result.value) ensure - BCDD::Result::Config.pattern_matching.disable!(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) end test 'BCDD::Result::Context::Expectations' do @@ -44,14 +44,14 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test _Result::Success(:ok, number: 1) end - BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) result = _Result::Success(:ok, number: 1) assert result.success?(:ok) assert_equal({ number: 1 }, result.value) ensure - BCDD::Result::Config.pattern_matching.disable!(:nil_as_valid_value_checking) + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) end end end From 166b82da68f1cbf8737a1a7031c7215ac59fa681 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 09:30:02 -0300 Subject: [PATCH 13/30] Improve BCDD::Result::Config --- lib/bcdd/result/config.rb | 43 +++++++++++------------- lib/bcdd/result/config/constant_alias.rb | 6 +++- lib/bcdd/result/config/switcher.rb | 14 ++++---- sig/bcdd/result.rbs | 24 ++++++------- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index 1579dd0..75f1c96 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -10,46 +10,43 @@ class BCDD::Result class Config include Singleton - CONSTANT_ALIAS = Switcher.factory( - options: ConstantAlias::OPTIONS, - listener: ConstantAlias::Listener - ) - - PATTERN_MATCHING = Switcher.factory( - options: { - nil_as_valid_value_checking: %w[ - BCDD::Result::Expectations - BCDD::Result::Context::Expectations - ] + PATTERN_MATCHING = { + nil_as_valid_value_checking: { + default: false, + affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations] } - ) + }.transform_values!(&:freeze).freeze - attr_reader :constant_alias, :pattern_matching + attr_reader :addon, :constant_alias, :pattern_matching def initialize - @constant_alias = CONSTANT_ALIAS.call - @pattern_matching = PATTERN_MATCHING.call + @constant_alias = ConstantAlias.switcher + @pattern_matching = Switcher.new(options: PATTERN_MATCHING) end def freeze + addon.freeze constant_alias.freeze pattern_matching.freeze super end - def self.freeze; instance.freeze; end - def self.constant_alias; instance.constant_alias; end - def self.pattern_matching; instance.pattern_matching; end + def options + { + constant_alias: constant_alias, + pattern_matching: pattern_matching + } + end - def self.options - %i[constant_alias pattern_matching] + def to_h + options.transform_values(&:to_h) end - def self.inspect - "#<#{name} options=#{options.inspect}>" + def inspect + "#<#{self.class.name} options=#{options.keys.inspect}>" end - private_class_method :instance + private_constant :PATTERN_MATCHING end end diff --git a/lib/bcdd/result/config/constant_alias.rb b/lib/bcdd/result/config/constant_alias.rb index 6178d32..70f3fe2 100644 --- a/lib/bcdd/result/config/constant_alias.rb +++ b/lib/bcdd/result/config/constant_alias.rb @@ -6,7 +6,7 @@ module ConstantAlias RESULT = 'Result' OPTIONS = { - RESULT => %w[Object] + RESULT => { default: false, affects: %w[Object] } }.transform_values!(&:freeze).freeze MAPPING = { @@ -22,6 +22,10 @@ module ConstantAlias boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name) end + + def self.switcher + Switcher.new(options: OPTIONS, listener: Listener) + end end private_constant :ConstantAlias diff --git a/lib/bcdd/result/config/switcher.rb b/lib/bcdd/result/config/switcher.rb index 5eec29e..8db30c4 100644 --- a/lib/bcdd/result/config/switcher.rb +++ b/lib/bcdd/result/config/switcher.rb @@ -7,13 +7,9 @@ class Switcher private :_options, :_affects, :listener - def self.factory(options:, listener: nil) - -> { new(options: options, listener: listener) } - end - - def initialize(options:, listener:) - @_affects = options - @_options = options.keys.to_h { [_1, false] } + def initialize(options:, listener: nil) + @_options = options.transform_values { _1.fetch(:default) } + @_affects = options.transform_values { _1.fetch(:affects) } @listener = listener end @@ -26,6 +22,10 @@ def freeze super end + def to_h + _options.dup + end + def options _affects.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] } end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 68dc63d..086f20a 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -483,7 +483,7 @@ class BCDD::Result::Config include Singleton CONSTANT_ALIAS: Proc - PATTERN_MATCHING: Proc + PATTERN_MATCHING: Hash[Symbol, Hash[Symbol, untyped]] attr_reader constant_alias: BCDD::Result::Config::Switcher attr_reader pattern_matching: BCDD::Result::Config::Switcher @@ -493,11 +493,8 @@ class BCDD::Result::Config def initialize: -> void def freeze: -> BCDD::Result::Config - - def self.freeze: -> BCDD::Result::Config - def self.constant_alias: -> BCDD::Result::Config::Switcher - def self.pattern_matching: -> BCDD::Result::Config::Switcher - def self.options: -> Array[Symbol] + def options: -> Hash[Symbol, BCDD::Result::Config::Switcher] + def to_h: -> Hash[Symbol, Hash[Symbol | String, bool]] end class BCDD::Result::Config::Switcher @@ -505,18 +502,15 @@ class BCDD::Result::Config::Switcher private attr_reader _options: Hash[Symbol | String, bool] private attr_reader listener: Proc - def self.factory:( - options: Hash[Symbol | String, Array[String]], - ?listener: Proc - ) -> Proc - def initialize: ( - options: Hash[Symbol | String, Array[String]], - listener: Proc + options: Hash[Symbol | String, Hash[Symbol, untyped]], + ?listener: Proc ) -> void def freeze: -> BCDD::Result::Config::Switcher + def to_h: -> Hash[Symbol | String, bool] + def options: -> Hash[Symbol | String, Hash[Symbol, untyped]] def enabled?: (Symbol | String) -> bool @@ -541,9 +535,11 @@ end module BCDD::Result::Config::ConstantAlias RESULT: String - OPTIONS: Hash[String, Array[String]] + OPTIONS: Hash[String, Hash[Symbol, untyped]] MAPPING: Hash[String, Hash[Symbol, untyped]] Listener: Proc + + def self.switcher: -> BCDD::Result::Config::Switcher end module BCDD::Result::Config::Options From 3a159aff492ac9d423803fa9fc214ccc94dfce37 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 09:31:57 -0300 Subject: [PATCH 14/30] Add BCDD::Result::Config#addon --- lib/bcdd/result/config.rb | 11 ++++++++++- sig/bcdd/result.rbs | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index 75f1c96..aa9cd53 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -10,6 +10,13 @@ class BCDD::Result class Config include Singleton + ADDON = { + continue: { + default: false, + affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations] + } + }.transform_values!(&:freeze).freeze + PATTERN_MATCHING = { nil_as_valid_value_checking: { default: false, @@ -20,6 +27,7 @@ class Config attr_reader :addon, :constant_alias, :pattern_matching def initialize + @addon = Switcher.new(options: ADDON) @constant_alias = ConstantAlias.switcher @pattern_matching = Switcher.new(options: PATTERN_MATCHING) end @@ -34,6 +42,7 @@ def freeze def options { + addon: addon, constant_alias: constant_alias, pattern_matching: pattern_matching } @@ -47,6 +56,6 @@ def inspect "#<#{self.class.name} options=#{options.keys.inspect}>" end - private_constant :PATTERN_MATCHING + private_constant :ADDON, :PATTERN_MATCHING end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 086f20a..012288e 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -482,9 +482,10 @@ end class BCDD::Result::Config include Singleton - CONSTANT_ALIAS: Proc + ADDON: Hash[Symbol, Hash[Symbol, untyped]] PATTERN_MATCHING: Hash[Symbol, Hash[Symbol, untyped]] + attr_reader addon: BCDD::Result::Config::Switcher attr_reader constant_alias: BCDD::Result::Config::Switcher attr_reader pattern_matching: BCDD::Result::Config::Switcher From 8689802c4e2da6949ca953a25298c272f7733dc1 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 09:33:29 -0300 Subject: [PATCH 15/30] Add BCDD::Config::Options --- lib/bcdd/result/config/options.rb | 30 +++++++++++++++++++++++++++--- sig/bcdd/result.rbs | 8 +++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/bcdd/result/config/options.rb b/lib/bcdd/result/config/options.rb index 5fecdc7..c6d21f0 100644 --- a/lib/bcdd/result/config/options.rb +++ b/lib/bcdd/result/config/options.rb @@ -3,10 +3,34 @@ class BCDD::Result class Config module Options - def self.filter(config, all_options) - config = Array(config).to_h { [_1, true] } unless config.is_a?(::Hash) + UnwrapValueFromOptions = ->(flags, options, config_category, (config_name, default_config)) do + flag_enabled = flags.dig(config_category, config_name) - config.filter_map { |name, boolean| all_options[name] if boolean } + options.dig(config_category, config_name) if flag_enabled.nil? ? default_config : flag_enabled + end.curry + + MapOptionsWithDefaults = ->(options) do + default_options = Config.instance.options + + defaults = default_options.slice(*options.keys).transform_values(&:to_h) + + return defaults unless defaults.empty? + + raise ArgumentError, "Invalid options: #{options.keys}. Valid ones: #{default_options.keys}" + end + + def self.unwrap(flags:, options:) + flags ||= {} + + flags.is_a?(::Hash) or raise ArgumentError, "expected a Hash[Symbol, bool], got #{flags.inspect}" + + options_with_defaults = MapOptionsWithDefaults[options] + + unwrap_value_from_options = UnwrapValueFromOptions[flags, options] + + options_with_defaults.flat_map do |config_category, default_configs| + default_configs.filter_map(&unwrap_value_from_options[config_category]) + end end end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 012288e..03de835 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -544,5 +544,11 @@ module BCDD::Result::Config::ConstantAlias end module BCDD::Result::Config::Options - def self.filter: (untyped, Hash[untyped, untyped]) -> Array[Module] + UnwrapValueFromOptions: Proc + MapOptionsWithDefaults: Proc + + def self.unwrap: ( + flags: Hash[Symbol, Hash[Symbol, bool]], + options: Hash[Symbol, Hash[Symbol, untyped]] + ) -> Array[untyped] end From 41e18fa75eb290629e27299a6cd467248ed142f9 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 09:34:56 -0300 Subject: [PATCH 16/30] Change addons definition (mixin + config) --- CHANGELOG.md | 4 +--- README.md | 18 +++++++++--------- lib/bcdd/result/context/expectations/mixin.rb | 4 ++-- lib/bcdd/result/context/mixin.rb | 4 ++-- lib/bcdd/result/expectations/mixin.rb | 4 ++-- lib/bcdd/result/mixin.rb | 4 ++-- sig/bcdd/result.rbs | 12 ++++++------ .../with_subject/continue_instance_test.rb | 2 +- .../with_subject/continue_singleton_test.rb | 2 +- .../with_subject/continue_instance_test.rb | 2 +- .../with_subject/continue_singleton_test.rb | 2 +- .../expectations/with_subject/continue_test.rb | 6 +++--- .../expectations/with_subject/continue_test.rb | 6 +++--- .../result_mixin/singleton_test.rb | 2 +- 14 files changed, 35 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b03ae..4e18149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,9 +53,7 @@ ``` - **To** ```ruby - BCDD::Result.mixin(with: :continue) - BCDD::Result.mixin(with: [:continue]) - BCDD::Result.mixin(with: { continue: true }) + BCDD::Result.mixin(config: { addon: { continue: true } }) ``` - These examples are valid to all kinds of mixins (`BCDD::Result.mixin`, `BCDD::Result::Context.mixin`, `BCDD::Result::Expectations.mixin`, `BCDD::Result::Context::Expectations.mixin`) diff --git a/README.md b/README.md index 6ae5eaa..1f57f0b 100644 --- a/README.md +++ b/README.md @@ -746,10 +746,10 @@ class Divide private def validate_numbers - arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric') # This will raise an error + arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric') # This will raise an error arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric') - BCDD::Result::Success(:ok, [arg1, arg2]) # This will raise an error + Success(:ok, [arg1, arg2]) # This will raise an error end def validate_non_zero(numbers) @@ -838,7 +838,7 @@ This addon will create the `Continue(value)` method, which will know how to prod ```ruby module Divide - extend self, BCDD::Result.mixin(config: :continue) + extend self, BCDD::Result.mixin(config: { addon: { continue: true } }) def call(arg1, arg2) validate_numbers(arg1, arg2) @@ -947,10 +947,10 @@ class Divide end ``` -This mode also defines an `Expected` constant to be used inside and outside the module. +This mode also defines an `Result` constant to be used inside and outside the module. > **PROTIP:** -> You can use the `Expected` constant to mock the result's type and value in your tests. As they will have the exact expectations, your tests will check if the result clients are handling the result correctly. +> You can use the `Result` constant to mock the result's type and value in your tests. As they will have the exact expectations, your tests will check if the result clients are handling the result correctly. Now that you know the two modes, let's understand how expectations can be beneficial and powerful for defining contracts. @@ -1274,12 +1274,12 @@ The `BCDD::Result::Expectations.mixin` also accepts the `with:` argument. It is **Continue** -It is similar to `BCDD::Result.mixin(config: :continue)`, the key difference is that the `Continue(value)` will be ignored by the expectations. This is extremely useful when you want to use `Continue(value)` to chain operations, but you don't want to declare N success types in the expectations. +It is similar to `BCDD::Result.mixin(config: { addon: { continue: true } })`, the key difference is that the `Continue(value)` will be ignored by the expectations. This is extremely useful when you want to use `Continue(value)` to chain operations, but you don't want to declare N success types in the expectations. ```ruby class Divide include BCDD::Result::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: :division_completed, failure: %i[invalid_arg division_by_zero] ) @@ -1581,7 +1581,7 @@ The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin **Continue** -The `BCDD::Result::Context.mixin(config: :continue)` or `BCDD::Result::Context::Expectations.mixin(config: :continue)` adds a `Continue(**input)` that will be ignored by the expectations. This is extremely useful when you want to use `Continue()` to chain operations, but you don't want to declare N success types in the expectations. +The `BCDD::Result::Context.mixin(config: { addon: { continue: true } })` or `BCDD::Result::Context::Expectations.mixin(config: { addon: { continue: true } })` adds a `Continue(**input)` that will be ignored by the expectations. This is extremely useful when you want to use `Continue()` to chain operations, but you don't want to declare N success types in the expectations. Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on: @@ -1596,7 +1596,7 @@ module Divide require 'logger' extend self, BCDD::Result::Context::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: { division_completed: ->(value) { value => { number: Numeric } } }, diff --git a/lib/bcdd/result/context/expectations/mixin.rb b/lib/bcdd/result/context/expectations/mixin.rb index dea44c7..9774d30 100644 --- a/lib/bcdd/result/context/expectations/mixin.rb +++ b/lib/bcdd/result/context/expectations/mixin.rb @@ -15,8 +15,8 @@ module Continuable OPTIONS = { continue: Continuable }.freeze - def self.options(names) - ::BCDD::Result::Config::Options.filter(names, OPTIONS) + def self.options(config_flags) + ::BCDD::Result::Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) end end end diff --git a/lib/bcdd/result/context/mixin.rb b/lib/bcdd/result/context/mixin.rb index 1e55ecc..6433aa7 100644 --- a/lib/bcdd/result/context/mixin.rb +++ b/lib/bcdd/result/context/mixin.rb @@ -23,8 +23,8 @@ module Continuable OPTIONS = { continue: Continuable }.freeze - def self.options(names) - ::BCDD::Result::Config::Options.filter(names, OPTIONS) + def self.options(config_flags) + ::BCDD::Result::Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) end end end diff --git a/lib/bcdd/result/expectations/mixin.rb b/lib/bcdd/result/expectations/mixin.rb index 7bbfb10..aff0d9b 100644 --- a/lib/bcdd/result/expectations/mixin.rb +++ b/lib/bcdd/result/expectations/mixin.rb @@ -36,8 +36,8 @@ module Continuable OPTIONS = { continue: Continuable }.freeze - def self.options(names) - Config::Options.filter(names, OPTIONS) + def self.options(config_flags) + Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) end end end diff --git a/lib/bcdd/result/mixin.rb b/lib/bcdd/result/mixin.rb index 1e18b47..9ed5394 100644 --- a/lib/bcdd/result/mixin.rb +++ b/lib/bcdd/result/mixin.rb @@ -30,8 +30,8 @@ module Continuable OPTIONS = { continue: Continuable }.freeze - def self.options(names) - Config::Options.filter(names, OPTIONS) + def self.options(config_flags) + Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) end end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 03de835..5e97f82 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -118,11 +118,11 @@ class BCDD::Result OPTIONS: Hash[Symbol, Module] - def self.options: (Array[Symbol]) -> Array[Module] + def self.options: (Hash[Symbol, Hash[Symbol, bool]]) -> Array[Module] end end - def self.mixin: (?config: Array[Symbol]) -> Module + def self.mixin: (?config: Hash[Symbol, Hash[Symbol, bool]]) -> Module def self.mixin_module: -> singleton(BCDD::Result::Mixin) end @@ -342,7 +342,7 @@ end class BCDD::Result::Expectations def self.mixin: ( - ?config: Symbol, + ?config: Hash[Symbol, Hash[Symbol, bool]], ?success: Hash[Symbol, untyped] | Array[Symbol], ?failure: Hash[Symbol, untyped] | Array[Symbol] ) -> Module @@ -381,7 +381,7 @@ module BCDD::Result::Expectations::Mixin OPTIONS: Hash[Symbol, Module] - def self.options: (Symbol) -> Array[Module] + def self.options: (Hash[Symbol, Hash[Symbol, bool]]) -> Array[Module] end end @@ -450,7 +450,7 @@ class BCDD::Result::Context OPTIONS: Hash[Symbol, Module] - def self.options: (Array[Symbol]) -> Array[Module] + def self.options: (Hash[Symbol, Hash[Symbol, bool]]) -> Array[Module] end end @@ -475,7 +475,7 @@ module BCDD::Result::Context::Expectations::Mixin OPTIONS: Hash[Symbol, Module] - def self.options: (Symbol) -> Array[Module] + def self.options: (Hash[Symbol, Hash[Symbol, bool]]) -> Array[Module] end end diff --git a/test/bcdd/result/and_then/with_subject/continue_instance_test.rb b/test/bcdd/result/and_then/with_subject/continue_instance_test.rb index 2a3c747..18e8c33 100644 --- a/test/bcdd/result/and_then/with_subject/continue_instance_test.rb +++ b/test/bcdd/result/and_then/with_subject/continue_instance_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::AndThenWithSubjectContinueInstanceTest < Minitest::Test class Divide - include BCDD::Result.mixin(config: :continue) + include BCDD::Result.mixin(config: { addon: { continue: true } }) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb b/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb index 5de8c9e..11a26e0 100644 --- a/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb +++ b/test/bcdd/result/and_then/with_subject/continue_singleton_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::AndThenWithSubjectContinueSingletonTest < Minitest::Test module Divide - extend self, BCDD::Result.mixin(config: :continue) + extend self, BCDD::Result.mixin(config: { addon: { continue: true } }) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb b/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb index ed8afc6..ed5a0b6 100644 --- a/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb +++ b/test/bcdd/result/context/and_then/with_subject/continue_instance_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::Context::AndThenWithSubjectContinueInstanceTest < Minitest::Test class Divide - include BCDD::Result::Context.mixin(config: :continue) + include BCDD::Result::Context.mixin(config: { addon: { continue: true } }) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb b/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb index 3d129d6..224e108 100644 --- a/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb +++ b/test/bcdd/result/context/and_then/with_subject/continue_singleton_test.rb @@ -4,7 +4,7 @@ class BCDD::Result::Context::AndThenWithSubjectContinueSingletonTest < Minitest::Test module Divide - extend self, BCDD::Result::Context.mixin(config: :continue) + extend self, BCDD::Result::Context.mixin(config: { addon: { continue: true } }) def call(arg1, arg2) validate_numbers(arg1, arg2) diff --git a/test/bcdd/result/context/expectations/with_subject/continue_test.rb b/test/bcdd/result/context/expectations/with_subject/continue_test.rb index a8ba554..73dc1e9 100644 --- a/test/bcdd/result/context/expectations/with_subject/continue_test.rb +++ b/test/bcdd/result/context/expectations/with_subject/continue_test.rb @@ -5,7 +5,7 @@ class BCDD::Result::Context::ExpectationsWithSubjectSuccessTypeTest < Minitest::Test class DivideType include BCDD::Result::Context::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: :ok, failure: :err ) @@ -36,7 +36,7 @@ def divide(number1:, number2:) class DivideTypes include BCDD::Result::Context::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: :division_completed, failure: %i[invalid_arg division_by_zero] ) @@ -67,7 +67,7 @@ def divide(number1:, number2:) module DivideTypeAndValue extend self, BCDD::Result::Context::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: { division_completed: ->(value) { case value diff --git a/test/bcdd/result/expectations/with_subject/continue_test.rb b/test/bcdd/result/expectations/with_subject/continue_test.rb index 1247cfe..a07a002 100644 --- a/test/bcdd/result/expectations/with_subject/continue_test.rb +++ b/test/bcdd/result/expectations/with_subject/continue_test.rb @@ -5,7 +5,7 @@ class BCDD::Result::ExpectationsWithSubjectSuccessTypeTest < Minitest::Test class DivideType include BCDD::Result::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: :ok, failure: :err ) @@ -38,7 +38,7 @@ def divide((number1, number2)) class DivideTypes include BCDD::Result::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: :division_completed, failure: %i[invalid_arg division_by_zero] ) @@ -71,7 +71,7 @@ def divide((number1, number2)) module DivideTypeAndValue extend self, BCDD::Result::Expectations.mixin( - config: :continue, + config: { addon: { continue: true } }, success: { division_completed: Numeric }, failure: { invalid_arg: String, division_by_zero: String } ) diff --git a/test/railway_oriented_programming/result_mixin/singleton_test.rb b/test/railway_oriented_programming/result_mixin/singleton_test.rb index f388d56..413fa75 100644 --- a/test/railway_oriented_programming/result_mixin/singleton_test.rb +++ b/test/railway_oriented_programming/result_mixin/singleton_test.rb @@ -4,7 +4,7 @@ class BCDD::RailwayOrientedProgrammingResultMixinSingletonTest < Minitest::Test module Divide - extend self, BCDD::Result.mixin(config: :continue) + extend self, BCDD::Result.mixin(config: { addon: { continue: true } }) def call(arg1, arg2) validate_numbers(arg1, arg2) From b879270d3c2c9f09d7dea5835699cce051eb2a74 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 10:17:33 -0300 Subject: [PATCH 17/30] Add BCDD::Result::Config#feature --- lib/bcdd/result/config.rb | 14 ++++++++++++-- sig/bcdd/result.rbs | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index aa9cd53..2e901eb 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -17,6 +17,13 @@ class Config } }.transform_values!(&:freeze).freeze + FEATURE = { + expectations: { + default: true, + affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations] + } + }.transform_values!(&:freeze).freeze + PATTERN_MATCHING = { nil_as_valid_value_checking: { default: false, @@ -24,16 +31,18 @@ class Config } }.transform_values!(&:freeze).freeze - attr_reader :addon, :constant_alias, :pattern_matching + attr_reader :addon, :feature, :constant_alias, :pattern_matching def initialize @addon = Switcher.new(options: ADDON) + @feature = Switcher.new(options: FEATURE) @constant_alias = ConstantAlias.switcher @pattern_matching = Switcher.new(options: PATTERN_MATCHING) end def freeze addon.freeze + feature.freeze constant_alias.freeze pattern_matching.freeze @@ -43,6 +52,7 @@ def freeze def options { addon: addon, + feature: feature, constant_alias: constant_alias, pattern_matching: pattern_matching } @@ -56,6 +66,6 @@ def inspect "#<#{self.class.name} options=#{options.keys.inspect}>" end - private_constant :ADDON, :PATTERN_MATCHING + private_constant :ADDON, :FEATURE, :PATTERN_MATCHING end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 5e97f82..72b5ff8 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -483,9 +483,11 @@ class BCDD::Result::Config include Singleton ADDON: Hash[Symbol, Hash[Symbol, untyped]] + FEATURE: Hash[Symbol, Hash[Symbol, untyped]] PATTERN_MATCHING: Hash[Symbol, Hash[Symbol, untyped]] attr_reader addon: BCDD::Result::Config::Switcher + attr_reader feature: BCDD::Result::Config::Switcher attr_reader constant_alias: BCDD::Result::Config::Switcher attr_reader pattern_matching: BCDD::Result::Config::Switcher From fab3203029b3d6a152d31dbcd27ad34e50857a8d Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 10:18:27 -0300 Subject: [PATCH 18/30] Allow toggle expectations via configuration --- CHANGELOG.md | 40 +++++++++++++++++-------- README.md | 10 +++---- lib/bcdd/result/context/expectations.rb | 6 +++- lib/bcdd/result/context/mixin.rb | 6 +++- lib/bcdd/result/expectations.rb | 14 +++++++-- lib/bcdd/result/mixin.rb | 7 ++++- sig/bcdd/result.rbs | 14 +++++++++ 7 files changed, 74 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e18149..72d57f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,37 +3,51 @@ ### Added - Add `BCDD::Result.config` - - **Constant Aliases** + - **Feature** ```ruby - BCDD::Result.config.constant_alias.options - - BCDD::Result.config.constant_alias.enabled?('Result') - - BCDD::Result.config.constant_alias.enable!('Result') - - BCDD::Result.config.constant_alias.disable!('Result') + BCDD::Result.config.feature.options + BCDD::Result.config.feature.enabled?(:expectations) + BCDD::Result.config.feature.enable!(:expectations) + BCDD::Result.config.feature.disable!(:expectations) ``` - **Default Add-ons** + ```ruby + BCDD::Result.config.addon.options + BCDD::Result.config.addon.enabled?(:continue) + BCDD::Result.config.addon.enable!(:continue) + BCDD::Result.config.addon.disable!(:continue) + ``` + - **Pattern matching** ```ruby BCDD::Result.config.pattern_matching.options - BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking) - BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) ``` + - **Constant Aliases** + ```ruby + BCDD::Result.config.constant_alias.options + BCDD::Result.config.constant_alias.enabled?('Result') + BCDD::Result.config.constant_alias.enable!('Result') + BCDD::Result.config.constant_alias.disable!('Result') + ``` - Add `BCDD::Result::configuration`. It freezes the configuration, disallowing methods that promote changes but allowing the query ones. You can use this feature to ensure integrity in your configuration. ```ruby - BCDD::Result.config.constant_alias.enabled?('Result') # false - BCDD::Result.configuration do |config| + config.addon.enable!(:continue) + config.constant_alias.enable!('Result') + + config.pattern_matching.disable!(:nil_as_valid_value_checking) + + # config.feature.disable!(:expectations) if ::Rails.env.production? end + BCDD::Result.config.addon.enabled?(:continue) # true BCDD::Result.config.constant_alias.enabled?('Result') # true + BCDD::Result.config.addon.disable!(:continue) # raises FrozenError BCDD::Result.config.constant_alias.disable!('Result') # raises FrozenError ``` diff --git a/README.md b/README.md index 1f57f0b..d65d703 100644 --- a/README.md +++ b/README.md @@ -1231,7 +1231,7 @@ The value checking has support for handling pattern-matching errors, and the cle How does this operator work? They raise an error when the pattern does not match but returns nil when it matches. -Because of this, you will need to enable `nil` as a valid value checking. You can do it by calling the `BCDD::Result::Contract.nil_as_valid_value_checking!` method. +Because of this, you will need to enable `nil` as a valid value checking. You can do it by calling the `BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking)` method. **Attention:** @@ -1242,7 +1242,7 @@ If you decide to enable this, you will do it at the beginning of your code or in # Put this line in an initializer or at the beginning of your code. # It is required if you decide to use pattern matching to validate all of your result's values. # -BCDD::Result::Contract.nil_as_valid_value_checking! +BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) module Divide extend BCDD::Result::Expectations.mixin( @@ -1534,7 +1534,7 @@ This is an example using the mixin mode, but the standalone mode is also support # Put this line in an initializer or at the beginning of your code. # It is required if you decide to use pattern matching to validate all of your result's values. # -BCDD::Result::Contract.nil_as_valid_value_checking! +BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) class Divide include BCDD::Result::Context::Expectations.mixin( @@ -1553,7 +1553,7 @@ class Divide arg2.zero? and return Failure(:division_by_zero, message: 'arg2 must not be zero') - Success(:division_completed, number: arg1 / arg2) + Success(:division_completed, number: (arg1 / arg2)) end end @@ -1590,7 +1590,7 @@ Let's use a mix of `BCDD::Result::Context` features to see in action with this a # Put this line in an initializer or at the beginning of your code. # It is required if you decide to use pattern matching to validate all of your result's values. # -BCDD::Result::Contract.nil_as_valid_value_checking! +BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) module Divide require 'logger' diff --git a/lib/bcdd/result/context/expectations.rb b/lib/bcdd/result/context/expectations.rb index cf294ae..c34db70 100644 --- a/lib/bcdd/result/context/expectations.rb +++ b/lib/bcdd/result/context/expectations.rb @@ -8,7 +8,11 @@ def self.mixin_module Mixin end - private_class_method :mixin_module + def self.result_factory_without_expectations + ::BCDD::Result::Context + end + + private_class_method :mixin!, :mixin_module, :result_factory_without_expectations def Success(type, **value) Success.new(type: type, value: value, subject: subject, expectations: contract) diff --git a/lib/bcdd/result/context/mixin.rb b/lib/bcdd/result/context/mixin.rb index 6433aa7..c753d41 100644 --- a/lib/bcdd/result/context/mixin.rb +++ b/lib/bcdd/result/context/mixin.rb @@ -33,5 +33,9 @@ def self.mixin_module Mixin end - private_class_method :mixin_module + def self.result_factory + ::BCDD::Result::Context + end + + private_class_method :mixin_module, :result_factory end diff --git a/lib/bcdd/result/expectations.rb b/lib/bcdd/result/expectations.rb index 082b340..263bb04 100644 --- a/lib/bcdd/result/expectations.rb +++ b/lib/bcdd/result/expectations.rb @@ -4,7 +4,13 @@ class BCDD::Result class Expectations require_relative 'expectations/mixin' - def self.mixin(success: nil, failure: nil, config: nil) + def self.mixin(**options) + return mixin!(**options) if Config.instance.feature.enabled?(:expectations) + + result_factory_without_expectations.mixin(**options.slice(:config)) + end + + def self.mixin!(success: nil, failure: nil, config: nil) addons = mixin_module::Addons.options(config) mod = mixin_module::Factory.module! @@ -18,7 +24,11 @@ def self.mixin_module Mixin end - private_class_method :mixin_module + def self.result_factory_without_expectations + ::BCDD::Result + end + + private_class_method :mixin!, :mixin_module, :result_factory_without_expectations def initialize(subject: nil, success: nil, failure: nil, contract: nil) @subject = subject diff --git a/lib/bcdd/result/mixin.rb b/lib/bcdd/result/mixin.rb index 9ed5394..ebaf826 100644 --- a/lib/bcdd/result/mixin.rb +++ b/lib/bcdd/result/mixin.rb @@ -41,6 +41,7 @@ def self.mixin(config: nil) mod = mixin_module::Factory.module! mod.send(:include, mixin_module::Methods) + mod.const_set(:Result, result_factory) mod.send(:include, *addons) unless addons.empty? mod end @@ -49,5 +50,9 @@ def self.mixin_module Mixin end - private_class_method :mixin_module + def self.result_factory + ::BCDD::Result + end + + private_class_method :mixin_module, :result_factory end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 72b5ff8..7451a13 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -125,6 +125,8 @@ class BCDD::Result def self.mixin: (?config: Hash[Symbol, Hash[Symbol, bool]]) -> Module def self.mixin_module: -> singleton(BCDD::Result::Mixin) + + def self.result_factory: -> singleton(BCDD::Result) end class BCDD::Result @@ -347,8 +349,16 @@ class BCDD::Result::Expectations ?failure: Hash[Symbol, untyped] | Array[Symbol] ) -> Module + def self.mixin!: ( + ?config: Hash[Symbol, Hash[Symbol, bool]], + ?success: Hash[Symbol, untyped] | Array[Symbol], + ?failure: Hash[Symbol, untyped] | Array[Symbol] + ) -> Module + def self.mixin_module: -> singleton(BCDD::Result::Expectations::Mixin) + def self.result_factory_without_expectations: -> singleton(BCDD::Result) + def initialize: ( ?subject: untyped, ?success: Hash[Symbol, untyped] | Array[Symbol], @@ -455,11 +465,15 @@ class BCDD::Result::Context end def self.mixin_module: -> singleton(BCDD::Result::Context::Mixin) + + def self.result_factory: -> singleton(BCDD::Result::Context) end class BCDD::Result::Context::Expectations < BCDD::Result::Expectations def self.mixin_module: -> singleton(BCDD::Result::Context::Expectations::Mixin) + def self.result_factory_without_expectations: -> singleton(BCDD::Result) + def Success: (Symbol, **untyped) -> BCDD::Result::Context::Success def Failure: (Symbol, **untyped) -> BCDD::Result::Context::Failure end From b02935fab2a7374d7439da71d62d83f491f03f31 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 12:31:14 -0300 Subject: [PATCH 19/30] Simplify BCDD::Config::Options --- lib/bcdd/result/config/options.rb | 33 +++++++------------ lib/bcdd/result/context/expectations/mixin.rb | 2 +- lib/bcdd/result/context/mixin.rb | 2 +- lib/bcdd/result/expectations/mixin.rb | 2 +- lib/bcdd/result/mixin.rb | 2 +- sig/bcdd/result.rbs | 20 +++++++---- 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/lib/bcdd/result/config/options.rb b/lib/bcdd/result/config/options.rb index c6d21f0..17cdc93 100644 --- a/lib/bcdd/result/config/options.rb +++ b/lib/bcdd/result/config/options.rb @@ -3,34 +3,23 @@ class BCDD::Result class Config module Options - UnwrapValueFromOptions = ->(flags, options, config_category, (config_name, default_config)) do - flag_enabled = flags.dig(config_category, config_name) + def self.with_defaults(all_flags, config:) + all_flags ||= {} - options.dig(config_category, config_name) if flag_enabled.nil? ? default_config : flag_enabled - end.curry + default_flags = Config.instance.to_h.fetch(config) - MapOptionsWithDefaults = ->(options) do - default_options = Config.instance.options + config_flags = all_flags.fetch(config, {}) - defaults = default_options.slice(*options.keys).transform_values(&:to_h) - - return defaults unless defaults.empty? - - raise ArgumentError, "Invalid options: #{options.keys}. Valid ones: #{default_options.keys}" + default_flags.merge(config_flags).slice(*default_flags.keys) end - def self.unwrap(flags:, options:) - flags ||= {} - - flags.is_a?(::Hash) or raise ArgumentError, "expected a Hash[Symbol, bool], got #{flags.inspect}" - - options_with_defaults = MapOptionsWithDefaults[options] - - unwrap_value_from_options = UnwrapValueFromOptions[flags, options] + def self.filter_map(all_flags, config:, from:) + with_defaults(all_flags, config: config) + .filter_map { |name, truthy| from[name] if truthy } + end - options_with_defaults.flat_map do |config_category, default_configs| - default_configs.filter_map(&unwrap_value_from_options[config_category]) - end + def self.addon(map:, from:) + filter_map(map, config: :addon, from: from) end end end diff --git a/lib/bcdd/result/context/expectations/mixin.rb b/lib/bcdd/result/context/expectations/mixin.rb index 9774d30..99a5cf6 100644 --- a/lib/bcdd/result/context/expectations/mixin.rb +++ b/lib/bcdd/result/context/expectations/mixin.rb @@ -16,7 +16,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(config_flags) - ::BCDD::Result::Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) + ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS) end end end diff --git a/lib/bcdd/result/context/mixin.rb b/lib/bcdd/result/context/mixin.rb index c753d41..cd99bf3 100644 --- a/lib/bcdd/result/context/mixin.rb +++ b/lib/bcdd/result/context/mixin.rb @@ -24,7 +24,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(config_flags) - ::BCDD::Result::Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) + ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS) end end end diff --git a/lib/bcdd/result/expectations/mixin.rb b/lib/bcdd/result/expectations/mixin.rb index aff0d9b..e9b73ef 100644 --- a/lib/bcdd/result/expectations/mixin.rb +++ b/lib/bcdd/result/expectations/mixin.rb @@ -37,7 +37,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(config_flags) - Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) + Config::Options.addon(map: config_flags, from: OPTIONS) end end end diff --git a/lib/bcdd/result/mixin.rb b/lib/bcdd/result/mixin.rb index ebaf826..6e115b1 100644 --- a/lib/bcdd/result/mixin.rb +++ b/lib/bcdd/result/mixin.rb @@ -31,7 +31,7 @@ module Continuable OPTIONS = { continue: Continuable }.freeze def self.options(config_flags) - Config::Options.unwrap(options: { addon: OPTIONS }, flags: config_flags) + Config::Options.addon(map: config_flags, from: OPTIONS) end end end diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 7451a13..8c0e7fe 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -560,11 +560,19 @@ module BCDD::Result::Config::ConstantAlias end module BCDD::Result::Config::Options - UnwrapValueFromOptions: Proc - MapOptionsWithDefaults: Proc - - def self.unwrap: ( - flags: Hash[Symbol, Hash[Symbol, bool]], - options: Hash[Symbol, Hash[Symbol, untyped]] + def self.with_defaults: ( + Hash[Symbol, Hash[Symbol, bool]], + config: Symbol + ) -> Hash[Symbol, bool] + + def self.filter_map: ( + Hash[Symbol, Hash[Symbol, bool]], + config: Symbol, + from: Hash[Symbol, untyped] ) -> Array[untyped] + + def self.addon: ( + map: Hash[Symbol, Hash[Symbol, bool]], + from: Hash[Symbol, Module] + ) -> Array[Module] end From 30afe8aa89896e7ed6f924c01d8ac725d8d0d321 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Wed, 8 Nov 2023 13:06:49 -0300 Subject: [PATCH 20/30] Allow expectations to receive pattern matching config --- lib/bcdd/result/config/options.rb | 4 +- lib/bcdd/result/contract.rb | 8 ++-- .../result/contract/for_types_and_values.rb | 15 +++---- lib/bcdd/result/expectations.rb | 6 +-- sig/bcdd/result.rbs | 15 ++++--- .../contract/for_types_and_values_test.rb | 4 +- .../nil_as_valid_value_checking_test.rb | 44 ++++++++++--------- 7 files changed, 51 insertions(+), 45 deletions(-) diff --git a/lib/bcdd/result/config/options.rb b/lib/bcdd/result/config/options.rb index 17cdc93..dca6887 100644 --- a/lib/bcdd/result/config/options.rb +++ b/lib/bcdd/result/config/options.rb @@ -3,7 +3,7 @@ class BCDD::Result class Config module Options - def self.with_defaults(all_flags, config:) + def self.with_defaults(all_flags, config) all_flags ||= {} default_flags = Config.instance.to_h.fetch(config) @@ -14,7 +14,7 @@ def self.with_defaults(all_flags, config:) end def self.filter_map(all_flags, config:, from:) - with_defaults(all_flags, config: config) + with_defaults(all_flags, config) .filter_map { |name, truthy| from[name] if truthy } end diff --git a/lib/bcdd/result/contract.rb b/lib/bcdd/result/contract.rb index 7a182f7..2d9b137 100644 --- a/lib/bcdd/result/contract.rb +++ b/lib/bcdd/result/contract.rb @@ -19,14 +19,14 @@ def self.evaluate(data, contract) TypeChecker.new(data.type, expectations: contract) end - ToEnsure = ->(spec) do + ToEnsure = ->(spec, config) do return Disabled if spec.nil? - spec.is_a?(::Hash) ? ForTypesAndValues.new(spec) : ForTypes.new(Array(spec)) + spec.is_a?(::Hash) ? ForTypesAndValues.new(spec, config) : ForTypes.new(Array(spec)) end - def self.new(success:, failure:) - Evaluator.new(ToEnsure[success], ToEnsure[failure]) + def self.new(success:, failure:, config:) + Evaluator.new(ToEnsure[success, config], ToEnsure[failure, config]) end private_constant :ToEnsure diff --git a/lib/bcdd/result/contract/for_types_and_values.rb b/lib/bcdd/result/contract/for_types_and_values.rb index 39b3ced..caa0ab8 100644 --- a/lib/bcdd/result/contract/for_types_and_values.rb +++ b/lib/bcdd/result/contract/for_types_and_values.rb @@ -4,7 +4,12 @@ class BCDD::Result class Contract::ForTypesAndValues include Contract::Interface - def initialize(types_and_values) + def initialize(types_and_values, config) + @nil_as_valid_value_checking = + Config::Options + .with_defaults(config, :pattern_matching) + .fetch(:nil_as_valid_value_checking) + @types_and_values = types_and_values.transform_keys(&:to_sym) @types_contract = Contract::ForTypes.new(@types_and_values.keys) @@ -29,17 +34,11 @@ def type_and_value!(data) checking_result = value_checking === value - return value if checking_result || (checking_result.nil? && nil_as_valid_value_checking?) + return value if checking_result || (checking_result.nil? && @nil_as_valid_value_checking) raise Contract::Error::UnexpectedValue.build(type: type, value: value) rescue ::NoMatchingPatternError => e raise Contract::Error::UnexpectedValue.build(type: data.type, value: data.value, cause: e) end - - private - - def nil_as_valid_value_checking? - Config.instance.pattern_matching.enabled?(:nil_as_valid_value_checking) - end end end diff --git a/lib/bcdd/result/expectations.rb b/lib/bcdd/result/expectations.rb index 263bb04..989d711 100644 --- a/lib/bcdd/result/expectations.rb +++ b/lib/bcdd/result/expectations.rb @@ -14,7 +14,7 @@ def self.mixin!(success: nil, failure: nil, config: nil) addons = mixin_module::Addons.options(config) mod = mixin_module::Factory.module! - mod.const_set(:Result, new(success: success, failure: failure).freeze) + mod.const_set(:Result, new(success: success, failure: failure, config: config).freeze) mod.module_eval(mixin_module::METHODS) mod.send(:include, *addons) unless addons.empty? mod @@ -30,12 +30,12 @@ def self.result_factory_without_expectations private_class_method :mixin!, :mixin_module, :result_factory_without_expectations - def initialize(subject: nil, success: nil, failure: nil, contract: nil) + def initialize(subject: nil, success: nil, failure: nil, contract: nil, config: nil) @subject = subject @contract = contract if contract.is_a?(Contract::Evaluator) - @contract ||= Contract.new(success: success, failure: failure).freeze + @contract ||= Contract.new(success: success, failure: failure, config: config).freeze end def Success(type, value = nil) diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index 8c0e7fe..d6790ba 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -234,12 +234,13 @@ module BCDD::Result::Contract BCDD::Result::Contract::Evaluator ) -> BCDD::Result::Contract::TypeChecker - ToEnsure: ^(Hash[Symbol, untyped] | Array[Symbol]) + ToEnsure: ^(Hash[Symbol, untyped] | Array[Symbol], Hash[Symbol, Hash[Symbol, bool]]) -> BCDD::Result::Contract::Evaluator def self.new: ( success: Hash[Symbol, untyped] | Array[Symbol], - failure: Hash[Symbol, untyped] | Array[Symbol] + failure: Hash[Symbol, untyped] | Array[Symbol], + config: Hash[Symbol, Hash[Symbol, bool]] ) -> BCDD::Result::Contract::Evaluator end @@ -315,7 +316,10 @@ module BCDD::Result::Contract class ForTypesAndValues include Interface - def initialize: (Hash[Symbol, untyped]) -> void + def initialize: ( + Hash[Symbol, untyped], + Hash[Symbol, Hash[Symbol, bool]] + ) -> void private @@ -363,7 +367,8 @@ class BCDD::Result::Expectations ?subject: untyped, ?success: Hash[Symbol, untyped] | Array[Symbol], ?failure: Hash[Symbol, untyped] | Array[Symbol], - ?contract: BCDD::Result::Contract::Evaluator + ?contract: BCDD::Result::Contract::Evaluator, + ?config: Hash[Symbol, Hash[Symbol, bool]] ) -> void def Success: (Symbol, ?untyped) -> BCDD::Result::Success @@ -562,7 +567,7 @@ end module BCDD::Result::Config::Options def self.with_defaults: ( Hash[Symbol, Hash[Symbol, bool]], - config: Symbol + Symbol ) -> Hash[Symbol, bool] def self.filter_map: ( diff --git a/test/bcdd/result/contract/for_types_and_values_test.rb b/test/bcdd/result/contract/for_types_and_values_test.rb index a9b79d3..251139b 100644 --- a/test/bcdd/result/contract/for_types_and_values_test.rb +++ b/test/bcdd/result/contract/for_types_and_values_test.rb @@ -5,9 +5,7 @@ class BCDD::Result class Contract::ForTypesAndValuesTest < Minitest::Test test '#type?' do - contract = Contract::ForTypesAndValues.new( - ok: Object - ) + contract = Contract::ForTypesAndValues.new({ ok: Object }, nil) assert contract.type?(:ok) refute contract.type?(:yes) diff --git a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb index 1c31e92..c27a9f7 100644 --- a/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb +++ b/test/bcdd/result/contract/nil_as_valid_value_checking_test.rb @@ -5,23 +5,25 @@ class BCDD::Result class Contract::NiltAsValidValueCheckingTest < Minitest::Test test 'BCDD::Result::Expectations' do - _Result = BCDD::Result::Expectations.new( - success: { - ok: ->(value) { - case value - in Numeric then nil - end - } + contract = { + ok: ->(value) { + case value + in Numeric then nil + end } - ) + } + + _Result1 = BCDD::Result::Expectations.new(success: contract) assert_raises(Contract::Error::UnexpectedValue) do - _Result::Success(:ok, 1) + _Result1::Success(:ok, 1) end BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - result = _Result::Success(:ok, 1) + _Result2 = BCDD::Result::Expectations.new(success: contract) + + result = _Result2::Success(:ok, 1) assert result.success?(:ok) assert_equal(1, result.value) @@ -30,23 +32,25 @@ class Contract::NiltAsValidValueCheckingTest < Minitest::Test end test 'BCDD::Result::Context::Expectations' do - _Result = BCDD::Result::Context::Expectations.new( - success: { - ok: ->(value) { - case value - in { number: Numeric } then nil - end - } + contract = { + ok: ->(value) { + case value + in { number: Numeric } then nil + end } - ) + } + + _Result1 = BCDD::Result::Context::Expectations.new(success: contract) assert_raises(Contract::Error::UnexpectedValue) do - _Result::Success(:ok, number: 1) + _Result1::Success(:ok, number: 1) end BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - result = _Result::Success(:ok, number: 1) + _Result2 = BCDD::Result::Context::Expectations.new(success: contract) + + result = _Result2::Success(:ok, number: 1) assert result.success?(:ok) assert_equal({ number: 1 }, result.value) From 4df8f64d0c0ba250c26f09b045b80ee3140770b5 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Thu, 30 Nov 2023 09:13:36 -0300 Subject: [PATCH 21/30] Add lib/bcdd-result.rb --- .rubocop.yml | 4 ++++ lib/bcdd-result.rb | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 lib/bcdd-result.rb diff --git a/.rubocop.yml b/.rubocop.yml index 0636819..5911a5a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -65,3 +65,7 @@ Minitest/MultipleAssertions: Minitest/AssertEmptyLiteral: Enabled: false + +Naming/FileName: + Exclude: + - lib/bcdd-result.rb diff --git a/lib/bcdd-result.rb b/lib/bcdd-result.rb new file mode 100644 index 0000000..f24644a --- /dev/null +++ b/lib/bcdd-result.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require 'bcdd/result' From aae4a130b1c325b458b7f5249af84cd46a7b4658 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 09:46:22 -0300 Subject: [PATCH 22/30] Update gems and add mock --- Gemfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index a5d71f5..a65cc76 100644 --- a/Gemfile +++ b/Gemfile @@ -5,15 +5,16 @@ source 'https://rubygems.org' # Specify your gem's dependencies in bcdd-result.gemspec gemspec -gem 'rake', '~> 13.0' +gem 'rake', '~> 13.1' -gem 'minitest', '~> 5.0' +gem 'minitest', '~> 5.20' +gem 'mocha', '~> 2.1', require: false -gem 'rubocop', '~> 1.21' -gem 'rubocop-minitest', '~> 0.31.1' +gem 'rubocop', '~> 1.58', '>= 1.58.0' +gem 'rubocop-minitest', '~> 0.33.0' gem 'rubocop-performance', '~> 1.19', '>= 1.19.1' gem 'rubocop-rake', '~> 0.6.0' gem 'simplecov', '~> 0.22.0', require: false -gem 'steep', '~> 1.5', '>= 1.5.3', require: false +gem 'steep', '~> 1.6', require: false if RUBY_VERSION >= '3.0' From 5abc27a74af1d090f42506cfe8b45e5865270e2a Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 09:45:54 -0300 Subject: [PATCH 23/30] Test BCDD::Result::Config --- lib/bcdd/result/config.rb | 2 +- lib/bcdd/result/expectations.rb | 8 + sig/bcdd/result.rbs | 10 +- .../bcdd/result/config/addon/continue_test.rb | 98 +++++++++ test/bcdd/result/config/addon_test.rb | 28 +++ .../config/constant_alias/result_test.rb | 19 ++ .../bcdd/result/config/constant_alias_test.rb | 18 ++ .../config/feature/expectations_test.rb | 103 ++++++++++ test/bcdd/result/config/feature_test.rb | 26 +++ .../nil_as_valid_value_checking_test.rb | 189 ++++++++++++++++++ .../result/config/pattern_matching_test.rb | 26 +++ test/bcdd/result/config/switcher_test.rb | 173 ++++++++++++++++ test/bcdd/result/config_test.rb | 83 ++++++++ test/bcdd/result/configuration_test.rb | 47 +++++ test/test_helper.rb | 8 +- 15 files changed, 833 insertions(+), 5 deletions(-) create mode 100644 test/bcdd/result/config/addon/continue_test.rb create mode 100644 test/bcdd/result/config/addon_test.rb create mode 100644 test/bcdd/result/config/constant_alias/result_test.rb create mode 100644 test/bcdd/result/config/constant_alias_test.rb create mode 100644 test/bcdd/result/config/feature/expectations_test.rb create mode 100644 test/bcdd/result/config/feature_test.rb create mode 100644 test/bcdd/result/config/pattern_matching/nil_as_valid_value_checking_test.rb create mode 100644 test/bcdd/result/config/pattern_matching_test.rb create mode 100644 test/bcdd/result/config/switcher_test.rb create mode 100644 test/bcdd/result/config_test.rb create mode 100644 test/bcdd/result/configuration_test.rb diff --git a/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb index 2e901eb..8fa6f93 100644 --- a/lib/bcdd/result/config.rb +++ b/lib/bcdd/result/config.rb @@ -63,7 +63,7 @@ def to_h end def inspect - "#<#{self.class.name} options=#{options.keys.inspect}>" + "#<#{self.class.name} options=#{options.keys.sort.inspect}>" end private_constant :ADDON, :FEATURE, :PATTERN_MATCHING diff --git a/lib/bcdd/result/expectations.rb b/lib/bcdd/result/expectations.rb index 989d711..58b6127 100644 --- a/lib/bcdd/result/expectations.rb +++ b/lib/bcdd/result/expectations.rb @@ -28,6 +28,14 @@ def self.result_factory_without_expectations ::BCDD::Result end + def self.new(...) + return result_factory_without_expectations unless Config.instance.feature.enabled?(:expectations) + + instance = allocate + instance.send(:initialize, ...) + instance + end + private_class_method :mixin!, :mixin_module, :result_factory_without_expectations def initialize(subject: nil, success: nil, failure: nil, contract: nil, config: nil) diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs index d6790ba..1c512a5 100644 --- a/sig/bcdd/result.rbs +++ b/sig/bcdd/result.rbs @@ -235,7 +235,7 @@ module BCDD::Result::Contract ) -> BCDD::Result::Contract::TypeChecker ToEnsure: ^(Hash[Symbol, untyped] | Array[Symbol], Hash[Symbol, Hash[Symbol, bool]]) - -> BCDD::Result::Contract::Evaluator + -> BCDD::Result::Contract::Interface def self.new: ( success: Hash[Symbol, untyped] | Array[Symbol], @@ -363,6 +363,14 @@ class BCDD::Result::Expectations def self.result_factory_without_expectations: -> singleton(BCDD::Result) + def self.new: ( + ?subject: untyped, + ?success: Hash[Symbol, untyped] | Array[Symbol], + ?failure: Hash[Symbol, untyped] | Array[Symbol], + ?contract: BCDD::Result::Contract::Evaluator, + ?config: Hash[Symbol, Hash[Symbol, bool]] + ) -> (BCDD::Result::Expectations | untyped) + def initialize: ( ?subject: untyped, ?success: Hash[Symbol, untyped] | Array[Symbol], diff --git a/test/bcdd/result/config/addon/continue_test.rb b/test/bcdd/result/config/addon/continue_test.rb new file mode 100644 index 0000000..283e500 --- /dev/null +++ b/test/bcdd/result/config/addon/continue_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class AddonContinueTest < Minitest::Test + test 'the side effects' do + class1 = Class.new do + include BCDD::Result.mixin + + def call + Continue('the method Continue does not exist as the config is disabled') + end + end + + error1 = assert_raises(NoMethodError) { class1.new.call } + + assert_match(/undefined method.+Continue/, error1.message) + + BCDD::Result.config.addon.enable!(:continue) + + class2 = Class.new do + include BCDD::Result.mixin + + def call + Continue('the method Continue now exists as the config is enabled by default') + end + end + + assert_equal( + 'the method Continue now exists as the config is enabled by default', + class2.new.call.value + ) + ensure + BCDD::Result.config.addon.disable!(:continue) + + class3 = Class.new do + include BCDD::Result.mixin + + def call + Continue('the error is raised again as the config is disabled by default') + end + end + + error2 = assert_raises(NoMethodError) { class3.new.call } + + assert_match(/undefined method.+Continue/, error2.message) + end + + test 'the overwriting of the default config' do + BCDD::Result.config.addon.enable!(:continue) + + class1 = Class.new do + include BCDD::Result.mixin(config: { addon: { continue: false } }) + + def call + Continue("this method won't exist as the default config was overwritten") + end + end + + class2 = Class.new do + include BCDD::Result::Expectations.mixin(config: { addon: { continue: false } }) + + def call + Continue("this method won't exist as the default config was overwritten") + end + end + + class3 = Class.new do + include BCDD::Result::Context.mixin(config: { addon: { continue: false } }) + + def call + Continue("this method won't exist as the default config was overwritten") + end + end + + class4 = Class.new do + include BCDD::Result::Context::Expectations.mixin(config: { addon: { continue: false } }) + + def call + Continue("this method won't exist as the default config was overwritten") + end + end + + error1 = assert_raises(NoMethodError) { class1.new.call } + error2 = assert_raises(NoMethodError) { class2.new.call } + error3 = assert_raises(NoMethodError) { class3.new.call } + error4 = assert_raises(NoMethodError) { class4.new.call } + + assert_match(/undefined method.+Continue/, error1.message) + assert_match(/undefined method.+Continue/, error2.message) + assert_match(/undefined method.+Continue/, error3.message) + assert_match(/undefined method.+Continue/, error4.message) + ensure + BCDD::Result.config.addon.disable!(:continue) + end + end +end diff --git a/test/bcdd/result/config/addon_test.rb b/test/bcdd/result/config/addon_test.rb new file mode 100644 index 0000000..680c653 --- /dev/null +++ b/test/bcdd/result/config/addon_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class AddonTest < Minitest::Test + test 'the switcher' do + config = BCDD::Result.config.addon + + assert_instance_of(Switcher, config) + + assert_equal( + { + continue: { + enabled: false, + affects: [ + 'BCDD::Result', + 'BCDD::Result::Context', + 'BCDD::Result::Expectations', + 'BCDD::Result::Context::Expectations' + ] + } + }, + config.options + ) + end + end +end diff --git a/test/bcdd/result/config/constant_alias/result_test.rb b/test/bcdd/result/config/constant_alias/result_test.rb new file mode 100644 index 0000000..4d609fe --- /dev/null +++ b/test/bcdd/result/config/constant_alias/result_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class ConstantAliasResultTest < Minitest::Test + test 'the side effects' do + assert_raises(NameError) { ::Result } + + BCDD::Result.config.constant_alias.enable!('Result') + + assert_same(BCDD::Result, ::Result) + ensure + BCDD::Result.config.constant_alias.disable!('Result') + + assert_raises(NameError) { ::Result } + end + end +end diff --git a/test/bcdd/result/config/constant_alias_test.rb b/test/bcdd/result/config/constant_alias_test.rb new file mode 100644 index 0000000..ac5c8aa --- /dev/null +++ b/test/bcdd/result/config/constant_alias_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class ConstantAliasTest < Minitest::Test + test 'the switcher' do + config = BCDD::Result.config.constant_alias + + assert_instance_of(Switcher, config) + + assert_equal( + { 'Result' => { enabled: false, affects: ['Object'] } }, + config.options + ) + end + end +end diff --git a/test/bcdd/result/config/feature/expectations_test.rb b/test/bcdd/result/config/feature/expectations_test.rb new file mode 100644 index 0000000..15ea049 --- /dev/null +++ b/test/bcdd/result/config/feature/expectations_test.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class FeatureExpectationsTest < Minitest::Test + test 'the side effects' do + class1a = Class.new do + include BCDD::Result::Expectations.mixin(success: { ok: ::Numeric }) + + def call; Success(:ok, '1'); end + end + + class1b = Class.new + class1b.const_set(:Result, BCDD::Result::Expectations.new(success: { ok: ::Numeric })) + class1b.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, '1'); end } + + class1c = Class.new do + include BCDD::Result::Context::Expectations.mixin(success: { ok: ->(v) { v[:one] == 1 } }) + + def call; Success(:yes, one: 1); end + end + + class1d = Class.new + class1d.const_set(:Result, BCDD::Result::Context::Expectations.new(success: { ok: ->(v) { v[:one] == 1 } })) + class1d.class_eval { def call; self.class.const_get(:Result, false)::Success(:yes, one: 1); end } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1a.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1b.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class1a::Result::Success(:yes, 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class1b::Result::Success(:yes, 1) } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1c::Result::Success(:ok, one: 2) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1d::Result::Success(:ok, one: 2) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class1c.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class1d.new.call } + + BCDD::Result.config.feature.disable!(:expectations) + + class2a = Class.new do + include BCDD::Result::Expectations.mixin(success: { ok: ::Numeric }) + + def call; Success(:ok, '1'); end + end + + class2b = Class.new + class2b.const_set(:Result, BCDD::Result::Expectations.new(success: { ok: ::Numeric })) + class2b.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, '1'); end } + + class2c = Class.new do + include BCDD::Result::Context::Expectations.mixin(success: { ok: ->(v) { v[:one] == 1 } }) + + def call; Success(:yes, one: 1); end + end + + class2d = Class.new + class2d.const_set(:Result, BCDD::Result::Context::Expectations.new(success: { ok: ->(v) { v[:one] == 1 } })) + class2d.class_eval { def call; self.class.const_get(:Result, false)::Success(:yes, one: 1); end } + + assert(class2a.new.call.then { _1.success?(:ok) && _1.value == '1' }) + assert(class2b.new.call.then { _1.success?(:ok) && _1.value == '1' }) + assert(class2c.new.call.then { _1.success?(:yes) && _1.value == { one: 1 } }) + assert(class2d.new.call.then { _1.success?(:yes) && _1.value == { one: 1 } }) + + assert(class2a::Result::Success(:yes, 1).then { _1.success?(:yes) && _1.value == 1 }) + assert(class2b::Result::Success(:yes, 1).then { _1.success?(:yes) && _1.value == 1 }) + assert(class2c::Result::Success(:ok, one: 2).then { _1.success?(:ok) && _1.value == { one: 2 } }) + assert(class2d::Result::Success(:ok, one: 2).then { _1.success?(:ok) && _1.value == { one: 2 } }) + ensure + BCDD::Result.config.feature.enable!(:expectations) + + class3a = Class.new do + include BCDD::Result::Expectations.mixin(success: { ok: ::Numeric }) + + def call; Success(:ok, '1'); end + end + + class3b = Class.new + class3b.const_set(:Result, BCDD::Result::Expectations.new(success: { ok: ::Numeric })) + class3b.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, '1'); end } + + class3c = Class.new do + include BCDD::Result::Context::Expectations.mixin(success: { ok: ->(v) { v[:one] == 1 } }) + + def call; Success(:yes, one: 1); end + end + + class3d = Class.new + class3d.const_set(:Result, BCDD::Result::Context::Expectations.new(success: { ok: ->(v) { v[:one] == 1 } })) + class3d.class_eval { def call; self.class.const_get(:Result, false)::Success(:yes, one: 1); end } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3a.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3b.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class3a::Result::Success(:yes, 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class3b::Result::Success(:yes, 1) } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3c::Result::Success(:ok, one: 2) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3d::Result::Success(:ok, one: 2) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class3c.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedType) { class3d.new.call } + end + end +end diff --git a/test/bcdd/result/config/feature_test.rb b/test/bcdd/result/config/feature_test.rb new file mode 100644 index 0000000..c7543b8 --- /dev/null +++ b/test/bcdd/result/config/feature_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class FeatureInstanceTest < Minitest::Test + test 'the switcher' do + config = BCDD::Result.config.feature + + assert_instance_of(Switcher, config) + + assert_equal( + { + expectations: { + enabled: true, + affects: [ + 'BCDD::Result::Expectations', + 'BCDD::Result::Context::Expectations' + ] + } + }, + config.options + ) + end + end +end diff --git a/test/bcdd/result/config/pattern_matching/nil_as_valid_value_checking_test.rb b/test/bcdd/result/config/pattern_matching/nil_as_valid_value_checking_test.rb new file mode 100644 index 0000000..8bb0c0e --- /dev/null +++ b/test/bcdd/result/config/pattern_matching/nil_as_valid_value_checking_test.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class PatternMatchingNilAsAValidValueCheckingTest < Minitest::Test + test 'the side effects' do + is_numeric = ->(v) do + case v + in Numeric then true + end + + nil + end + + is_hash_numeric = ->(v) do + case v + in { number: Numeric } then true + end + + nil + end + + class1a = Class.new do + include BCDD::Result::Expectations.mixin(success: { ok: is_numeric }) + + def call; Success(:ok, 1); end + end + + class1b = Class.new + class1b.const_set(:Result, BCDD::Result::Expectations.new(success: { ok: is_numeric })) + class1b.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, 1); end } + + class1c = Class.new do + include BCDD::Result::Context::Expectations.mixin(success: { ok: is_hash_numeric }) + + def call; Success(:ok, number: 1); end + end + + class1d = Class.new + class1d.const_set(:Result, BCDD::Result::Context::Expectations.new(success: { ok: is_hash_numeric })) + class1d.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, number: 1); end } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1a.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1b.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1c.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1d.new.call } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1a::Result::Success(:ok, 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1b::Result::Success(:ok, 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1c::Result::Success(:ok, number: 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1d::Result::Success(:ok, number: 1) } + + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) + + class2a = Class.new do + include BCDD::Result::Expectations.mixin(success: { ok: is_numeric }) + + def call; Success(:ok, 1); end + end + + class2b = Class.new + class2b.const_set(:Result, BCDD::Result::Expectations.new(success: { ok: is_numeric })) + class2b.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, 1); end } + + class2c = Class.new do + include BCDD::Result::Context::Expectations.mixin(success: { ok: is_hash_numeric }) + + def call; Success(:ok, number: 1); end + end + + class2d = Class.new + class2d.const_set(:Result, BCDD::Result::Context::Expectations.new(success: { ok: is_hash_numeric })) + class2d.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, number: 1); end } + + assert(class2a.new.call.then { _1.success?(:ok) && _1.value == 1 }) + assert(class2b.new.call.then { _1.success?(:ok) && _1.value == 1 }) + assert(class2c.new.call.then { _1.success?(:ok) && _1.value == { number: 1 } }) + assert(class2d.new.call.then { _1.success?(:ok) && _1.value == { number: 1 } }) + + assert(class2a::Result::Success(:ok, 1).then { _1.success?(:ok) && _1.value == 1 }) + assert(class2b::Result::Success(:ok, 1).then { _1.success?(:ok) && _1.value == 1 }) + assert(class2c::Result::Success(:ok, number: 1).then { _1.success?(:ok) && _1.value == { number: 1 } }) + assert(class2d::Result::Success(:ok, number: 1).then { _1.success?(:ok) && _1.value == { number: 1 } }) + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class2a::Result::Success(:ok, '1') } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class2b::Result::Success(:ok, '1') } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class2c::Result::Success(:ok, number: '1') } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class2d::Result::Success(:ok, number: '1') } + ensure + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) + + class3a = Class.new do + include BCDD::Result::Expectations.mixin(success: { ok: is_numeric }) + + def call; Success(:ok, 1); end + end + + class3b = Class.new + class3b.const_set(:Result, BCDD::Result::Expectations.new(success: { ok: is_numeric })) + class3b.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, 1); end } + + class3c = Class.new do + include BCDD::Result::Context::Expectations.mixin(success: { ok: is_hash_numeric }) + + def call; Success(:ok, number: 1); end + end + + class3d = Class.new + class3d.const_set(:Result, BCDD::Result::Context::Expectations.new(success: { ok: is_hash_numeric })) + class3d.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, number: 1); end } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3a.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3b.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3c.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3d.new.call } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3a::Result::Success(:ok, 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3b::Result::Success(:ok, 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3c::Result::Success(:ok, number: 1) } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3d::Result::Success(:ok, number: 1) } + end + + test 'the overwriting of the default config' do + BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) + + is_numeric = ->(v) do + case v + in Numeric then true + end + + nil + end + + is_hash_numeric = ->(v) do + case v + in { number: Numeric } then true + end + + nil + end + + class1 = Class.new do + include BCDD::Result::Expectations.mixin( + config: { pattern_matching: { nil_as_valid_value_checking: false } }, + success: { ok: is_numeric } + ) + + def call; Success(:ok, 1); end + end + + class2 = Class.new + class2.const_set( + :Result, + BCDD::Result::Expectations.new( + config: { pattern_matching: { nil_as_valid_value_checking: false } }, + success: { ok: is_numeric } + ) + ) + class2.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, 1); end } + + class3 = Class.new do + include BCDD::Result::Context::Expectations.mixin( + config: { pattern_matching: { nil_as_valid_value_checking: false } }, + success: { ok: is_hash_numeric } + ) + + def call; Success(:ok, number: 1); end + end + + class4 = Class.new + class4.const_set( + :Result, + BCDD::Result::Context::Expectations.new( + config: { pattern_matching: { nil_as_valid_value_checking: false } }, + success: { ok: is_hash_numeric } + ) + ) + class4.class_eval { def call; self.class.const_get(:Result, false)::Success(:ok, number: 1); end } + + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class1.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class2.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class3.new.call } + assert_raises(BCDD::Result::Contract::Error::UnexpectedValue) { class4.new.call } + ensure + BCDD::Result.config.pattern_matching.disable!(:nil_as_valid_value_checking) + end + end +end diff --git a/test/bcdd/result/config/pattern_matching_test.rb b/test/bcdd/result/config/pattern_matching_test.rb new file mode 100644 index 0000000..8a85883 --- /dev/null +++ b/test/bcdd/result/config/pattern_matching_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class PatternMatchingTest < Minitest::Test + test 'the switcher' do + config = BCDD::Result.config.pattern_matching + + assert_instance_of(Switcher, config) + + assert_equal( + { + nil_as_valid_value_checking: { + enabled: false, + affects: [ + 'BCDD::Result::Expectations', + 'BCDD::Result::Context::Expectations' + ] + } + }, + config.options + ) + end + end +end diff --git a/test/bcdd/result/config/switcher_test.rb b/test/bcdd/result/config/switcher_test.rb new file mode 100644 index 0000000..4e55948 --- /dev/null +++ b/test/bcdd/result/config/switcher_test.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class SwitcherTest < Minitest::Test + test '#inspect' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + assert_equal( + '#true, :bar=>false}>', + switcher.inspect + ) + end + + test '#freeze' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + switcher.freeze + + assert_raises(FrozenError) { switcher.enable!(:foo) } + assert_raises(FrozenError) { switcher.disable!(:foo) } + + assert switcher.enabled?(:foo) + refute switcher.enabled?(:bar) + + assert_equal({ foo: true, bar: false }, switcher.to_h) + + assert_equal( + { + foo: { enabled: true, affects: ['foo'] }, + bar: { enabled: false, affects: ['bar'] } + }, + switcher.options + ) + end + + test '#to_h' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + assert_equal( + { foo: true, bar: false }, + switcher.to_h + ) + end + + test '#options' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + assert_equal( + { + foo: { enabled: true, affects: ['foo'] }, + bar: { enabled: false, affects: ['bar'] } + }, + switcher.options + ) + end + + test '#enabled?' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + assert switcher.enabled?(:foo) + refute switcher.enabled?(:bar) + end + + test '#enable! with valid arguments' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + switcher.enable!(:bar) + + assert switcher.enabled?(:bar) + + assert_equal({ bar: { enabled: true, affects: ['bar'] } }, switcher.enable!(:bar)) + end + + test '#enable! without arguments' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + error = assert_raises(ArgumentError) { switcher.enable! } + + assert_equal('One or more options required. Available options: :foo, :bar', error.message) + end + + test '#enable! with invalid arguments' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + error = assert_raises(ArgumentError) { switcher.enable!(:baz) } + + assert_equal('Invalid option: :baz. Available options: :foo, :bar', error.message) + end + + test '#disable! with valid arguments' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + switcher.disable!(:foo) + + refute switcher.enabled?(:foo) + + assert_equal({ foo: { enabled: false, affects: ['foo'] } }, switcher.disable!(:foo)) + end + + test '#disable! without arguments' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + error = assert_raises(ArgumentError) { switcher.disable! } + + assert_equal('One or more options required. Available options: :foo, :bar', error.message) + end + + test '#disable! with invalid arguments' do + switcher = Switcher.new( + options: { + foo: { default: true, affects: ['foo'] }, + bar: { default: false, affects: ['bar'] } + } + ) + + error = assert_raises(ArgumentError) { switcher.disable!(:baz) } + + assert_equal('Invalid option: :baz. Available options: :foo, :bar', error.message) + end + end +end diff --git a/test/bcdd/result/config_test.rb b/test/bcdd/result/config_test.rb new file mode 100644 index 0000000..d46762a --- /dev/null +++ b/test/bcdd/result/config_test.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result::Config + class Test < Minitest::Test + test '.instance' do + assert BCDD::Result::Config.instance.is_a?(Singleton) + + assert_same(BCDD::Result::Config.instance, BCDD::Result.config) + end + + test '#addon' do + assert_respond_to(BCDD::Result.config, :addon) + end + + test '#feature' do + assert_respond_to(BCDD::Result.config, :feature) + end + + test '#constant_alias' do + assert_respond_to(BCDD::Result.config, :constant_alias) + end + + test '#pattern_matching' do + assert_respond_to(BCDD::Result.config, :pattern_matching) + end + + test '#options' do + assert_instance_of(Hash, BCDD::Result.config.options) + + assert_equal( + %i[ + addon + constant_alias + feature + pattern_matching + ], + BCDD::Result.config.options.keys.sort + ) + + BCDD::Result.config.options.each_value do |switcher| + assert_instance_of(Switcher, switcher) + end + end + + test '#to_h' do + config_values = BCDD::Result.config.to_h + + assert_equal({ continue: false }, config_values[:addon]) + assert_equal({ expectations: true }, config_values[:feature]) + assert_equal({ 'Result' => false }, config_values[:constant_alias]) + assert_equal({ nil_as_valid_value_checking: false }, config_values[:pattern_matching]) + + BCDD::Result.config.options.each do |key, switcher| + assert_equal(switcher.to_h, config_values[key]) + end + end + + test '#inspect' do + assert_equal( + '#', + BCDD::Result.config.inspect + ) + end + + test '#freeze' do + instance = BCDD::Result::Config.send(:new) + + assert_instance_of(BCDD::Result::Config, instance) + + refute_same(BCDD::Result::Config.instance, instance) + + instance.freeze + + assert_predicate(instance, :frozen?) + assert_predicate(instance.addon, :frozen?) + assert_predicate(instance.feature, :frozen?) + assert_predicate(instance.constant_alias, :frozen?) + assert_predicate(instance.pattern_matching, :frozen?) + end + end +end diff --git a/test/bcdd/result/configuration_test.rb b/test/bcdd/result/configuration_test.rb new file mode 100644 index 0000000..23864ad --- /dev/null +++ b/test/bcdd/result/configuration_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BCDD::Result + class ConfigurationTest < Minitest::Test + test '.configuration' do + config_instance = Config.send(:new) + + BCDD::Result.expects(:config).twice.returns(config_instance) + + refute_predicate(config_instance, :frozen?) + + refute(config_instance.addon.enabled?(:continue)) + + BCDD::Result.configuration do |config| + assert_same(config_instance, config) + + config.addon.enable!(:continue) + + refute_predicate(config_instance, :frozen?) + end + + assert(config_instance.addon.enabled?(:continue)) + + assert_predicate(config_instance, :frozen?) + end + + test 'configuration freezing' do + String(ENV.fetch('TEST_CONFIG_FREEZING', nil)).match?(/true/i) or return + + refute(BCDD::Result.config.addon.enabled?(:continue)) + + BCDD::Result.configuration do |config| + assert_same(BCDD::Result.config, config) + + config.addon.enable!(:continue) + + refute_predicate(BCDD::Result.config, :frozen?) + end + + assert(BCDD::Result.config.addon.enabled?(:continue)) + + assert_predicate(BCDD::Result.config, :frozen?) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9e2def3..61ebdc7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,13 +13,15 @@ require 'minitest/autorun' +require 'mocha/minitest' + class Minitest::Test # Implementation based on: # https://github.com/rails/rails/blob/ac717d6/activesupport/lib/active_support/testing/declarative.rb def self.test(name, &block) - test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym - defined = method_defined? test_name - raise "#{test_name} is already defined in #{self}" if defined + test_name = :"test_#{name.gsub(/\s+/, '_')}" + + method_defined?(test_name) and raise "#{test_name} is already defined in #{self}" if block define_method(test_name, &block) From f1cf7667909d9bad9d32f2389d7e37b5195ac469 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 09:47:36 -0300 Subject: [PATCH 24/30] Update .rubocop.yml --- .rubocop.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 5911a5a..00f580a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,6 +66,9 @@ Minitest/MultipleAssertions: Minitest/AssertEmptyLiteral: Enabled: false +Minitest/AssertOperator: + Enabled: false + Naming/FileName: Exclude: - lib/bcdd-result.rb From 99929e66bfa0b1e48da1f2c2f1117865d296d854 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 09:47:46 -0300 Subject: [PATCH 25/30] Add rake test_configuration --- Rakefile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index 272c87a..5192bef 100644 --- a/Rakefile +++ b/Rakefile @@ -3,10 +3,16 @@ require 'bundler/gem_tasks' require 'rake/testtask' +Rake::TestTask.new(:test_configuration) do |t| + t.libs += %w[lib test] + + t.test_files = FileList.new('test/**/configuration_test.rb') +end + Rake::TestTask.new(:test) do |t| - t.libs << 'test' - t.libs << 'lib' - t.test_files = FileList['test/**/*_test.rb'] + t.libs += %w[lib test] + + t.test_files = FileList.new('test/**/*_test.rb') end require 'rubocop/rake_task' From d3f36d6addf1c02c81b058a2c59467ce2ec52450 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 09:49:19 -0300 Subject: [PATCH 26/30] Update .github/workflows/main.yml - Breakdown the steps - Add Ruby head to the test matrix - Update actions/checkout --- .github/workflows/main.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70264e8..0ca3dc7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,20 +13,20 @@ jobs: name: Ruby ${{ matrix.ruby }} strategy: matrix: - ruby: - - '3.2.2' - - '3.1.4' - - '3.0.6' - - '2.7.8' - + ruby: [2.7, 3.0, 3.1, 3.2, head] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - name: Run the default task - run: bundle exec rake - - name: Run the steep check + - name: Run BCDD::Result.configuration test (Minitest) + run: bundle exec rake test_configuration TEST_CONFIG_FREEZING=true + - name: Run tests (Minitest) + run: bundle exec rake test + - name: Run static code analysis (Rubocop) + run: bundle exec rake rubocop + - name: Run static type checking (Steep) run: bundle exec steep check + if: ${{ matrix.ruby == 3.2 }} From 9cf13d39ffa3d3f785d37e0aec5665e66ffa5ea0 Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 09:49:48 -0300 Subject: [PATCH 27/30] Fix README.md content --- README.md | 280 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 179 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index d65d703..d301ddb 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi - [`BCDD::Result` *versus* `Result`](#bcddresult-versus-result) - [Reference](#reference) - [Result Attributes](#result-attributes) - - [Receiving types in `result.success?` or `result.failure?`](#receiving-types-in-resultsuccess-or-resultfailure) + - [Checking types with `result.success?` or `result.failure?`](#checking-types-with-resultsuccess-or-resultfailure) - [Result Hooks](#result-hooks) - [`result.on`](#resulton) - [`result.on_type`](#resulton_type) @@ -84,7 +84,7 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi Add this line to your application's Gemfile: ```ruby -gem 'bcdd-result', require: 'bcdd/result' +gem 'bcdd-result' ``` And then execute: @@ -95,6 +95,10 @@ If bundler is not being used to manage dependencies, install the gem by executin $ gem install bcdd-result +And require it in your code: + + require 'bcdd/result' +

⬆️  back to top

## Usage @@ -119,17 +123,27 @@ BCDD::Result::Failure(:err) # #### `BCDD::Result` *versus* `Result` -The `BCDD::Result` is the main module of this gem. It contains all the features, constants, and methods you will use to create and manipulate results. +This gem provides a way to create constant aliases for `BCDD::Result` and other classes/modules. -The `Result` is an alias of `BCDD::Result`. It was created to facilitate the use of this gem in the code. So, instead of requiring `BCDD::Result` everywhere, you can require `Result` and use it as an alias. +To enable it, you must call the `BCDD::Result.configuration` method and pass a block to it. You can turn the aliases you want on/off in this block. ```ruby -require 'result' +BCDD::Result.configuration do |config| + config.constant_alias.enable!('Result') +end +``` + +So, instead of using `BCDD::Result` everywhere, you can use `Result` as an alias/shortcut. +```ruby Result::Success(:ok) # + +Result::Failure(:err) # ``` -All the examples in this README that use `BCDD::Result` can also be used with `Result`. +If you have enabled constant aliasing, all examples in this README that use `BCDD::Result` can be implemented using `Result`. + +There are other aliases and configurations available. Check the [BCDD::Result.configuration]() section for more information.

⬆️  back to top

@@ -169,12 +183,12 @@ result.value # nil ################ # With a value # ################ -result = BCDD::Result::Failure(:err, my: 'value') +result = BCDD::Result::Failure(:err, 'my_value') result.success? # false result.failure? # true result.type # :err -result.value # {:my => "value"} +result.value # "my_value" ################### # Without a value # @@ -187,9 +201,11 @@ result.type # :no result.value # nil ``` +In both cases, the `type` must be a symbol, and the `value` can be any kind of object. +

⬆️  back to top

-#### Receiving types in `result.success?` or `result.failure?` +#### Checking types with `result.success?` or `result.failure?` `BCDD::Result#success?` and `BCDD::Result#failure?` are methods that allow you to check if the result is a success or a failure. @@ -198,9 +214,11 @@ You can also check the result type by passing an argument to it. For example, `r ```ruby result = BCDD::Result::Success(:ok) -result.success? # true -result.success?(:ok) # true -result.success?(:okay) # false +result.success?(:ok) + +# This is the same as: + +result.success? && result.type == :ok ``` The same is valid for `BCDD::Result#failure?`. @@ -208,9 +226,11 @@ The same is valid for `BCDD::Result#failure?`. ```ruby result = BCDD::Result::Failure(:err) -result.failure? # true -result.failure?(:err) # true -result.failure?(:error) # false +result.failure?(:err) + +# This is the same as: + +result.failure? && result.type == :err ```

⬆️  back to top

@@ -218,7 +238,7 @@ result.failure?(:error) # false ### Result Hooks Result hooks are methods that allow you to execute a block of code based on the type of result obtained. -To demonstrate their use, I will implement a function that can divide two numbers. +To demonstrate their use, I will implement a method that can divide two numbers. ```ruby def divide(arg1, arg2) @@ -282,8 +302,7 @@ result = divide(nil, 2) output = result - .on_type(:invalid_arg) { |msg| puts msg } - .on_type(:division_by_zero) { |msg| puts msg } + .on_type(:invalid_arg, :division_by_zero) { |msg| puts msg } .on_type(:division_completed) { |number| puts number } # The code above will print 'arg1 must be numeric' and return the result itself. @@ -303,16 +322,18 @@ The `BCDD::Result#on_success` method is quite similar to the `BCDD::Result#on` h 2. If the type declaration is not included, the method will execute the block for any successful result, regardless of its type. ```ruby -# It executes the block and return itself. +# In both examples, it executes the block and returns the result itself. divide(4, 2).on_success { |number| puts number } divide(4, 2).on_success(:division_completed) { |number| puts number } -# It doesn't execute the block, but return itself. +# It doesn't execute the block as the type is different. divide(4, 4).on_success(:ok) { |value| puts value } +# It doesn't execute the block, as the result is a success, but the hook expects a failure. + divide(4, 4).on_failure { |error| puts error } ``` @@ -328,17 +349,19 @@ It is the opposite of `Result#on_success`: 2. If the type declaration is not included, the method will execute the block for any failed result, regardless of its type. ```ruby -# It executes the block and return itself. +# In both examples, it executes the block and returns the result itself. divide(nil, 2).on_failure { |error| puts error } -divide(4, 0).on_failure(:invalid_arg, :division_by_zero) { |error| puts error } - -# It doesn't execute the block, but return itself. +divide(4, 0).on_failure(:division_by_zero) { |error| puts error } -divide(4, 0).on_success { |number| puts number } +# It doesn't execute the block as the type is different. divide(4, 0).on_failure(:invalid_arg) { |error| puts error } + +# It doesn't execute the block, as the result is a failure, but the hook expects a success. + +divide(4, 0).on_success { |number| puts number } ``` *PS: The `divide()` implementation is [here](#result-hooks).* @@ -570,7 +593,7 @@ module Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then { |numbers| validate_non_zero(numbers) } + .and_then { |numbers| validate_nonzero(numbers) } .and_then { |numbers| divide(numbers) } end @@ -583,8 +606,8 @@ module Divide BCDD::Result::Success(:ok, [arg1, arg2]) end - def validate_non_zero(numbers) - return BCDD::Result::Success(:ok, numbers) unless numbers.last.zero? + def validate_nonzero(numbers) + return BCDD::Result::Success(:ok, numbers) if numbers.last.nonzero? BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') end @@ -615,9 +638,11 @@ Divide.call(2, 2) #### `BCDD::Result.mixin` -This method generates a module that can be included or extended by any object. It adds two methods to the target object: `Success()` and `Failure()`. The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's subject. +This method generates a module that any object can include or extend. It adds two methods to the target object: `Success()` and `Failure()`. + +The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's subject. -As a result, you can utilize the `#and_then` method to invoke methods from the result's subject. +Because the result has a subject, the `#and_then` method can call methods from it. ##### Class example (Instance Methods) @@ -634,7 +659,7 @@ class Divide def call validate_numbers - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) end @@ -649,7 +674,7 @@ class Divide Success(:ok, [arg1, arg2]) end - def validate_non_zero(numbers) + def validate_nonzero(numbers) return Success(:ok, numbers) unless numbers.last.zero? Failure(:division_by_zero, 'arg2 must not be zero') @@ -675,7 +700,7 @@ module Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) end @@ -688,7 +713,7 @@ module Divide Success(:ok, [arg1, arg2]) end - def validate_non_zero(numbers) + def validate_nonzero(numbers) return Success(:ok, numbers) unless numbers.last.zero? Failure(:division_by_zero, 'arg2 must not be zero') @@ -717,7 +742,7 @@ If you try to use `BCDD::Result::Subject()`/`BCDD::Result::Failure()`, or result **Note:** You can still use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods. ```ruby -module ValidateNonZero +module ValidateNonzero extend self, BCDD::Result.mixin def call(numbers) @@ -727,40 +752,83 @@ module ValidateNonZero end end -class Divide - include BCDD::Result.mixin +module Divide + extend self, BCDD::Result.mixin - attr_reader :arg1, :arg2 + def call(arg1, arg2) + validate_numbers(arg1, arg2) + .and_then(:validate_nonzero) + .and_then(:divide) + end - def initialize(arg1, arg2) - @arg1 = arg1 - @arg2 = arg2 + private + + def validate_numbers(arg1, arg2) + arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric') + arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric') + + Success(:ok, [arg1, arg2]) end - def call - validate_numbers - .and_then(:validate_non_zero) + def validate_nonzero(numbers) + ValidateNonzero.call(numbers) # This will raise an error + end + + def divide((number1, number2)) + Success(:division_completed, number1 / number2) + end +end +``` + +Look at the error produced by the code above: + +```ruby +Divide.call(2, 0) + +# You cannot call #and_then and return a result that does not belong to the subject! (BCDD::Result::Error::InvalidResultSubject) +# Expected subject: Divide +# Given subject: ValidateNonzero +# Given result: # +``` + +In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the subject. + +```ruby +module ValidateNonzero + extend self, BCDD::Result.mixin + + def call(numbers) + return Success(:ok, numbers) unless numbers.last.zero? + + Failure(:division_by_zero, 'arg2 must not be zero') + end +end + +module Divide + extend self, BCDD::Result.mixin + + def call(arg1, arg2) + validate_numbers(arg1, arg2) + .and_then(:validate_nonzero) .and_then(:divide) end private - def validate_numbers - arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric') # This will raise an error + def validate_numbers(arg1, arg2) + arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric') arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric') - Success(:ok, [arg1, arg2]) # This will raise an error + Success(:ok, [arg1, arg2]) end - def validate_non_zero(numbers) - ValidateNonZero.call(numbers) # This will raise an error - - # This would work: + def validate_nonzero(numbers) # In this case we are handling the other subject result and returning our own - # ValidateNonZero.call(numbers).handle do |on| - # on.success { |numbers| Success(:ok, numbers) } - # on.failure { |err| Failure(:division_by_zero, err) } - # end + ValidateNonzero.call(numbers).handle do |on| + on.success { |numbers| Success(:ok, numbers) } + + on.failure { |err| Failure(:division_by_zero, err) } + end end def divide((number1, number2)) @@ -769,11 +837,20 @@ class Divide end ``` +Look at the output of the code above: + +```ruby +Divide.call(2, 0) + +# +``` +

⬆️  back to top

##### Dependency Injection -The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the subject's method. To receive this argument, the subject's method must have an arity of two, where the first argument will be the result value and the second will be the shared value. +The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the subject's method. +To receive this argument, the subject's method must have an arity of two, where the first argument will be the result value and the second will be the shared value. ```ruby require 'logger' @@ -783,7 +860,7 @@ module Divide def call(arg1, arg2, logger: ::Logger.new(STDOUT)) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero, logger) + .and_then(:validate_nonzero, logger) .and_then(:divide, logger) end @@ -796,7 +873,7 @@ module Divide Success(:ok, [arg1, arg2]) end - def validate_non_zero(numbers, logger) + def validate_nonzero(numbers, logger) if numbers.last.zero? logger.error('arg2 must not be zero') @@ -830,9 +907,9 @@ Divide.call(4, 2, logger: Logger.new(IO::NULL)) ##### Add-ons -The `BCDD::Result.mixin` also accepts the `with:` argument. It is a hash that will be used to define the methods that will be added to the target object. +The `BCDD::Result.mixin` also accepts the `config:` argument. It is a hash that will be used to define custom behaviors for the mixin. -**Continue** +**continue** This addon will create the `Continue(value)` method, which will know how to produce a `Success(:continued, value)`. It is useful when you want to perform a sequence of operations but want to avoid returning a specific result for each step. @@ -842,7 +919,7 @@ module Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) end @@ -855,7 +932,7 @@ module Divide Continue([arg1, arg2]) end - def validate_non_zero(numbers) + def validate_nonzero(numbers) return Continue(numbers) unless numbers.last.zero? Failure(:division_by_zero, 'arg2 must not be zero') @@ -904,11 +981,11 @@ Look what happens if you try to create a result without one of the expected type ```ruby Divide::Result::Success(:ok) # type :ok is not allowed. Allowed types: :numbers, :division_completed -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) Divide::Result::Failure(:err) # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ``` The _**mixin mode**_ is similar to `BCDD::Result::Mixin`, but it also defines the expectations for the result's types and values. @@ -922,7 +999,7 @@ class Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) end @@ -935,7 +1012,7 @@ class Divide Success(:numbers, [arg1, arg2]) end - def validate_non_zero(numbers) + def validate_nonzero(numbers) return Success(:numbers, numbers) unless numbers.last.zero? Failure(:division_by_zero, 'arg2 must not be zero') @@ -975,7 +1052,7 @@ result.success?(:division_completed) # true result.success?(:ok) # type :ok is not allowed. Allowed types: :numbers, :division_completed -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ``` **Failure example:** @@ -989,7 +1066,7 @@ result.failure?(:division_by_zero) # false result.failure?(:err) # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ``` *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).* @@ -1011,7 +1088,7 @@ result result.on(:number) { |_| :this_type_does_not_exist } # type :number is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ``` *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).* @@ -1037,11 +1114,11 @@ result result.on_success(:ok) { |_| :this_type_does_not_exist } # type :ok is not allowed. Allowed types: :numbers, :division_completed -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) result.on_failure(:err) { |_| :this_type_does_not_exist } # type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ``` *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).* @@ -1058,17 +1135,17 @@ result = Divide.call(10, 2) result.handle do |on| on.type(:ok) { |_| :this_type_does_not_exist } end -# type :ok is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero (BCDD::Result::Expectations::Error::UnexpectedType) +# type :ok is not allowed. Allowed types: :numbers, :division_completed, :invalid_arg, :division_by_zero (BCDD::Result::Contract::Error::UnexpectedType) result.handle do |on| on.success(:ok) { |_| :this_type_does_not_exist } end -# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType) +# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Contract::Error::UnexpectedType) result.handle do |on| on.failure(:err) { |_| :this_type_does_not_exist } end -# type :err is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType) +# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Contract::Error::UnexpectedType) ``` *PS: The `Divide` implementation is [here](#standalone-versus-mixin-mode).* @@ -1099,11 +1176,11 @@ end Divide.call('4', 2) # type :invalid_arg is not allowed. Allowed types: :err -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) Divide.call(4, 2) # type :division_completed is not allowed. Allowed types: :ok -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ```

⬆️  back to top

@@ -1126,11 +1203,11 @@ end Divide.call('4', 2) # type :invalid_arg is not allowed. Allowed types: :err -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) Divide.call(4, 2) # type :division_completed is not allowed. Allowed types: :ok -# (BCDD::Result::Expectations::Error::UnexpectedType) +# (BCDD::Result::Contract::Error::UnexpectedType) ```

⬆️  back to top

@@ -1201,26 +1278,26 @@ The value validation will only be performed through the methods `Success()` and ```ruby Divide::Result::Success(:ok) -# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Expectations::Error::UnexpectedType) +# type :ok is not allowed. Allowed types: :numbers, :division_completed (BCDD::Result::Contract::Error::UnexpectedType) Divide::Result::Success(:numbers, [1]) -# value [1] is not allowed for :numbers type (BCDD::Result::Expectations::Error::UnexpectedValue) +# value [1] is not allowed for :numbers type (BCDD::Result::Contract::Error::UnexpectedValue) Divide::Result::Success(:division_completed, '2') -# value "2" is not allowed for :division_completed type (BCDD::Result::Expectations::Error::UnexpectedValue) +# value "2" is not allowed for :division_completed type (BCDD::Result::Contract::Error::UnexpectedValue) ``` ##### Failure() ```ruby Divide::Result::Failure(:err) -# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Expectations::Error::UnexpectedType) +# type :err is not allowed. Allowed types: :invalid_arg, :division_by_zero (BCDD::Result::Contract::Error::UnexpectedType) Divide::Result::Failure(:invalid_arg, :arg1_must_be_numeric) -# value :arg1_must_be_numeric is not allowed for :invalid_arg type (BCDD::Result::Expectations::Error::UnexpectedValue) +# value :arg1_must_be_numeric is not allowed for :invalid_arg type (BCDD::Result::Contract::Error::UnexpectedValue) Divide::Result::Failure(:division_by_zero, msg: 'arg2 must not be zero') -# value {:msg=>"arg2 must not be zero"} is not allowed for :division_by_zero type (BCDD::Result::Expectations::Error::UnexpectedValue) +# value {:msg=>"arg2 must not be zero"} is not allowed for :division_by_zero type (BCDD::Result::Contract::Error::UnexpectedValue) ```

⬆️  back to top

@@ -1263,14 +1340,14 @@ module Divide end Divide.call(10, 5) -# value "5" is not allowed for :division_completed type ("5": Float === "5" does not return true) (BCDD::Result::Contract::Error::UnexpectedValue) +# value "2" is not allowed for :division_completed type ("2": Float === "2" does not return true) (BCDD::Result::Contract::Error::UnexpectedValue) ```

⬆️  back to top

#### `BCDD::Result::Expectations.mixin` add-ons -The `BCDD::Result::Expectations.mixin` also accepts the `with:` argument. It is a hash that will be used to define the methods that will be added to the target object. +The `BCDD::Result::Expectations.mixin` also accepts the `config:` argument. It is a hash that can be used to define custom behaviors for the mixin. **Continue** @@ -1286,7 +1363,7 @@ class Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) end @@ -1299,7 +1376,7 @@ class Divide Continue([arg1, arg2]) end - def validate_non_zero(numbers) + def validate_nonzero(numbers) return Continue(numbers) unless numbers.last.zero? Failure(:division_by_zero, 'arg2 must not be zero') @@ -1310,7 +1387,7 @@ class Divide end end -result = Divide.new.call(4,2) +result = Divide.new.call(4, 2) # => # # The example below shows an error because the :ok type is not allowed. @@ -1318,7 +1395,7 @@ result = Divide.new.call(4,2) # This is because the :continued type is ignored by the expectations. # result.success?(:ok) -# type :ok is not allowed. Allowed types: :division_completed (BCDD::Result::Expectations::Error::UnexpectedType) +# type :ok is not allowed. Allowed types: :division_completed (BCDD::Result::Contract::Error::UnexpectedType) ```

⬆️  back to top

@@ -1368,14 +1445,14 @@ Let's see this feature and the data accumulation in action: ##### Class example (Instance Methods) ```ruby -class Divide - require 'logger' +require 'logger' +class Divide include BCDD::Result::Context.mixin def call(arg1, arg2, logger: ::Logger.new(STDOUT)) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide, logger: logger) end @@ -1388,7 +1465,7 @@ class Divide Success(:ok, number1: arg1, number2: arg2) end - def validate_non_zero(number2:, **) + def validate_nonzero(number2:, **) return Success(:ok) if number2.nonzero? Failure(:err, message: 'arg2 must not be zero') @@ -1434,8 +1511,9 @@ class Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) + .and_expose(:division_completed, [:number]) end private @@ -1447,7 +1525,7 @@ class Divide Success(:ok, number1: arg1, number2: arg2) end - def validate_non_zero(number2:, **) + def validate_nonzero(number2:, **) return Success(:ok) if number2.nonzero? Failure(:err, message: 'arg2 must not be zero') @@ -1483,7 +1561,7 @@ module Divide def call(arg1, arg2) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide) .and_expose(:division_completed, [:number]) end @@ -1497,7 +1575,7 @@ module Divide Success(:ok, number1: arg1, number2: arg2) end - def validate_non_zero(number2:, **) + def validate_nonzero(number2:, **) return Success(:ok) if number2.nonzero? Failure(:err, message: 'arg2 must not be zero') @@ -1577,7 +1655,7 @@ Divide::Result::Success(:division_completed, number: '2') #### Mixin add-ons -The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin` also accepts the `with:` argument. And it works the same way as the `BCDD::Result` mixins. +The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin` also accepts the `config:` argument. And it works the same way as the `BCDD::Result` mixins. **Continue** @@ -1608,7 +1686,7 @@ module Divide def call(arg1, arg2, logger: ::Logger.new(STDOUT)) validate_numbers(arg1, arg2) - .and_then(:validate_non_zero) + .and_then(:validate_nonzero) .and_then(:divide, logger: logger) .and_expose(:division_completed, [:number]) end @@ -1622,7 +1700,7 @@ module Divide Continue(number1: arg1, number2: arg2) end - def validate_non_zero(number2:, **) + def validate_nonzero(number2:, **) return Continue() if number2.nonzero? Failure(:division_by_zero, message: 'arg2 must not be zero') From eee8fa2fdf34df86c82cf7be82dfc87d7a81124e Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 10:18:21 -0300 Subject: [PATCH 28/30] Update CHANGELOG.md --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d57f9..e394679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +- [\[Unreleased\]](#unreleased) + - [Added](#added) + - [Changed](#changed) + - [Removed](#removed) +- [\[0.7.0\] - 2023-10-27](#070---2023-10-27) + - [Added](#added-1) + - [Changed](#changed-1) +- [\[0.6.0\] - 2023-10-11](#060---2023-10-11) + - [Added](#added-2) + - [Changed](#changed-2) +- [\[0.5.0\] - 2023-10-09](#050---2023-10-09) + - [Added](#added-3) +- [\[0.4.0\] - 2023-09-28](#040---2023-09-28) + - [Added](#added-4) + - [Changed](#changed-3) + - [Removed](#removed-1) +- [\[0.3.0\] - 2023-09-26](#030---2023-09-26) + - [Added](#added-5) +- [\[0.2.0\] - 2023-09-26](#020---2023-09-26) + - [Added](#added-6) + - [Removed](#removed-2) +- [\[0.1.0\] - 2023-09-25](#010---2023-09-25) + - [Added](#added-7) + ## [Unreleased] ### Added @@ -41,7 +65,7 @@ config.pattern_matching.disable!(:nil_as_valid_value_checking) - # config.feature.disable!(:expectations) if ::Rails.env.production? + config.feature.disable!(:expectations) if ::Rails.env.production? end BCDD::Result.config.addon.enabled?(:continue) # true @@ -51,6 +75,24 @@ BCDD::Result.config.constant_alias.disable!('Result') # raises FrozenError ``` +- Allow the pattern matching feature to be turned on/off through the `BCDD::Result::Expectations.mixin`. Now, it can be used without enabling it for the whole project. + ```ruby + extend BCDD::Result::Expectations.mixin( + config: { + addon: { continue: false }, + pattern_matching: { nil_as_valid_value_checking: true }, + }, + success: { + numbers: ->(value) { value => [Numeric, Numeric] }, + division_completed: Numeric + }, + failure: { + invalid_arg: String, + division_by_zero: String + } + ) + ``` + ### Changed - **(BREAKING)** Replace `BCDD::Result::Contract.nil_as_valid_value_checking!` with `BCDD::Result::Config.pattern_matching.enable!(:nil_as_valid_value_checking)`. From 160ac4d8626b28e295a1678fcf74864eeb0e2c3b Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 10:55:48 -0300 Subject: [PATCH 29/30] Update README.md --- README.md | 154 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 129 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d301ddb..0777bb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

🔀 BCDD::Result

-

Empower Ruby apps with a pragmatic use of Railway Oriented Programming.

+

Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.

Ruby bcdd-result gem version @@ -69,6 +69,12 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi - [Module example (Singleton Methods)](#module-example-singleton-methods-1) - [`BCDD::Result::Context::Expectations`](#bcddresultcontextexpectations) - [Mixin add-ons](#mixin-add-ons) + - [`BCDD::Result.configuration`](#bcddresultconfiguration) + - [`config.addon.enable!(:continue)`](#configaddonenablecontinue) + - [`config.constant_alias.enable!('Result')`](#configconstant_aliasenableresult) + - [`config.pattern_matching.disable!(:nil_as_valid_value_checking)`](#configpattern_matchingdisablenil_as_valid_value_checking) + - [`config.feature.disable!(:expectations)`](#configfeaturedisableexpectations) + - [`BCDD::Result.config`](#bcddresultconfig) - [About](#about) - [Development](#development) - [Contributing](#contributing) @@ -1308,21 +1314,14 @@ The value checking has support for handling pattern-matching errors, and the cle How does this operator work? They raise an error when the pattern does not match but returns nil when it matches. -Because of this, you will need to enable `nil` as a valid value checking. You can do it by calling the `BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking)` method. - -**Attention:** - -If you decide to enable this, you will do it at the beginning of your code or in an initializer. And remember, this will affect all kinds of result expectations (`BCDD::Result::Expectations` and `BCDD::Result::Context::Expectations`). So, it is recommended to use it only when you are using pattern matching for **ALL** the result's value validations. +Because of this, you will need to enable `nil` as a valid value checking. You can do it through the `BCDD::Result.configuration` or by allowing it directly on the mixin config. ```ruby -# -# Put this line in an initializer or at the beginning of your code. -# It is required if you decide to use pattern matching to validate all of your result's values. -# -BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - module Divide extend BCDD::Result::Expectations.mixin( + config: { + pattern_matching: { nil_as_valid_value_checking: true } + }, success: { division_completed: ->(value) { value => (Integer | Float) } }, @@ -1608,14 +1607,11 @@ The `BCDD::Result::Context::Expectations` is a `BCDD::Result::Expectations` with This is an example using the mixin mode, but the standalone mode is also supported. ```ruby -# -# Put this line in an initializer or at the beginning of your code. -# It is required if you decide to use pattern matching to validate all of your result's values. -# -BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - class Divide include BCDD::Result::Context::Expectations.mixin( + config: { + pattern_matching: { nil_as_valid_value_checking: true } + }, success: { division_completed: ->(value) { value => { number: Numeric } } }, @@ -1664,17 +1660,14 @@ The `BCDD::Result::Context.mixin(config: { addon: { continue: true } })` or `BCD Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on: ```ruby -# -# Put this line in an initializer or at the beginning of your code. -# It is required if you decide to use pattern matching to validate all of your result's values. -# -BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking) - module Divide require 'logger' extend self, BCDD::Result::Context::Expectations.mixin( - config: { addon: { continue: true } }, + config: { + addon: { continue: true }, + pattern_matching: { nil_as_valid_value_checking: true } + }, success: { division_completed: ->(value) { value => { number: Numeric } } }, @@ -1729,8 +1722,119 @@ Divide.call(14, 0) #"arg2 must not be zero"}> ``` +### `BCDD::Result.configuration` + +The `BCDD::Result.configuration` allows you to configure default behaviors for `BCDD::Result` and `BCDD::Result::Context` through a configuration block. After using it, the configuration is frozen, ensuring the expected behaviors for your application. + +```ruby +BCDD::Result.configuration do |config| + config.addon.enable!(:continue) + + config.constant_alias.enable!('Result') + + config.pattern_matching.disable!(:nil_as_valid_value_checking) + + config.feature.disable!(:expectations) if ::Rails.env.production? +end +``` + +Use `disable!` to disable a feature and `enable!` to enable it. + +Let's see what each configuration in the example above does: + +#### `config.addon.enable!(:continue)` + +This configuration enables the `Continue()` method for `BCDD::Result` and `BCDD::Result::Context`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons). + +#### `config.constant_alias.enable!('Result')` + +This configuration make `Result` a constant alias for `BCDD::Result`. Link to [documentation](#bcddresult-versus-result). + +#### `config.pattern_matching.disable!(:nil_as_valid_value_checking)` + +This configuration disables the `nil_as_valid_value_checking` for `BCDD::Result` and `BCDD::Result::Context`. Link to [documentation](#pattern-matching-support). +

⬆️  back to top

+#### `config.feature.disable!(:expectations)` + +This configuration turns off the expectations for `BCDD::Result` and `BCDD::Result::Context`. The expectations are helpful in development and test environments, but they can be disabled in production environments for performance gain. + +PS: I'm using `::Rails.env.production?` to check the environment, but you can use any logic you want. + +### `BCDD::Result.config` + +The `BCDD::Result.config` allows you to access the current configuration. It is useful when you want to check the current configuration. + +**BCDD::Result.config.addon** + +```ruby +BCDD::Result.config.addon.enabled?(:continue) + +BCDD::Result.config.addon.options +# { +# :continue=>{ +# :enabled=>false, +# :affects=>[ +# "BCDD::Result", +# "BCDD::Result::Context", +# "BCDD::Result::Expectations", +# "BCDD::Result::Context::Expectations" +# ] +# } +# } +``` + +**BCDD::Result.config.constant_alias** + +```ruby +BCDD::Result.config.constant_alias.enabled?('Result') + +BCDD::Result.config.constant_alias.options +# { +# "Result"=>{ +# :enabled=>false, +# :affects=>[ +# "Object" +# ] +# } +# } +``` + +**BCDD::Result.config.pattern_matching** + +```ruby +BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking) + +BCDD::Result.config.pattern_matching.options +# { +# :nil_as_valid_value_checking=>{ +# :enabled=>false, +# :affects=>[ +# "BCDD::Result::Expectations, +# "BCDD::Result::Context::Expectations" +# ] +# } +# } +``` + +**BCDD::Result.config.feature** + +```ruby +BCDD::Result.config.feature.enabled?(:expectations) + +BCDD::Result.config.feature.options +# { +# :expectations=>{ +# :enabled=>true, +# :affects=>[ +# "BCDD::Result::Expectations, +# "BCDD::Result::Context::Expectations" +# ] +# } +# } +``` + ## About [Rodrigo Serradura](https://github.com/serradura) created this project. He is the B/CDD process/method creator and has already made similar gems like the [u-case](https://github.com/serradura/u-case) and [kind](https://github.com/serradura/kind/blob/main/lib/kind/result.rb). This gem is a general-purpose abstraction/monad, but it also contains key features that serve as facilitators for adopting B/CDD in the code. From 20ef3a3b772b139d5ee9df7c4846b18cad9934aa Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Mon, 11 Dec 2023 10:56:00 -0300 Subject: [PATCH 30/30] Update bcdd-result.gemspec --- bcdd-result.gemspec | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bcdd-result.gemspec b/bcdd-result.gemspec index 186cb3d..eb6b1a4 100644 --- a/bcdd-result.gemspec +++ b/bcdd-result.gemspec @@ -8,11 +8,9 @@ Gem::Specification.new do |spec| spec.authors = ['Rodrigo Serradura'] spec.email = ['rodrigo.serradura@gmail.com'] - spec.summary = 'A pragmatic result abstraction (monad based) for Ruby.' - spec.description = - "Empower Ruby apps with a pragmatic use of Railway Oriented Programming.\n\n" \ - "It's a general-purpose result monad that allows you to create objects representing " \ - 'a success (BCDD::Result::Success) or failure (BCDD::Result::Failure).' + spec.summary = 'Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.' + + spec.description = 'Empower Ruby apps with pragmatic use of Result monad, Railway Oriented Programming, and B/CDD.' spec.homepage = 'https://github.com/b-cdd/result' spec.license = 'MIT'