CasePaths

repository·main·Indexed 21 days ago

https://github.com/pointfreeco/swift-case-paths

A library that extends Swift's key path functionality to enums, allowing developers to zoom in on, inspect, and modify associated values of specific enum cases. It provides the @CasePathable macro and CaseKeyPath to enable dot-chaining, generic access, and dynamic member lookup for enums, bringing the ergonomics of struct KeyPaths to enum cases.

Tokens
4.2K
Snippets
18
Records
31
Agent score
27%

What's inside swift-case-paths

  1. Compose case paths

    main

    Case paths support composition. You can dive deeper into nested enums using dot-chaining or by appending paths together using the .append(path:) method.

    // Dot-chaining
    \AppAction.Cases.user.home
    
    // Appending
    let highScoreToUser = \HighScore.user
    let userToName = \User.name
    let highScoreToUserName = highScoreToUser.append(path: userToName)
  2. Extract, embed, and modify associated values with case paths

    main

    Case paths allow you to interact with enum associated values similarly to how key paths interact with struct properties:

    • Extraction: Use the [case:] subscript to optionally extract an associated value. If the case does not match, it returns nil.
    • Embedding: Call a case path as a function to wrap an associated value into a new instance of the enum.
    • Modification: Use the modify(_:) method to mutate an associated value in place.
    • Testing: Use the is(_:) method to check if an enum instance matches a specific case path.
    // Extraction
    userAction[case: \.home] // Optional(HomeAction.onAppear)
    userAction[case: \.settings] // nil
    
    // Embedding
    let userActionToHome = \UserAction.Cases.home
    userActionToHome(.onAppear) // UserAction.home(.onAppear)
    
    // Testing
    userAction.is(\.home) // true
    
    // Modification
    var result = Result<String, Error>.success("Blob")
    result.modify(\.success) {
      $0 += ", Jr."
    }
    // result is now Result.success("Blob, Jr.")
  3. What are CasePaths?

    main
    CasePaths is a library that extends the Swift key path hierarchy to include enum cases. While Swift provides built-in KeyPath support for structs and classes to zoom in on, inspect, and modify properties, it lacks a native mechanism for enums. CasePaths fills this gap by allowing developers to write generic code that can abstractly zoom in on and modify the data contained within a specific enum case.
  4. Case paths vs. key paths

    main

    In Swift, KeyPath allows you to access properties of a struct or class:

    struct User {
      let id: Int
      var name: String
    }
    
    \User.id    // KeyPath<User, Int>
    \User.name  // WritableKeyPath<User, String>

    However, you cannot use standard key paths to refer to enum cases:

    enum UserAction {
      case home(HomeAction)
      case settings(SettingsState)
    }
    
    \UserAction.home  // 🛑 Error: key path cannot refer to static member 'home'

    CasePaths provides CaseKeyPath to solve this limitation, enabling dot-chaining and generic access to enum case data.

  5. Migrate to CaseKeyPaths using @CasePathable

    main

    In CasePaths 1.1 and later, the reflection-based /Enum.case syntax is replaced by a more performant and safer two-step process using the @CasePathable macro and Swift KeyPaths.

    1. Attach the macro: Add @CasePathable to your enum definition.
    2. Use KeyPath syntax: Derive case paths using the \Enum.Cases.case syntax, which returns a CaseKeyPath.
    @CasePathable
    enum UserAction {
      case home(HomeAction)
    }
    
    // Use the new CaseKeyPath syntax
    let path = \UserAction.Cases.home
  6. Use CaseKeyPaths for extracting, embedding, and modifying values

    main

    When working with CaseKeyPaths, you should use subscript and method syntax similar to standard Swift KeyPaths instead of the older CasePath methods.

    TaskOld CasePath APINew CaseKeyPath API
    ExtractcasePath.extract(from: root)root[case: casePath]
    EmbedcasePath.embed(value)casePath(value)
    ModifycasePath.modify(&root) { ... }root.modify(casePath) { ... }
    ReplaceN/Aroot[case: casePath] = value
    // Extracting
    let action = root[case: casePath]
    
    // Embedding
    let newRoot = casePath(value)
    
    // Replacing
    root[case: casePath] = value
    
    // Modifying
    root.modify(casePath) { $0.count += 1 }
  7. Enable case paths in an enum using @CasePathable

    main

    To use case paths, annotate your enum with the @CasePathable macro. This enables the generation of case paths through the enum's Cases namespace.

    Once annotated, you can access case paths using \EnumName.Cases.caseName or by using the shorthand \.caseName when the type can be inferred.

    @CasePathable
    enum UserAction {
      case home(HomeAction)
      case settings(SettingsAction)
    }
    
    // Usage:
    \UserAction.Cases.home      // CaseKeyPath<UserAction, HomeAction>
    \UserAction.Cases.settings  // CaseKeyPath<UserAction, SettingsAction>
    
    // Shorthand:
    \.home as CaseKeyPath<UserAction, HomeAction>
  8. Using CasePaths to extend library ergonomics

    main

    CasePaths is primarily a tool for library authors to help users work with enums as easily as they work with structs. A common pattern is using the @CasePathable macro and CaseKeyPath to enable dynamicMemberLookup on types like Binding.

    By implementing a dynamicMember subscript that accepts a CaseKeyPath, a library can allow users to derive sub-bindings for specific enum cases using dot-chaining syntax.

    import CasePaths
    
    // 1. Annotate the enum with @CasePathable
    @CasePathable
    enum Destination {
      case home(HomeState)
      case settings(SettingsState)
    }
    
    // 2. Use dot-chaining to derive bindings (assuming an extension on Binding exists)
    let destination: Binding<Destination> = // ...
    let homeBinding: Binding<HomeState>? = destination.home