Skip to content

Commit

Permalink
Merge pull request #41 from B-CDD/feat/add-hash-methods-to-bcdd-context
Browse files Browse the repository at this point in the history
Add some Hash's methods to BCDD::Context
  • Loading branch information
serradura authored Mar 25, 2024
2 parents 3e1cfed + 8ffa69a commit 7bf8ba6
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 41 deletions.
36 changes: 22 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,58 @@
- [\[Unreleased\]](#unreleased)
- [1.0.0 - 2024-03-16](#100---2024-03-16)
- [Added](#added)
- [1.0.0 - 2024-03-16](#100---2024-03-16)
- [Added](#added-1)
- [Changed](#changed)
- [\[0.13.0\] - 2024-02-01](#0130---2024-02-01)
- [Added](#added-1)
- [Added](#added-2)
- [Changed](#changed-1)
- [\[0.12.0\] - 2024-01-07](#0120---2024-01-07)
- [Added](#added-2)
- [Added](#added-3)
- [Changed](#changed-2)
- [\[0.11.0\] - 2024-01-02](#0110---2024-01-02)
- [Added](#added-3)
- [Added](#added-4)
- [Changed](#changed-3)
- [\[0.10.0\] - 2023-12-31](#0100---2023-12-31)
- [Added](#added-4)
- [Added](#added-5)
- [\[0.9.1\] - 2023-12-12](#091---2023-12-12)
- [Changed](#changed-4)
- [Fixed](#fixed)
- [\[0.9.0\] - 2023-12-12](#090---2023-12-12)
- [Added](#added-5)
- [Added](#added-6)
- [Changed](#changed-5)
- [\[0.8.0\] - 2023-12-11](#080---2023-12-11)
- [Added](#added-6)
- [Added](#added-7)
- [Changed](#changed-6)
- [Removed](#removed)
- [\[0.7.0\] - 2023-10-27](#070---2023-10-27)
- [Added](#added-7)
- [Added](#added-8)
- [Changed](#changed-7)
- [\[0.6.0\] - 2023-10-11](#060---2023-10-11)
- [Added](#added-8)
- [Added](#added-9)
- [Changed](#changed-8)
- [\[0.5.0\] - 2023-10-09](#050---2023-10-09)
- [Added](#added-9)
- [\[0.4.0\] - 2023-09-28](#040---2023-09-28)
- [Added](#added-10)
- [\[0.4.0\] - 2023-09-28](#040---2023-09-28)
- [Added](#added-11)
- [Changed](#changed-9)
- [Removed](#removed-1)
- [\[0.3.0\] - 2023-09-26](#030---2023-09-26)
- [Added](#added-11)
- [\[0.2.0\] - 2023-09-26](#020---2023-09-26)
- [Added](#added-12)
- [\[0.2.0\] - 2023-09-26](#020---2023-09-26)
- [Added](#added-13)
- [Removed](#removed-2)
- [\[0.1.0\] - 2023-09-25](#010---2023-09-25)
- [Added](#added-13)
- [Added](#added-14)

## [Unreleased]

### Added

- Add some Hash's methods to `BCDD::Context`. They are:
- `#slice` to extract only the desired keys.
- `#[]`, `#dig`, `#fetch` to access the values.
- `#values_at` and `#fetch_values` to get the values of the desired keys.

## 1.0.0 - 2024-03-16

### Added
Expand Down
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
- [`BCDD::Result::Expectations.mixin` add-ons](#bcddresultexpectationsmixin-add-ons)
- [`BCDD::Context`](#bcddcontext)
- [Defining successes and failures](#defining-successes-and-failures)
- [Constant aliases](#constant-aliases)
- [`BCDD::Context.mixin`](#bcddcontextmixin)
- [Class example (Instance Methods)](#class-example-instance-methods-1)
- [Hash methods](#hash-methods)
- [`and_expose`](#and_expose)
- [Module example (Singleton Methods)](#module-example-singleton-methods-1)
- [`BCDD::Context::Expectations`](#bcddcontextexpectations)
Expand Down Expand Up @@ -1436,20 +1434,33 @@ BCDD::Context::Success(:ok, **{ message: 'hashes can be converted to keyword arg

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

#### Constant aliases
#### Hash methods

You can configure `Context` or `BCDD::Context` as an alias for `BCDD::Context`. This is helpful to define a standard way to avoid the full constant name/path in your code.
The `BCDD::Context` only accepts hashes as its values. Because of this, its instances have some Hash's methods to query/access the values. The available methods are:

- `#slice` to extract only the desired keys.
- `#[]`, `#dig`, `#fetch` to access the values.
- `#values_at` and `#fetch_values` to get the values of the desired keys.

```ruby
BCDD::Result.configuration do |config|
config.context_alias.enable!('BCDD::Context')
result = BCDD::Context::Success(:ok, a: 1, b: 2, c: {d: 4})

# or
result[:a] # 1
result.fetch(:a) # 1
result.dig(:c, :d) # 4

config.context_alias.enable!('Context')
end
result.slice(:a, :b) # {:a=>1, :b=>2}

result.values_at(:a, :b) # [1, 2]
result.fetch_values(:a, :b) # [1, 2]
```

These methods are available for `BCDD::Context::Success` and `BCDD::Context::Failure` instances.

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

```ruby

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

#### `BCDD::Context.mixin`
Expand Down
44 changes: 34 additions & 10 deletions lib/bcdd/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def self.Failure(type, **value)
def initialize(type:, value:, source: nil, expectations: nil, terminal: nil)
value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'

@acc = {}
@memo = {}

super
end
Expand All @@ -32,14 +32,38 @@ def and_then(method_name = nil, **injected_value, &block)
def and_then!(source, **injected_value)
_call = injected_value.delete(:_call)

acc.merge!(injected_value)
memo.merge!(injected_value)

super(source, injected_value, _call: _call)
end

def [](key)
value[key]
end

def dig(...)
value.dig(...)
end

def fetch(...)
value.fetch(...)
end

def slice(...)
value.slice(...)
end

def values_at(...)
value.values_at(...)
end

def fetch_values(...)
value.fetch_values(...)
end

protected

attr_reader :acc
attr_reader :memo

private

Expand All @@ -54,31 +78,31 @@ def and_then!(source, **injected_value)
end

def call_and_then_source_method!(method, injected_value)
acc.merge!(value.merge(injected_value))
memo.merge!(value.merge(injected_value))

case SourceMethodArity[method]
when 0 then source.send(method.name)
when 1 then source.send(method.name, **acc)
when 1 then source.send(method.name, **memo)
else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 1)
end
end

def call_and_then_block!(block)
acc.merge!(value)
memo.merge!(value)

block.call(acc)
block.call(memo)
end

def call_and_then_callable!(source, value:, injected_value:, method_name:)
acc.merge!(value.merge(injected_value))
memo.merge!(value.merge(injected_value))

CallableAndThen::Caller.call(source, value: acc, injected_value: injected_value, method_name: method_name)
CallableAndThen::Caller.call(source, value: memo, injected_value: injected_value, method_name: method_name)
end

def ensure_result_object(result, origin:)
raise_unexpected_outcome_error(result, origin) unless result.is_a?(BCDD::Context)

return result.tap { _1.acc.merge!(acc) } if result.source.equal?(source)
return result.tap { _1.memo.merge!(memo) } if result.source.equal?(source)

raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/bcdd/context/callable_and_then.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def self.call_method!(source, method, value, _injected_value)
end

def self.ensure_result_object(source, value, result)
return result.tap { result.send(:acc).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)
return result.tap { result.send(:memo).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)

raise Result::Error::UnexpectedOutcome.build(outcome: result, origin: source,
expected: Context::EXPECTED_OUTCOME)
Expand Down
10 changes: 5 additions & 5 deletions lib/bcdd/context/success.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ class Error < BCDD::Result::Error
class Success < self
include ::BCDD::Success

FetchValues = ->(acc_values, keys) do
fetched_values = acc_values.fetch_values(*keys)
FetchValues = ->(memo_values, keys) do
fetched_values = memo_values.fetch_values(*keys)

keys.zip(fetched_values).to_h
rescue ::KeyError => e
message = "#{e.message}. Available to expose: #{acc_values.keys.map(&:inspect).join(', ')}"
message = "#{e.message}. Available to expose: #{memo_values.keys.map(&:inspect).join(', ')}"

raise Error::InvalidExposure, message
end
Expand All @@ -25,9 +25,9 @@ def and_expose(type, keys, terminal: true)

EventLogs.tracking.reset_and_then!

acc_values = acc.merge(value)
memo_values = memo.merge(value)

value_to_expose = FetchValues.call(acc_values, keys)
value_to_expose = FetchValues.call(memo_values, keys)

expectations = type_checker.expectations

Expand Down
2 changes: 1 addition & 1 deletion sig/bcdd/context.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class BCDD::Context < BCDD::Result

SourceMethodArity: ^(Method) -> Integer

attr_reader acc: Hash[Symbol, untyped]
attr_reader memo: Hash[Symbol, untyped]

def initialize: (
type: Symbol,
Expand Down
89 changes: 89 additions & 0 deletions test/bcdd/context/success_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,94 @@ class ContextSuccessTest < Minitest::Test
result.inspect
)
end

test '#[]' do
result1 = Context::Success(:ok)
result2 = Context::Success(:ok, a: 1, b: 2)

assert_nil result1[:a]
assert_nil result1[:b]

assert_equal 1, result2[:a]
assert_equal 2, result2[:b]
end

# rubocop:disable Style/SingleArgumentDig
test '#dig' do
result1 = Context::Success(:ok)
result2 = Context::Success(:ok, a: { b: 1 })

assert_nil result1.dig(:a, :b)
assert_nil result2.dig(:a, :c)

assert_equal({ b: 1 }, result2.dig(:a))
assert_equal 1, result2.dig(:a, :b)
end
# rubocop:enable Style/SingleArgumentDig

test '#fetch' do
result1 = Context::Success(:ok)
result2 = Context::Success(:ok, a: 1, b: 2)

assert_raises(KeyError) { result1.fetch(:a) }
assert_raises(KeyError) { result1.fetch(:b) }

assert_equal 1, result2.fetch(:a)
assert_equal 2, result2.fetch(:b)

# ---

assert_equal 3, result1.fetch(:a, 3)
assert_equal 4, result1.fetch(:b, 4)

assert_equal 1, result2.fetch(:a, 3)
assert_equal 2, result2.fetch(:b, 4)

# ---

# rubocop:disable Style/RedundantFetchBlock
assert_equal(5, result1.fetch(:a) { 5 })
assert_equal(6, result1.fetch(:b) { 6 })

assert_equal(1, result2.fetch(:a) { 7 })
assert_equal(2, result2.fetch(:b) { 8 })
# rubocop:enable Style/RedundantFetchBlock
end

test '#slice' do
result1 = Context::Success(:ok)
result2 = Context::Success(:ok, a: 1, b: 2)

assert_equal({}, result1.slice(:a, :b))
assert_equal({ a: 1, b: 2 }, result2.slice(:a, :b))
end

test '#values_at' do
result1 = Context::Success(:ok)
result2 = Context::Success(:ok, a: 1, b: 2)

assert_equal [nil, nil], result1.values_at(:a, :b)
assert_equal [1, 2], result2.values_at(:a, :b)
end

test '#fetch_values' do
result1 = Context::Success(:ok)
result2 = Context::Success(:ok, a: 1, b: 2)

assert_raises(KeyError) do
result1.fetch_values(:a, :b)
end

assert_equal [1, 2], result2.fetch_values(:a, :b)

# ---

values1 = result1.fetch_values(:a, :b, :c, :d) { |key| key.to_s.upcase }
values2 = result2.fetch_values(:a, :b, :c, :d) { |key| key.to_s.upcase }

assert_equal %w[A B C D], values1

assert_equal [1, 2, 'C', 'D'], values2
end
end
end

0 comments on commit 7bf8ba6

Please sign in to comment.