Skip to content

Commit

Permalink
Merge pull request #5 from B-CDD/feature/pattern_matching
Browse files Browse the repository at this point in the history
Add pattern matching support
  • Loading branch information
serradura committed Sep 28, 2023
2 parents 3a2630f + 2544556 commit dd9643e
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added

- Add support to pattern matching (Ruby 2.7+).

- Add `BCDD::Result#on_unknown` to execute a block if no other hook (`#on`, `#on_type`, `#on_failure`, `#on_success`) has been executed. Attention: always use it as the last hook.

- Add `BCDD::Result::Handler#unknown` to execute a block if no other handler (`#[]`, `#type`, `#failure`, `#success`) has been executed. Attention: always use it as the last handler.
Expand Down
98 changes: 81 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
- [Class example (instance methods)](#class-example-instance-methods)
- [Module example (singleton methods)](#module-example-singleton-methods)
- [Restrictions](#restrictions)
- [Pattern Matching](#pattern-matching)
- [`Array`/`Find` patterns](#arrayfind-patterns)
- [`Hash` patterns](#hash-patterns)
- [About](#about)
- [Development](#development)
- [Contributing](#contributing)
Expand All @@ -62,7 +65,7 @@ If bundler is not being used to manage dependencies, install the gem by executin

$ gem install bcdd-result

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## Usage

Expand All @@ -82,7 +85,7 @@ BCDD::Result::Success(:ok) #
BCDD::Result::Failure(:err) #
```

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## Reference

Expand Down Expand Up @@ -138,7 +141,7 @@ result.type # :no
result.value # nil
```

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### Receiving types in `result.success?` or `result.failure?`

Expand All @@ -164,7 +167,7 @@ result.failure?(:err) # true
result.failure?(:error) # false
```

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

### Result Hooks

Expand All @@ -183,7 +186,7 @@ def divide(arg1, arg2)
end
```

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `result.on`

Expand Down Expand Up @@ -221,7 +224,7 @@ result.object_id == output.object_id # true

*PS: The `divide()` implementation is [here](#result-hooks).*

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `result.on_type`

Expand Down Expand Up @@ -250,7 +253,7 @@ divide(4, 4).on_failure { |error| puts error }

*PS: The `divide()` implementation is [here](#result-hooks).*

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `result.on_failure`

Expand All @@ -272,7 +275,7 @@ divide(4, 0).on_failure(:invalid_arg) { |error| puts error }

*PS: The `divide()` implementation is [here](#result-hooks).*

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `result.on_unknown`

Expand All @@ -293,7 +296,7 @@ divide(4, 2)

*PS: The `divide()` implementation is [here](#result-hooks).*

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `result.handle`

Expand Down Expand Up @@ -343,7 +346,7 @@ end

*PS: The `divide()` implementation is [here](#result-hooks).*

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

### Result Value

Expand Down Expand Up @@ -376,7 +379,7 @@ divide(100, 0).value_or { 0 } # 0

*PS: The `divide()` implementation is [here](#result-hooks).*

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `result.data_or`

Expand Down Expand Up @@ -442,7 +445,7 @@ Divide.call(2, 2)
#<BCDD::Result::Success type=:division_completed data=1>
```

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `BCDD::Resultable`

Expand Down Expand Up @@ -548,33 +551,94 @@ If you use `BCDD::Result::Subject()`/`BCDD::Result::Failure()`, or call another

> **Note**: You still can use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

### Pattern Matching

The `BCDD::Result` also provides support to pattern matching.

In the further examples, I will use the `Divide` lambda to exemplify its usage.

```ruby
Divide = lambda do |arg1, arg2|
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')

return BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?

BCDD::Result::Success(:division_completed, arg1 / arg2)
end
```

#### `Array`/`Find` patterns

```ruby
case Divide.call(4, 2)
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
in BCDD::Result::Success[:division_completed, value] then puts value
end

# The code above will print: 2

case Divide.call(4, 0)
in BCDD::Result::Failure[:invalid_arg, msg] then puts msg
in BCDD::Result::Failure[:division_by_zero, msg] then puts msg
in BCDD::Result::Success[:division_completed, value] then puts value
end

# The code above will print: arg2 must not be zero
```

<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

#### `Hash` patterns

```ruby
case Divide.call(10, 2)
in { failure: { invalid_arg: msg } } then puts msg
in { failure: { division_by_zero: msg } } then puts msg
in { success: { division_completed: value } } then puts value
end

# The code above will print: 5

case Divide.call('10', 2)
in { failure: { invalid_arg: msg } } then puts msg
in { failure: { division_by_zero: msg } } then puts msg
in { success: { division_completed: value } } then puts value
end

# The code above will print: arg1 must be numeric
```

<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## 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.

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/B-CDD/bcdd-result. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/B-CDD/bcdd-result/blob/master/CODE_OF_CONDUCT.md).

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

<p align="right">(<a href="#-bcddresult">⬆️ &nbsp;back to top</a>)</p>
<p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>

## Code of Conduct

Expand Down
12 changes: 12 additions & 0 deletions lib/bcdd/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,25 @@ def inspect
format('#<%<class_name>s type=%<type>p value=%<value>p>', class_name: self.class.name, type: type, value: value)
end

def deconstruct
[type, value]
end

def deconstruct_keys(_keys)
{ name => { type => value } }
end

alias eql? ==
alias data value
alias data_or value_or
alias on_type on

private

def name
raise Error::NotImplemented
end

def known(block)
self.unknown = false

Expand Down
6 changes: 6 additions & 0 deletions lib/bcdd/result/failure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def value_or
end

alias data_or value_or

private

def name
:failure
end
end

def self.Failure(type, value = nil)
Expand Down
6 changes: 6 additions & 0 deletions lib/bcdd/result/success.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def value_or
end

alias data_or value_or

private

def name
:success
end
end

def self.Success(type, value = nil)
Expand Down
6 changes: 4 additions & 2 deletions sig/bcdd/result.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,20 @@ class BCDD::Result
def handle: { (BCDD::Result::Handler) -> void } -> untyped

def ==: (untyped) -> bool

def hash: -> Integer

def inspect: -> String

def deconstruct: -> [Symbol, [Symbol, untyped]]
def deconstruct_keys: (Array[Symbol]) -> Hash[Symbol, Hash[Symbol, untyped]]

alias eql? ==
alias data value
alias data_or value_or
alias on_type on

private

def name: -> Symbol
def known: (Proc) -> untyped
def call_subject_method: (Symbol) -> BCDD::Result
def ensure_result_object: (untyped, origin: Symbol) -> BCDD::Result
Expand Down
12 changes: 12 additions & 0 deletions test/bcdd/result_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ class ResultTest < Minitest::Test
assert_raises(BCDD::Result::Error::NotImplemented) { result.value_or { 0 } }
end

test '#deconstruct' do
result = Result.new(type: :ok, value: 1)

assert_equal([:ok, 1], result.deconstruct)
end

test '#deconstruct_keys' do
result = Result.new(type: :ok, value: 1)

assert_raises(BCDD::Result::Error::NotImplemented) { result.deconstruct_keys([]) }
end

test '#==' do
result = Result.new(type: :ok, value: 2)

Expand Down
43 changes: 43 additions & 0 deletions test/pattern_matching/deconstruct_keys_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require 'test_helper'

class BCDD::PatternMatchingDeconstructKeysTest < Minitest::Test
Divide = lambda do |arg1, arg2|
arg1.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg1 must be numeric')
arg2.is_a?(::Numeric) or return BCDD::Result::Failure(:invalid_arg, 'arg2 must be numeric')

return BCDD::Result::Failure(:division_by_zero, 'arg2 must not be zero') if arg2.zero?

BCDD::Result::Success(:division_completed, arg1 / arg2)
end

test '#deconstruct_keys success' do
result = Divide.call(10, 2)

assert_equal({ success: { division_completed: 5 } }, result.deconstruct_keys([]))

case result
in { failure: _ }
raise
in { success: { division_completed: value } }
assert_equal 5, value
end
end

test '#deconstruct_keys failure' do
result = Divide.call(10, 0)

assert_equal(
{ failure: { division_by_zero: 'arg2 must not be zero' } },
result.deconstruct_keys([])
)

case result
in { success: _ }
raise
in { failure: { division_by_zero: msg } }
assert_equal 'arg2 must not be zero', msg
end
end
end
Loading

0 comments on commit dd9643e

Please sign in to comment.