Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some Hash's methods to BCDD::Context #41

Merged
merged 3 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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