pointfreeco / swift-case-paths

🧰 Case paths extends the key path hierarchy to enum cases.
https://www.pointfree.co/collections/enums-and-structs/case-paths
MIT License
921 stars 108 forks source link

Regression between 0.5.0 and 0.6.x #47

Closed mluisbrown closed 3 years ago

mluisbrown commented 3 years ago

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.

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)
    }
}
stephencelis commented 3 years ago

Thanks for opening this! We were missing one of the signatures needed to feed input to the right metadata extractor function, sorry! Can you check out #48 to confirm it fixes your problem?

mluisbrown commented 3 years ago

Thanks for the quick turnaround! I can confirm that #48 does fix the problem 👍