Skip to content

Commit

Permalink
Fixes around optionals and partial case key paths (#147)
Browse files Browse the repository at this point in the history
We recently added code that can flatten optionals in a couple of ways,
but because these methods weren't disfavored, they take precedent and
can produce case key paths incapable of expressing what you want, like
the ability to embed a optional in an case that contains an optional:

```swift
@CasePathable
enum Foo {
  case bar(Int?)
}

let kp = \Foo.Cases.bar  // CaseKeyPath<Foo, Int>, not <Foo, Int?>
kp(nil)  // 'nil' is not compatible with expected argument type 'Int'
```

This PR disfavors these overloads to allow you to flatten optionals
contextually, but by default will leave them alone.

It also fixes a bug in `PartialCaseKeyPath.callAsFunction` that would
prevent non-optionals from being promoted to optionals in cases that
expect them:

```swift
let kp: PartialCaseKeyPath<Foo> = \.bar
kp(42)  // nil
```

Finally, this PR fixes a bug in which
`PartialCaseKeyPath.callAsFunction` was technically available on
`CaseKeyPath`, which meant you could call it with any value.
  • Loading branch information
stephencelis authored Feb 1, 2024
1 parent 735d18b commit e072139
Show file tree
Hide file tree
Showing 3 changed files with 7 additions and 4 deletions.
5 changes: 3 additions & 2 deletions Sources/CasePaths/CasePathable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ extension CaseKeyPath {
/// A partially type-erased key path, from a concrete root enum to any resulting value type.
public typealias PartialCaseKeyPath<Root> = PartialKeyPath<Case<Root>>

extension PartialCaseKeyPath {
extension _AppendKeyPath {
/// Attempts to embeds any value in an enum at this case key path's case.
///
/// - Parameter value: A value to embed. If the value type does not match the case path's value
Expand All @@ -282,9 +282,10 @@ extension PartialCaseKeyPath {
public func callAsFunction<Enum: CasePathable>(
_ value: Any
) -> Enum?
where Root == Case<Enum> {
where Self == PartialCaseKeyPath<Enum> {
func open<AnyAssociatedValue>(_ value: AnyAssociatedValue) -> Enum? {
(Case<Enum>()[keyPath: self] as? Case<AnyAssociatedValue>)?.embed(value) as? Enum
?? (Case<Enum>()[keyPath: self] as? Case<AnyAssociatedValue?>)?.embed(value) as? Enum
}
return _openExistential(value, do: open)
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CasePaths/Optional+CasePathable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension Optional: CasePathable {
}

/// A case path to an optional-chained value.
@_disfavoredOverload
public subscript<Member>(
dynamicMember keyPath: KeyPath<Wrapped.AllCasePaths, AnyCasePath<Wrapped, Member>>
) -> AnyCasePath<Optional, Member?>
Expand All @@ -49,6 +50,7 @@ extension Case {
///
/// This subscript can chain into an optional's wrapped value without explicitly specifying each
/// `some` component.
@_disfavoredOverload
public subscript<Member>(
dynamicMember keyPath: KeyPath<Value.AllCasePaths, AnyCasePath<Value, Member?>>
) -> Case<Member>
Expand Down
4 changes: 2 additions & 2 deletions Tests/CasePathsTests/CasePathsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ final class CasePathsTests: XCTestCase {
XCTAssertEqual(buzzPath1, \.buzz)
XCTAssertEqual(buzzPath2, \.buzz)
let buzzPath3 = \Fizz.Cases.buzz
XCTAssertNotEqual(buzzPath1, buzzPath3)
XCTAssertEqual(buzzPath2, buzzPath3)
XCTAssertEqual(buzzPath1, buzzPath3)
XCTAssertNotEqual(buzzPath2, buzzPath3)
XCTAssertEqual(ifLet(state: \Fizz.buzz, action: \Fizz.Cases.buzz), 42)
XCTAssertEqual(ifLet(state: \Fizz.buzz, action: \Foo.Cases.bar), nil)
let fizzBuzzPath1: CaseKeyPath<Fizz, Int?> = \Fizz.Cases.buzz.fizzBuzz.int
Expand Down

0 comments on commit e072139

Please sign in to comment.