Repository: pointfreeco/swift-case-paths (swift)

Issue:
Regression between 0.5.0 and 0.6.x
From `0.1.3` through `0.5.0` the following test passes. From `0.6.0` it fails. 

Apologies for the somewhat verbose scenario for the test, I wanted it to be as close to what we are using in our project as possible. We are using this extraction in a TCA `.scope().ifLet(then:else:)` so it's causing the `then` block to always be executed when it should be executing the `else` block. 

Our workaround for now is to hand code computed properties to determine if we are in a specific enum case. 

It's possible this was not exactly the intended way of using CasePaths, but it is nevertheless, a regression.

```swift
import CasePaths
import XCTest

precedencegroup ForwardComposition {
    associativity: left
    higherThan: AssignmentPrecedence
}

infix operator >>>: ForwardComposition

/// Forward composition eg:
/// Instead of doing `anArray.map(String.init).map(Int.init)`
/// you can do `anArray.map(String.init >>> Int.init)`
public func >>> <A, B, C>(_ a2b: @escaping (A) -> B, _ b2c: @escaping (B) -> C) -> (A) -> C {
    { a in b2c(a2b(a)) }
}

/// this is a cut down version of a real state in our application
public struct AccountCreationState: Equatable {
    public struct Basic: Equatable {
        public enum Stage: Equatable {
            case basicAccount
            case basicAccountComplete(name: String)
            case checkingEmailVerifiedLoader
            case confirmingEmail(email: String)
        }

        public var stage: Stage
    }

    public struct Real: Equatable {
        enum Stage {
            case emailConfirmed
            case profileIntro
            case profileLearnMoreAboutBonus
            case usResidentCheck
            case nonUSResident
            case realAccount
        }

        var stage: Stage
    }

    public enum Stage: Equatable {
        case initial
        case basic(Basic)
        case real(Real)
    }

    public var stage: Stage = .initial
    public var basic: Basic? {
        get {
            (/AccountCreationState.Stage.basic).extract(from: stage)
        }
        set {
            guard let value = newValue else { return }
            stage = .basic(value)
        }
    }

    var real: Real? {
        get {
            (/AccountCreationState.Stage.real).extract(from: stage)
        }
        set {
            guard let value = newValue else { return }
            stage = .real(value)
        }
    }
}

final class BugCheck: XCTestCase {
    func testNestedExtraction() {
        let state = AccountCreationState(stage: .basic(.init(stage: .basicAccount)))

        // the objective is to return non-nil if the state is the basic stage, with the `.checkingEmailVerifiedLoader`
        // sub-state, and otherwise, to return nil
        let extractor: (AccountCreationState) -> Void? = \.basic?.stage >>> (/AccountCreationState.Basic.Stage.checkingEmailVerifiedLoader)

        // in Releases 0.1.3 through 0.5.0 this returns `nil`
        // in Releases 0.6.x this returns `Optional(())`
        let result: Void? = extractor(state)

        XCTAssertNil(result)
    }
}
```

Pull request:
Fixes #47.

Patch:
diff --git a/Sources/CasePaths/Operators.swift b/Sources/CasePaths/Operators.swift
index af48b3c1..b5e1ea3c 100644
--- a/Sources/CasePaths/Operators.swift
+++ b/Sources/CasePaths/Operators.swift
@@ -142,6 +142,19 @@ public prefix func / <Root>(
   (/root).extract(from:)
 }
 
+/// Returns a void case path for a case with no associated value.
+///
+/// - Note: This operator is only intended to be used with enum cases that have no associated
+///   values. Its behavior is otherwise undefined.
+/// - Parameter root: A case with no an associated value.
+/// - Returns: A void case path.
+@_disfavoredOverload
+public prefix func / <Root>(
+  root: Root
+) -> (Root?) -> Void? {
+  (/root).extract(from:)
+}
+
 precedencegroup CasePathCompositionPrecedence {
   associativity: right
 }


Test patch:
diff --git a/Tests/CasePathsTests/CasePathsTests.swift b/Tests/CasePathsTests/CasePathsTests.swift
index 3597a1bc..ab4e4c6d 100644
--- a/Tests/CasePathsTests/CasePathsTests.swift
+++ b/Tests/CasePathsTests/CasePathsTests.swift
@@ -824,6 +824,11 @@ final class CasePathsTests: XCTestCase {
       "hello, world")
 
     XCTAssertNil((/Result.failure).extract(from: result))
+
+    let success: (Result<String?, Error>) -> String? = /Result.success
+    XCTAssertEqual(success(result), "hello, world")
+    let failure: (Result<String?, Error>) -> Error? = /Result.failure
+    XCTAssertNil(failure(result))
   }
 
   func testExtractFromOptionalRoot() {
@@ -852,6 +857,18 @@ final class CasePathsTests: XCTestCase {
     XCTAssertNil((/Foo.foo).extract(from: opt))
     XCTAssertNil((/Foo.bar).extract(from: opt))
     XCTAssertNil((/Foo.baz).extract(from: opt))
+
+    let extractExpression: (Foo?) -> String? = /Foo.foo
+    XCTAssertNotNil(extractExpression(.some(.foo("blob1"))))
+    XCTAssertNil(extractExpression(.some(.bar("blob2"))))
+    XCTAssertNil(extractExpression(.some(.baz)))
+    XCTAssertNil(extractExpression(nil))
+
+    let voidExtractExpression: (Foo?) -> Void? = /Foo.baz
+    XCTAssertNil(voidExtractExpression(.some(.foo("blob1"))))
+    XCTAssertNil(voidExtractExpression(.some(.bar("blob2"))))
+    XCTAssertNotNil(voidExtractExpression(.some(.baz)))
+    XCTAssertNil(voidExtractExpression(nil))
   }
 
   func testExtractFromOptionalRootWithEmbeddedTagBits() {