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 }}
diff --git a/.rubocop.yml b/.rubocop.yml
index 0636819..00f580a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -65,3 +65,10 @@ Minitest/MultipleAssertions:
Minitest/AssertEmptyLiteral:
Enabled: false
+
+Minitest/AssertOperator:
+ Enabled: false
+
+Naming/FileName:
+ Exclude:
+ - lib/bcdd-result.rb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e99485d..e394679 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,122 @@
+- [\[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
+
+- Add `BCDD::Result.config`
+ - **Feature**
+ ```ruby
+ 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.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
+ ```
+
+- 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)`.
+
+- **(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.
+
+- **(BREAKING)** Change the addons definition.
+ - **From**
+ ```ruby
+ BCDD::Result.mixin(with: :Continue)
+ BCDD::Result.mixin(with: [:Continue])
+ ```
+ - **To**
+ ```ruby
+ 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`)
+
+### 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
### Added
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'
diff --git a/README.md b/README.md
index 3f809e7..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.
@@ -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)
@@ -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)
@@ -84,7 +90,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 +101,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 +129,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 +189,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 +207,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 +220,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 +232,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 +244,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 +308,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 +328,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 +355,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 }
+divide(4, 0).on_failure(:division_by_zero) { |error| puts error }
-# It doesn't execute the block, but return itself.
-
-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 +599,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 +612,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 +644,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()`.
-As a result, you can utilize the `#and_then` method to invoke methods from the result's subject.
+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.
+
+Because the result has a subject, the `#and_then` method can call methods from it.
##### Class example (Instance Methods)
@@ -634,7 +665,7 @@ class Divide
def call
validate_numbers
- .and_then(:validate_non_zero)
+ .and_then(:validate_nonzero)
.and_then(:divide)
end
@@ -649,7 +680,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 +706,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 +719,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 +748,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 +758,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 BCDD::Result::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')
- BCDD::Result::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 +843,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 +866,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 +879,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,19 +913,19 @@ 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.
```ruby
module Divide
- extend self, BCDD::Result.mixin(with: :Continue)
+ extend self, BCDD::Result.mixin(config: { addon: { continue: true } })
def call(arg1, arg2)
validate_numbers(arg1, arg2)
- .and_then(:validate_non_zero)
+ .and_then(:validate_nonzero)
.and_then(:divide)
end
@@ -855,7 +938,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 +987,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 +1005,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 +1018,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')
@@ -947,10 +1030,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.
@@ -975,7 +1058,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 +1072,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 +1094,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 +1120,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 +1141,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 +1182,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 +1209,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 +1284,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
@@ -1231,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::Contract.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::Contract.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) }
},
@@ -1263,30 +1339,30 @@ 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**
-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: { 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(
- with: :Continue,
+ config: { addon: { continue: true } },
success: :division_completed,
failure: %i[invalid_arg division_by_zero]
)
def call(arg1, arg2)
validate_numbers(arg1, arg2)
- .and_then(:validate_non_zero)
+ .and_then(:validate_nonzero)
.and_then(:divide)
end
@@ -1299,7 +1375,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 +1386,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 +1394,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 +1444,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 +1464,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 +1510,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 +1524,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 +1560,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 +1574,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')
@@ -1530,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::Contract.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 } }
},
@@ -1553,7 +1627,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
@@ -1577,26 +1651,23 @@ 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**
-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: { 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:
```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::Contract.nil_as_valid_value_checking!
-
module Divide
require 'logger'
extend self, BCDD::Result::Context::Expectations.mixin(
- with: :Continue,
+ config: {
+ addon: { continue: true },
+ pattern_matching: { nil_as_valid_value_checking: true }
+ },
success: {
division_completed: ->(value) { value => { number: Numeric } }
},
@@ -1608,7 +1679,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 +1693,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')
@@ -1651,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.
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'
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/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'
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'
diff --git a/lib/bcdd/result.rb b/lib/bcdd/result.rb
index fd57f4e..47b3a4a 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'
@@ -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/lib/bcdd/result/config.rb b/lib/bcdd/result/config.rb
new file mode 100644
index 0000000..8fa6f93
--- /dev/null
+++ b/lib/bcdd/result/config.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'singleton'
+
+require_relative 'config/options'
+require_relative 'config/switcher'
+require_relative 'config/constant_alias'
+
+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
+
+ 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,
+ affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
+ }
+ }.transform_values!(&:freeze).freeze
+
+ 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
+
+ super
+ end
+
+ def options
+ {
+ addon: addon,
+ feature: feature,
+ constant_alias: constant_alias,
+ pattern_matching: pattern_matching
+ }
+ end
+
+ def to_h
+ options.transform_values(&:to_h)
+ end
+
+ def inspect
+ "#<#{self.class.name} options=#{options.keys.sort.inspect}>"
+ end
+
+ private_constant :ADDON, :FEATURE, :PATTERN_MATCHING
+ end
+end
diff --git a/lib/bcdd/result/config/constant_alias.rb b/lib/bcdd/result/config/constant_alias.rb
new file mode 100644
index 0000000..70f3fe2
--- /dev/null
+++ b/lib/bcdd/result/config/constant_alias.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class BCDD::Result
+ class Config
+ module ConstantAlias
+ RESULT = 'Result'
+
+ OPTIONS = {
+ RESULT => { default: false, affects: %w[Object] }
+ }.transform_values!(&:freeze).freeze
+
+ MAPPING = {
+ RESULT => { target: ::Object, name: :Result, value: ::BCDD::Result }
+ }.transform_values!(&:freeze).freeze
+
+ Listener = ->(option_name, boolean) do
+ mapping = MAPPING.fetch(option_name)
+
+ target, name, value = mapping.fetch_values(:target, :name, :value)
+
+ defined = target.const_defined?(name, false)
+
+ 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
+ end
+end
diff --git a/lib/bcdd/result/config/options.rb b/lib/bcdd/result/config/options.rb
new file mode 100644
index 0000000..dca6887
--- /dev/null
+++ b/lib/bcdd/result/config/options.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BCDD::Result
+ class Config
+ module Options
+ def self.with_defaults(all_flags, config)
+ all_flags ||= {}
+
+ default_flags = Config.instance.to_h.fetch(config)
+
+ config_flags = all_flags.fetch(config, {})
+
+ default_flags.merge(config_flags).slice(*default_flags.keys)
+ end
+
+ def self.filter_map(all_flags, config:, from:)
+ with_defaults(all_flags, config)
+ .filter_map { |name, truthy| from[name] if truthy }
+ end
+
+ def self.addon(map:, from:)
+ filter_map(map, config: :addon, from: from)
+ end
+ end
+ end
+end
diff --git a/lib/bcdd/result/config/switcher.rb b/lib/bcdd/result/config/switcher.rb
new file mode 100644
index 0000000..8db30c4
--- /dev/null
+++ b/lib/bcdd/result/config/switcher.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+class BCDD::Result
+ class Config
+ class Switcher
+ attr_reader :_options, :_affects, :listener
+
+ private :_options, :_affects, :listener
+
+ def initialize(options:, listener: nil)
+ @_options = options.transform_values { _1.fetch(:default) }
+ @_affects = options.transform_values { _1.fetch(:affects) }
+ @listener = listener
+ end
+
+ def inspect
+ "#<#{self.class.name} options=#{_options.inspect}>"
+ end
+
+ def freeze
+ _options.freeze
+ super
+ end
+
+ def to_h
+ _options.dup
+ 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
diff --git a/lib/bcdd/result/context/expectations.rb b/lib/bcdd/result/context/expectations.rb
index 9c189ed..c34db70 100644
--- a/lib/bcdd/result/context/expectations.rb
+++ b/lib/bcdd/result/context/expectations.rb
@@ -1,27 +1,19 @@
# 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, with: nil)
- addons = Mixin::Addons.options(with)
-
- 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
+ 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)
end
@@ -29,13 +21,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 f2f3572..99a5cf6 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
@@ -25,10 +13,10 @@ module Continuable
end
end
- OPTIONS = { Continue: Continuable }.freeze
+ OPTIONS = { continue: Continuable }.freeze
- def self.options(names)
- Array(names).filter_map { |name| OPTIONS[name] }
+ def self.options(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 34d404f..cd99bf3 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)
@@ -19,20 +21,21 @@ module Continuable
end
end
- OPTIONS = { Continue: Continuable }.freeze
+ OPTIONS = { continue: Continuable }.freeze
- def self.options(names)
- Array(names).filter_map { |name| OPTIONS[name] }
+ def self.options(config_flags)
+ ::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
end
end
end
- def self.mixin(with: nil)
- addons = Mixin::Addons.options(with)
+ def self.mixin_module
+ Mixin
+ end
- mod = ::BCDD::Result::Mixin.module!
- mod.send(:include, Mixin::Methods)
- mod.send(:include, *addons) unless addons.empty?
- mod
+ def self.result_factory
+ ::BCDD::Result::Context
end
+
+ private_class_method :mixin_module, :result_factory
end
diff --git a/lib/bcdd/result/contract.rb b/lib/bcdd/result/contract.rb
index 21a8c96..2d9b137 100644
--- a/lib/bcdd/result/contract.rb
+++ b/lib/bcdd/result/contract.rb
@@ -19,24 +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])
- 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
+ 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 9cf8a4f..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,7 +34,7 @@ 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 || (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..58b6127 100644
--- a/lib/bcdd/result/expectations.rb
+++ b/lib/bcdd/result/expectations.rb
@@ -4,22 +4,46 @@ 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(**options)
+ return mixin!(**options) if Config.instance.feature.enabled?(:expectations)
- mod = Mixin.module!
- mod.const_set(:Result, new(success: success, failure: failure).freeze)
- mod.module_eval(Mixin::METHODS)
+ 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!
+ 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
end
- def initialize(subject: nil, success: nil, failure: nil, contract: nil)
+ def self.mixin_module
+ Mixin
+ end
+
+ 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)
@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/lib/bcdd/result/expectations/mixin.rb b/lib/bcdd/result/expectations/mixin.rb
index 63d765b..e9b73ef 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(...)
@@ -25,17 +34,10 @@ module Continuable
end
end
- OPTIONS = { Continue: Continuable }.freeze
-
- def self.options(names)
- Array(names).filter_map { |name| OPTIONS[name] }
- end
- end
+ OPTIONS = { continue: Continuable }.freeze
- 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
+ def self.options(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 ba46f99..6e115b1 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)
@@ -19,27 +28,31 @@ module Continuable
end
end
- OPTIONS = { Continue: Continuable }.freeze
-
- def self.options(names)
- Array(names).filter_map { |name| OPTIONS[name] }
- end
- end
+ OPTIONS = { continue: Continuable }.freeze
- 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
+ def self.options(config_flags)
+ Config::Options.addon(map: config_flags, from: OPTIONS)
end
end
end
- def self.mixin(with: nil)
- addons = Mixin::Addons.options(with)
+ def self.mixin(config: nil)
+ 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.const_set(:Result, result_factory)
mod.send(:include, *addons) unless addons.empty?
mod
end
+
+ def self.mixin_module
+ Mixin
+ end
+
+ def self.result_factory
+ ::BCDD::Result
+ end
+
+ private_class_method :mixin_module, :result_factory
end
diff --git a/lib/result.rb b/lib/result.rb
deleted file mode 100644
index e287cae..0000000
--- a/lib/result.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'bcdd/result'
-
-Object.const_set(:Result, BCDD::Result)
diff --git a/sig/bcdd/result.rbs b/sig/bcdd/result.rbs
index f126124..1c512a5 100644
--- a/sig/bcdd/result.rbs
+++ b/sig/bcdd/result.rbs
@@ -11,6 +11,9 @@ 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,
@@ -94,6 +97,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
@@ -111,13 +118,15 @@ 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
-
- def self.module!: -> Module
end
- def self.mixin: (?with: Array[Symbol]) -> Module
+ 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
@@ -225,16 +234,14 @@ module BCDD::Result::Contract
BCDD::Result::Contract::Evaluator
) -> BCDD::Result::Contract::TypeChecker
- ToEnsure: ^(Hash[Symbol, untyped] | Array[Symbol])
- -> BCDD::Result::Contract::Evaluator
+ ToEnsure: ^(Hash[Symbol, untyped] | Array[Symbol], Hash[Symbol, Hash[Symbol, bool]])
+ -> BCDD::Result::Contract::Interface
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
-
- def self.nil_as_valid_value_checking!: (?enabled: bool) -> void
- def self.nil_as_valid_value_checking?: -> bool
end
module BCDD::Result::Contract
@@ -309,7 +316,14 @@ 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
+
+ def nil_as_valid_value_checking?: -> bool
end
end
@@ -334,16 +348,35 @@ end
class BCDD::Result::Expectations
def self.mixin: (
- ?with: Symbol,
+ ?config: Hash[Symbol, Hash[Symbol, bool]],
?success: Hash[Symbol, untyped] | Array[Symbol],
?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 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],
?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
@@ -358,6 +391,10 @@ class BCDD::Result::Expectations
end
module BCDD::Result::Expectations::Mixin
+ module Factory
+ def self.module!: -> Module
+ end
+
METHODS: String
module Addons
@@ -367,10 +404,8 @@ 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
-
- def self.module!: -> Module
end
class BCDD::Result::Context < BCDD::Result
@@ -419,6 +454,8 @@ end
class BCDD::Result::Context
module Mixin
+ Factory: singleton(BCDD::Result::Mixin::Factory)
+
module Methods
def Success: (Symbol, **untyped) -> BCDD::Result::Context::Success
@@ -436,40 +473,27 @@ 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
- def self.mixin: (?with: Array[Symbol]) -> Module
+ def self.mixin_module: -> singleton(BCDD::Result::Context::Mixin)
+
+ def self.result_factory: -> singleton(BCDD::Result::Context)
end
-class BCDD::Result::Context::Expectations
- def self.mixin: (
- ?with: Symbol,
- ?success: Hash[Symbol, untyped] | Array[Symbol],
- ?failure: Hash[Symbol, untyped] | Array[Symbol]
- ) -> Module
+class BCDD::Result::Context::Expectations < BCDD::Result::Expectations
+ def self.mixin_module: -> singleton(BCDD::Result::Context::Expectations::Mixin)
- def initialize: (
- ?subject: untyped,
- ?success: Hash[Symbol, untyped] | Array[Symbol],
- ?failure: Hash[Symbol, untyped] | Array[Symbol],
- ?contract: BCDD::Result::Contract::Evaluator
- ) -> void
+ 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
-
- 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
@@ -478,6 +502,90 @@ 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
+
+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
+
+ def self.instance: -> BCDD::Result::Config
+
+ def initialize: -> void
+
+ def freeze: -> BCDD::Result::Config
+ def options: -> Hash[Symbol, BCDD::Result::Config::Switcher]
+ def to_h: -> Hash[Symbol, Hash[Symbol | String, bool]]
+end
+
+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
+
+ def initialize: (
+ 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
+
+ def enable!: (*(Symbol | String)) -> Hash[Symbol | String, Hash[Symbol, untyped]]
+
+ def disable!: (*(Symbol | String)) -> Hash[Symbol | String, Hash[Symbol, untyped]]
+
+ private
+
+ def set_many: (Array[Symbol | String], to: bool) -> Hash[Symbol | String, Hash[Symbol, untyped]]
+
+ def set_one: (Symbol | String, bool) -> void
+
+ def require_option!: (Array[Symbol | String]) -> void
+
+ def validate_option!: (Symbol | String) -> void
+
+ def available_options_message: -> String
+end
+
+module BCDD::Result::Config::ConstantAlias
+ RESULT: 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
+ def self.with_defaults: (
+ Hash[Symbol, Hash[Symbol, bool]],
+ 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
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..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(with: :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 89d5619..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(with: :Continue)
+ extend self, BCDD::Result.mixin(config: { addon: { continue: true } })
def call(arg1, arg2)
validate_numbers(arg1, arg2)
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/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..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(with: :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 7f33683..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(with: :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 e5b6479..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(
- with: :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(
- with: :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(
- with: :Continue,
+ config: { addon: { continue: true } },
success: {
division_completed: ->(value) {
case value
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 cc034a8..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,53 +5,57 @@
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
- Contract.nil_as_valid_value_checking!
+ BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking)
+
+ _Result2 = BCDD::Result::Expectations.new(success: contract)
- result = _Result::Success(:ok, 1)
+ result = _Result2::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
- _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
- Contract.nil_as_valid_value_checking!
+ BCDD::Result.config.pattern_matching.enable!(:nil_as_valid_value_checking)
+
+ _Result2 = BCDD::Result::Context::Expectations.new(success: contract)
- result = _Result::Success(:ok, number: 1)
+ result = _Result2::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
diff --git a/test/bcdd/result/expectations/with_subject/continue_test.rb b/test/bcdd/result/expectations/with_subject/continue_test.rb
index 698787e..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(
- with: :Continue,
+ config: { addon: { continue: true } },
success: :ok,
failure: :err
)
@@ -38,7 +38,7 @@ def divide((number1, number2))
class DivideTypes
include BCDD::Result::Expectations.mixin(
- with: :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(
- with: :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 254c07e..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(with: :Continue)
+ extend self, BCDD::Result.mixin(config: { addon: { continue: true } })
def call(arg1, arg2)
validate_numbers(arg1, arg2)
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
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)