Case Paths + Key Paths = Optional Paths
This PR adds some basic support for case/key path composition via "optional" paths.
Often there are case path APIs that don't need the full power of a case path. You might need to extract an optional value, or update an existing optional value, but you don't need to embed that value into a whole new root. This is the behavior of optional-chaining in Swift, which is sadly unsupported when it comes to key paths: an optional-chained key path loses writability.
This PR changes that by expanding the functionality of case key paths so that they gracefully degrade to an "optional" key path.
For example:
@CasePathable
enum Destination {
case login(Login)
}
struct Login {
var username = ""
var password = ""
}
let loginKeyPath: CaseKeyPath<Destination, Login> = \.login
loginKeyPath as OptionalKeyPath // ✅
let loginPasswordKeyPath: OptionalKeyPath<Destination, String> = \.login.username // ✅
Some details and experimentation to figure out before merging, so opening as a draft for now.
A few other things come to mind:
- Is it weird to call the internal thing
Casestill now that it can refer to non-enum values? - While
\Enum.Cases.casemade sense for qualifying case key paths, it makes less sense for optional paths:\Struct.Cases.property - It's possible to create an "optional" path from any non-optional key path. This is required for composition to work (
\.selfpaths, as well as\.nonOptionalProperty.optionalProperty). It may feel weird to allow an API that takes optional paths to take a non-optional one, but it also seems the most flexible and we've even had an example in isowords where we wanted to genericallyifLetto an optional path in one case and a non-optional path in another.
@stephencelis I was using the old OptionalPath code in a project that was on TCA 1.10.4 and swift-case-paths 1.3.0. Trying to migrate it to TCA 1.13.0, and had some compilation errors due to deprecations with case paths.
Just stumbled across this PR, what great timing! Just curious if you know what the timing might be for getting this merged and released?
Hi @JimRoepcke! Theoretically this branch is ready to go, but we haven't really prioritized it. In most of our projects we've remodeled our domains in ways that no longer require optional paths, and so the main thing needed for this branch to land is some beta testers, which is where you could come in :smile:
Want to point your project at this branch and take things for a spin to see if it unblocks you? We're eager for feedback from users with real world requirements!
@stephencelis I pointed my code at this branch. Working through issues in my code, but I'm also seeing TCA (1.13.0) itself is not building?
When I cmd-click on id(state:action:) it takes me here:
Hi @stephencelis, will this also fix an issue I had when trying to model my domain by wrapping my enum reducer in another enum before putting it in State? The code can be simplified to this
@CasePathable
enum LoadingState: Equatable {
case loading
case loaded(ContentReducer.State)
}
@Reducer(state: .equatable)
enum ContentReducer {
case child1(Child1Reducer.State)
}
@Reducer
struct Child1Reducer {
typealias State = Never
typealias Action = Never
typealias Body = EmptyReducer
}
@Reducer
struct Child2Reducer {
typealias State = Never
typealias Action = Never
typealias Body = EmptyReducer
}
@Reducer
struct Feature {
@ObservableState
struct State: Equatable {
var content: LoadingState = .loading
}
enum Action {
case content(ContentReducer.Action)
}
public var body: some ReducerOf<Self> {
EmptyReducer()
.ifLet(\.content.loaded, action: \.content) {
ContentReducer.body
}
}
}
This gives me the following error:
@fruitcoder Technically it should, though TCA would need to update all its helpers to work with optional paths rather than key paths and case paths.