├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── question.md └── workflows │ ├── ci.yml │ ├── format.yml │ └── release.yml ├── .gitignore ├── .spi.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── Package@swift-6.0.swift ├── README.md ├── Sources ├── CasePaths │ ├── Documentation.docc │ │ ├── CasePathableMacro.md │ │ ├── CasePaths.md │ │ ├── Deprecations.md │ │ ├── XCTModify.md │ │ └── XCTModifyDeprecations.md │ ├── EnumReflection.swift │ ├── Internal │ │ ├── Deprecations.swift │ │ ├── Exports.swift │ │ └── LockIsolated.swift │ ├── Macros.swift │ └── XCTestSupport.swift ├── CasePathsCore │ ├── AnyCasePath.swift │ ├── CasePathIterable.swift │ ├── CasePathReflectable.swift │ ├── CasePathable.swift │ ├── Documentation.docc │ │ ├── CasePathsCore.md │ │ ├── Extensions │ │ │ ├── AnyCasePath.md │ │ │ ├── CaseKeyPath.md │ │ │ └── CasePathable.md │ │ ├── MigrationGuides.md │ │ └── MigrationGuides │ │ │ └── MigratingTo1.1.md │ ├── Internal │ │ ├── KeyPath+Sendable.swift │ │ ├── TypeName.swift │ │ └── UncheckedSendable.swift │ ├── Never+CasePathable.swift │ ├── Optional+CasePathable.swift │ └── Result+CasePathable.swift └── CasePathsMacros │ ├── CasePathableMacro.swift │ └── Plugin.swift └── Tests ├── CasePathsMacrosTests └── CasePathableMacroTests.swift └── CasePathsTests ├── CasePathableTests.swift ├── CasePathsTests.swift ├── CaseSetTests.swift ├── CompileTimeTests.swift ├── DeprecatedTests.swift ├── DeprecatedXCTModifyTests.swift ├── MacroTests.swift ├── ReflectionTests.swift ├── XCTModifyTests.swift └── XCTUnwrapTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Have you found a bug with Case Paths you'd like to share? 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Give a clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Zip up a project that reproduces the behavior and attach it by dragging it here. 15 | 16 | ```swift 17 | // And/or enter code that reproduces the behavior here. 18 | 19 | ``` 20 | 21 | **Expected behavior** 22 | Give a clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Environment** 28 | - swift-case-paths [e.g. 0.8.0] 29 | - Xcode [e.g. 13.4] 30 | - Swift [e.g. 5.6] 31 | - OS: [e.g. iOS 15] 32 | 33 | **Additional context** 34 | Add any more context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Have a question about the Case Paths? 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Case Paths uses GitHub issues for bugs. For more general discussion and help, please use [GitHub Discussions](https://github.com/pointfreeco/swift-composable-architecture/discussions) or [the Swift forum](https://forums.swift.org/c/related-projects/swift-composable-architecture) first. 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '*' 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ci-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | macos: 18 | name: macOS (Xcode ${{ matrix.xcode }}) 19 | runs-on: macos-14 20 | strategy: 21 | matrix: 22 | xcode: 23 | - '15.4' 24 | - '16.2' 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Select Xcode ${{ matrix.xcode }} 28 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 29 | - name: Print Swift version 30 | run: swift --version 31 | - name: Run tests 32 | run: make test-swift 33 | 34 | macos-library-evolution: 35 | name: macOS Library Evolution (Xcode ${{ matrix.xcode }}) 36 | runs-on: macos-latest 37 | strategy: 38 | matrix: 39 | xcode: 40 | - '15.4' 41 | - '16.2' 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Select Xcode ${{ matrix.xcode }} 45 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 46 | - name: Print Swift version 47 | run: swift --version 48 | - name: Build for Library Evolution 49 | run: make build-for-library-evolution 50 | 51 | linux: 52 | strategy: 53 | matrix: 54 | swift: 55 | - '5.10' 56 | name: Ubuntu (Swift ${{ matrix.swift }}) 57 | runs-on: ubuntu-latest 58 | container: swift:${{ matrix.swift }} 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Run tests 62 | run: swift test --parallel 63 | - name: Run tests (release) 64 | run: swift test -c release --parallel 65 | 66 | wasm: 67 | name: Wasm 68 | runs-on: ubuntu-latest 69 | env: 70 | OMIT_MACRO_TESTS: 1 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: bytecodealliance/actions/wasmtime/setup@v1 74 | - name: Install Swift and Swift SDK for WebAssembly 75 | run: | 76 | PREFIX=/opt/swift 77 | set -ex 78 | curl -f -o /tmp/swift.tar.gz "https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz" 79 | sudo mkdir -p $PREFIX; sudo tar -xzf /tmp/swift.tar.gz -C $PREFIX --strip-component 1 80 | $PREFIX/usr/bin/swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip --checksum 6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4 81 | echo "$PREFIX/usr/bin" >> $GITHUB_PATH 82 | 83 | - name: Build tests 84 | run: swift build --swift-sdk wasm32-unknown-wasi --build-tests -Xlinker -z -Xlinker stack-size=$((1024 * 1024)) 85 | - name: Run tests 86 | run: wasmtime --dir . .build/debug/swift-case-pathsPackageTests.wasm 87 | 88 | check-macro-compatibility: 89 | name: Check Macro Compatibility 90 | runs-on: macos-latest 91 | steps: 92 | - name: Checkout repository 93 | uses: actions/checkout@v4 94 | - name: Run Swift Macro Compatibility Check 95 | uses: Matejkob/swift-macro-compatibility-check@v1 96 | with: 97 | run-tests: false 98 | major-versions-only: true 99 | 100 | # windows: 101 | # name: Windows (Swift ${{ matrix.swift }}, ${{ matrix.config }}) 102 | # strategy: 103 | # matrix: 104 | # os: [windows-latest] 105 | # config: 106 | # - debug 107 | # #- release 108 | # swift: ['6.0'] 109 | # fail-fast: false 110 | # runs-on: ${{ matrix.os }} 111 | # steps: 112 | # - uses: compnerd/gha-setup-swift@main 113 | # with: 114 | # branch: swift-${{ matrix.swift }}-branch 115 | # tag: ${{ matrix.swift }}-DEVELOPMENT-SNAPSHOT-2024-06-03-a 116 | # - uses: actions/checkout@v4 117 | # - name: Build 118 | # run: swift build -c ${{ matrix.config }} 119 | 120 | android: 121 | strategy: 122 | matrix: 123 | swift: 124 | - "6.0.2" 125 | name: Android 126 | runs-on: ubuntu-latest 127 | env: 128 | OMIT_MACRO_TESTS: 1 129 | steps: 130 | - uses: actions/checkout@v4 131 | - uses: skiptools/swift-android-action@v2 132 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: format-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | swift_format: 14 | name: swift-format 15 | runs-on: macos-15 16 | permissions: 17 | contents: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Select Xcode 16.2 21 | run: sudo xcode-select -s /Applications/Xcode_16.2.app 22 | - name: Format 23 | run: make format 24 | - uses: stefanzweifel/git-auto-commit-action@v5 25 | with: 26 | commit_message: Run swift-format 27 | branch: 'main' 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | release-project-channel: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Dump Github context 11 | env: 12 | GITHUB_CONTEXT: ${{ toJSON(github) }} 13 | run: echo "$GITHUB_CONTEXT" 14 | - name: Slack Notification on SUCCESS 15 | if: success() 16 | uses: tokorom/action-slack-incoming-webhook@main 17 | env: 18 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_PROJECT_CHANNEL_WEBHOOK_URL }} 19 | with: 20 | text: swift-case-paths ${{ github.event.release.tag_name }} has been released. 21 | blocks: | 22 | [ 23 | { 24 | "type": "header", 25 | "text": { 26 | "type": "plain_text", 27 | "text": "swift-case-paths ${{ github.event.release.tag_name }}" 28 | } 29 | }, 30 | { 31 | "type": "section", 32 | "text": { 33 | "type": "mrkdwn", 34 | "text": ${{ toJSON(github.event.release.body) }} 35 | } 36 | }, 37 | { 38 | "type": "section", 39 | "text": { 40 | "type": "mrkdwn", 41 | "text": "${{ github.event.release.html_url }}" 42 | } 43 | } 44 | ] 45 | release-releases-channel: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Dump Github context 49 | env: 50 | GITHUB_CONTEXT: ${{ toJSON(github) }} 51 | run: echo "$GITHUB_CONTEXT" 52 | - name: Slack Notification on SUCCESS 53 | if: success() 54 | uses: tokorom/action-slack-incoming-webhook@main 55 | env: 56 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }} 57 | with: 58 | text: swift-case-paths ${{ github.event.release.tag_name }} has been released. 59 | blocks: | 60 | [ 61 | { 62 | "type": "header", 63 | "text": { 64 | "type": "plain_text", 65 | "text": "swift-case-paths ${{ github.event.release.tag_name }}" 66 | } 67 | }, 68 | { 69 | "type": "section", 70 | "text": { 71 | "type": "mrkdwn", 72 | "text": ${{ toJSON(github.event.release.body) }} 73 | } 74 | }, 75 | { 76 | "type": "section", 77 | "text": { 78 | "type": "mrkdwn", 79 | "text": "${{ github.event.release.html_url }}" 80 | } 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [CasePathsCore, CasePaths] 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at mbw234@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Point-Free, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SWIFT_DOCKER_IMAGE = swift:5.6 2 | 3 | test-all: test-linux test-swift 4 | 5 | test-linux: 6 | docker run \ 7 | --rm \ 8 | -v "$(PWD):$(PWD)" \ 9 | -w "$(PWD)" \ 10 | $(SWIFT_DOCKER_IMAGE) \ 11 | bash -c 'apt-get update && apt-get -y install make && make test-swift' 12 | 13 | test-swift: 14 | swift test \ 15 | --parallel 16 | swift test \ 17 | -c release \ 18 | --parallel 19 | 20 | build-for-library-evolution: 21 | swift build \ 22 | -c release \ 23 | --target CasePaths \ 24 | -Xswiftc -emit-module-interface \ 25 | -Xswiftc -enable-library-evolution \ 26 | -Xswiftc -DRESILIENT_LIBRARIES # Required to build swift-syntax; see https://github.com/swiftlang/swift-syntax/pull/2540 27 | 28 | format: 29 | swift format --in-place --recursive . 30 | 31 | .PHONY: format test-all test-linux test-swift 32 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "75a28c180eaa852896a1e043cbbf340d2f28af3ec620072b1a1b7d9a6041d14e", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-docc-plugin", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-docc-plugin", 8 | "state" : { 9 | "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", 10 | "version" : "1.4.3" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-docc-symbolkit", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 17 | "state" : { 18 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 19 | "version" : "1.0.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-macro-testing", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-macro-testing", 26 | "state" : { 27 | "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", 28 | "version" : "0.5.2" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-snapshot-testing", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 35 | "state" : { 36 | "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", 37 | "version" : "1.17.6" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-syntax", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/swiftlang/swift-syntax", 44 | "state" : { 45 | "revision" : "0687f71944021d616d34d922343dcef086855920", 46 | "version" : "600.0.1" 47 | } 48 | }, 49 | { 50 | "identity" : "xctest-dynamic-overlay", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 53 | "state" : { 54 | "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", 55 | "version" : "1.4.3" 56 | } 57 | } 58 | ], 59 | "version" : 3 60 | } 61 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import CompilerPluginSupport 4 | import Foundation 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "swift-case-paths", 9 | platforms: [ 10 | .iOS(.v13), 11 | .macOS(.v10_15), 12 | .tvOS(.v13), 13 | .watchOS(.v6), 14 | ], 15 | products: [ 16 | .library( 17 | name: "CasePaths", 18 | targets: ["CasePaths"] 19 | ), 20 | .library( 21 | name: "CasePathsCore", 22 | targets: ["CasePathsCore"] 23 | ), 24 | ], 25 | dependencies: [ 26 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"602.0.0"), 27 | .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), 28 | ], 29 | targets: [ 30 | .target( 31 | name: "CasePaths", 32 | dependencies: [ 33 | "CasePathsCore", 34 | "CasePathsMacros", 35 | ] 36 | ), 37 | .target( 38 | name: "CasePathsCore", 39 | dependencies: [ 40 | .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), 41 | .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), 42 | ] 43 | ), 44 | .macro( 45 | name: "CasePathsMacros", 46 | dependencies: [ 47 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 48 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 49 | ] 50 | ), 51 | .testTarget( 52 | name: "CasePathsTests", 53 | dependencies: ["CasePaths"] 54 | ), 55 | ] 56 | ) 57 | 58 | #if !os(Windows) 59 | // Add the documentation compiler plugin if possible 60 | package.dependencies.append( 61 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 62 | ) 63 | #endif 64 | 65 | if ProcessInfo.processInfo.environment["OMIT_MACRO_TESTS"] == nil { 66 | package.dependencies.append( 67 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0") 68 | ) 69 | package.targets.append( 70 | .testTarget( 71 | name: "CasePathsMacrosTests", 72 | dependencies: [ 73 | "CasePathsMacros", 74 | .product( 75 | name: "MacroTesting", 76 | package: "swift-macro-testing" 77 | ), 78 | ] 79 | ) 80 | ) 81 | } 82 | 83 | for target in package.targets { 84 | target.swiftSettings = target.swiftSettings ?? [] 85 | target.swiftSettings?.append(contentsOf: [ 86 | .enableExperimentalFeature("StrictConcurrency") 87 | ]) 88 | // target.swiftSettings?.append( 89 | // .unsafeFlags([ 90 | // "-enable-library-evolution", 91 | // ]) 92 | // ) 93 | } 94 | -------------------------------------------------------------------------------- /Package@swift-6.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import CompilerPluginSupport 4 | import Foundation 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "swift-case-paths", 9 | platforms: [ 10 | .iOS(.v13), 11 | .macOS(.v10_15), 12 | .tvOS(.v13), 13 | .watchOS(.v6), 14 | ], 15 | products: [ 16 | .library( 17 | name: "CasePaths", 18 | targets: ["CasePaths"] 19 | ), 20 | .library( 21 | name: "CasePathsCore", 22 | targets: ["CasePathsCore"] 23 | ), 24 | ], 25 | dependencies: [ 26 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"602.0.0"), 27 | .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), 28 | ], 29 | targets: [ 30 | .target( 31 | name: "CasePaths", 32 | dependencies: [ 33 | "CasePathsCore", 34 | "CasePathsMacros", 35 | ] 36 | ), 37 | .target( 38 | name: "CasePathsCore", 39 | dependencies: [ 40 | .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), 41 | .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), 42 | ] 43 | ), 44 | .macro( 45 | name: "CasePathsMacros", 46 | dependencies: [ 47 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 48 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 49 | ] 50 | ), 51 | .testTarget( 52 | name: "CasePathsTests", 53 | dependencies: ["CasePaths"] 54 | ), 55 | ], 56 | swiftLanguageModes: [.v6] 57 | ) 58 | 59 | #if !os(Windows) 60 | // Add the documentation compiler plugin if possible 61 | package.dependencies.append( 62 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 63 | ) 64 | #endif 65 | 66 | if ProcessInfo.processInfo.environment["OMIT_MACRO_TESTS"] == nil { 67 | package.dependencies.append( 68 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0") 69 | ) 70 | package.targets.append( 71 | .testTarget( 72 | name: "CasePathsMacrosTests", 73 | dependencies: [ 74 | "CasePathsMacros", 75 | .product( 76 | name: "MacroTesting", 77 | package: "swift-macro-testing" 78 | ), 79 | ] 80 | ) 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧰 CasePaths 2 | 3 | [![CI](https://github.com/pointfreeco/swift-case-paths/workflows/CI/badge.svg)](https://actions-badge.atrox.dev/pointfreeco/swift-case-paths/goto) 4 | [![Slack](https://img.shields.io/badge/slack-chat-informational.svg?label=Slack&logo=slack)](http://pointfree.co/slack-invite) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswift-case-paths%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/swift-case-paths) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswift-case-paths%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/swift-case-paths) 7 | 8 | Case paths extends the key path hierarchy to enum cases. 9 | 10 | ## Motivation 11 | 12 | Swift endows every struct and class property with a [key path][key-path-docs]. 13 | 14 | [key-path-docs]: https://developer.apple.com/documentation/swift/swift_standard_library/key-path_expressions 15 | 16 | ``` swift 17 | struct User { 18 | let id: Int 19 | var name: String 20 | } 21 | 22 | \User.id // KeyPath 23 | \User.name // WritableKeyPath 24 | ``` 25 | 26 | This is compiler-generated code that can be used to abstractly zoom in on part of a structure, 27 | inspect and even change it, all while propagating those changes to the structure's whole. They are 28 | the silent partner of many modern Swift APIs powered by 29 | [dynamic member lookup][dynamic-member-lookup-proposal], like SwiftUI 30 | [bindings][binding-dynamic-member-lookup-docs], but also make more direct appearances, like in the 31 | SwiftUI [environment][environment-property-wrapper-docs] and [unsafe mutable pointers][pointee]. 32 | 33 | [pointee]: https://developer.apple.com/documentation/swift/unsafemutablepointer/pointer(to:)-8veyb 34 | 35 | Unfortunately, no such structure exists for enum cases. 36 | 37 | ``` swift 38 | enum UserAction { 39 | case home(HomeAction) 40 | case settings(SettingsAction) 41 | } 42 | 43 | \UserAction.home // 🛑 44 | ``` 45 | 46 | > 🛑 key path cannot refer to static member 'home' 47 | 48 | And so it's not possible to write generic code that can zoom in and modify the data of a particular 49 | case in the enum. 50 | 51 | [key-path-docs]: https://developer.apple.com/documentation/swift/swift_standard_library/key-path_expressions 52 | [dynamic-member-lookup-proposal]: https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md 53 | [binding-dynamic-member-lookup-docs]: https://developer.apple.com/documentation/swiftui/bindable/subscript(dynamicmember:) 54 | [environment-property-wrapper-docs]: https://developer.apple.com/documentation/swiftui/scene/environment(_:_:) 55 | [combine-publisher-assign-docs]: https://developer.apple.com/documentation/combine/publisher/assign(to:on:) 56 | 57 | ## Using case paths in libraries 58 | 59 | By far the most common use of case paths is as a tool inside a library that is distributed to other 60 | developers. Case paths are used in the [Composable Architecture][tca-gh], 61 | [SwiftUI Navigation][sui-nav-gh], [Parsing][parsers-gh], and many other libraries. 62 | 63 | [tca-gh]: http://github.com/pointfreeco/swift-composable-architecture 64 | [sui-nav-gh]: http://github.com/pointfreeco/swiftui-navigation 65 | [parsers-gh]: http://github.com/pointfreeco/swift-parsing 66 | 67 | If you maintain a library where you expect your users to model their domains with enums, then 68 | providing case path tools to them can help them break their domains into smaller units. For 69 | example, consider the `Binding` type provided by SwiftUI: 70 | 71 | ```swift 72 | struct Binding { 73 | let get: () -> Value 74 | let set: (Value) -> Void 75 | } 76 | ``` 77 | 78 | Through the power of [dynamic member lookup][dynamic-member-lookup-proposal] we are able to support 79 | dot-chaining syntax for deriving new bindings to members of values: 80 | 81 | ```swift 82 | @dynamicMemberLookup 83 | struct Binding { 84 | … 85 | subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { 86 | Binding( 87 | get: { self.get()[keyPath: keyPath] }, 88 | set: { 89 | var value = self.get() 90 | value[keyPath: keyPath] = $0 91 | self.set(value) 92 | } 93 | ) 94 | } 95 | } 96 | ``` 97 | 98 | If you had a binding of a user, you could simply append `.name` to that binding to immediately 99 | derive a binding to the user's name: 100 | 101 | ```swift 102 | let user: Binding = // ... 103 | let name: Binding = user.name 104 | ``` 105 | 106 | However, there are no such affordances for enums: 107 | 108 | ```swift 109 | enum Destination { 110 | case home(HomeState) 111 | case settings(SettingsState) 112 | } 113 | let destination: Binding = // ... 114 | destination.home // 🛑 115 | destination.settings // 🛑 116 | ``` 117 | 118 | It is not possible to derive a binding to just the `home` case of a destination binding by using 119 | simple dot-chaining syntax. 120 | 121 | However, if SwiftUI used this CasePaths library, then they could provide this tool quite easily. 122 | They could provide an additional `dynamicMember` subscript that uses a `CaseKeyPath`, which is a 123 | key path that singles out a case of an enum, and use that to derive a binding to a particular 124 | case of an enum: 125 | 126 | ```swift 127 | import CasePaths 128 | 129 | extension Binding { 130 | public subscript(dynamicMember keyPath: CaseKeyPath) -> Binding? 131 | where Value: CasePathable { 132 | Binding( 133 | unwrapping: Binding( 134 | get: { self.wrappedValue[case: keyPath] }, 135 | set: { newValue, transaction in 136 | guard let newValue else { return } 137 | self.transaction(transaction).wrappedValue[case: keyPath] = newValue 138 | } 139 | ) 140 | ) 141 | } 142 | } 143 | ``` 144 | 145 | With that defined, one can annotate their enum with the `@CasePathable` macro and then immediately 146 | use dot-chaining to derive a binding of a case from a binding of an enum: 147 | 148 | ```swift 149 | @CasePathable 150 | enum Destination { 151 | case home(HomeState) 152 | case settings(SettingsState) 153 | } 154 | let destination: Binding = // ... 155 | destination.home // Binding? 156 | destination.settings // Binding? 157 | ``` 158 | 159 | This is an example of how libraries can provide tools for their users to embrace enums without 160 | losing out on the ergonomics of structs. 161 | 162 | ## Basics of case paths 163 | 164 | While library tooling is the biggest use case for using this library, there are some ways that you 165 | can use case paths in first-party code too. The library bridges the gap between structs and enums by 166 | introducing what we call "case paths": key paths for enum cases. 167 | 168 | Case paths can be enabled for an enum using the `@CasePathable` macro: 169 | 170 | ```swift 171 | @CasePathable 172 | enum UserAction { 173 | case home(HomeAction) 174 | case settings(SettingsAction) 175 | } 176 | ``` 177 | 178 | And they can be produced from a "case-pathable" enum through its `Cases` namespace: 179 | 180 | ```swift 181 | \UserAction.Cases.home // CaseKeyPath 182 | \UserAction.Cases.settings // CaseKeyPath 183 | ``` 184 | 185 | And like any key path, they can be abbreviated when the enum type can be inferred: 186 | 187 | ```swift 188 | \.home as CaseKeyPath 189 | \.settings as CaseKeyPath 190 | ``` 191 | 192 | ### Case paths vs. key paths 193 | 194 | #### Extracting, embedding, modifying, and testing values 195 | 196 | As key paths package up the functionality of getting and setting a value on a root structure, case 197 | paths package up the functionality of optionally extracting and modifying an associated value of a 198 | root enumeration. 199 | 200 | ``` swift 201 | user[keyPath: \User.name] = "Blob" 202 | user[keyPath: \.name] // "Blob" 203 | 204 | userAction[case: \UserAction.Cases.home] = .onAppear 205 | userAction[case: \.home] // Optional(HomeAction.onAppear) 206 | ``` 207 | 208 | If the case doesn't match, the extraction can fail and return `nil`: 209 | 210 | ```swift 211 | userAction[case: \.settings] // nil 212 | ``` 213 | 214 | Case paths have an additional ability, which is to embed an associated value into a brand new root: 215 | 216 | ```swift 217 | let userActionToHome = \UserAction.Cases.home 218 | userActionToHome(.onAppear) // UserAction.home(.onAppear) 219 | ``` 220 | 221 | Cases can be tested using the `is` method on case-pathable enums: 222 | 223 | ```swift 224 | userAction.is(\.home) // true 225 | userAction.is(\.settings) // false 226 | 227 | let actions: [UserAction] = […] 228 | let homeActionsCount = actions.count(where: { $0.is(\.home) }) 229 | ``` 230 | 231 | And their associated values can be mutated in place using the `modify` method: 232 | 233 | ```swift 234 | var result = Result.success("Blob") 235 | result.modify(\.success) { 236 | $0 += ", Jr." 237 | } 238 | result // Result.success("Blob, Jr.") 239 | ``` 240 | 241 | #### Composing paths 242 | 243 | Case paths, like key paths, compose. You can dive deeper into the enumeration of an enumeration's 244 | case using familiar dot-chaining: 245 | 246 | ``` swift 247 | \HighScore.user.name 248 | // WritableKeyPath 249 | 250 | \AppAction.Cases.user.home 251 | // CaseKeyPath 252 | ``` 253 | 254 | Or you can append them together: 255 | 256 | ```swift 257 | let highScoreToUser = \HighScore.user 258 | let userToName = \User.name 259 | let highScoreToUserName = highScoreToUser.append(path: userToName) 260 | // WritableKeyPath 261 | 262 | let appActionToUser = \AppAction.Cases.user 263 | let userActionToHome = \UserAction.Cases.home 264 | let appActionToHome = appActionToUser.append(path: userActionToHome) 265 | // CaseKeyPath 266 | ``` 267 | 268 | #### Identity paths 269 | 270 | Case paths, also like key paths, provide an 271 | [identity](https://github.com/apple/swift-evolution/blob/master/proposals/0227-identity-keypath.md) 272 | path, which is useful for interacting with APIs that use key paths and case paths but you want to 273 | work with entire structure. 274 | 275 | ``` swift 276 | \User.self // WritableKeyPath 277 | \UserAction.Cases.self // CaseKeyPath 278 | ``` 279 | 280 | #### Property access 281 | 282 | Since Swift 5.2, key path expressions can be passed directly to methods like `map`. Case-pathable 283 | enums that are annotated with dynamic member lookup enable property access and key path expressions 284 | for each case. 285 | 286 | ```swift 287 | @CasePathable 288 | @dynamicMemberLookup 289 | enum UserAction { 290 | case home(HomeAction) 291 | case settings(SettingsAction) 292 | } 293 | 294 | let userAction: UserAction = .home(.onAppear) 295 | userAction.home // Optional(HomeAction.onAppear) 296 | userAction.settings // nil 297 | 298 | let userActions: [UserAction] = [.home(.onAppear), .settings(.purchaseButtonTapped)] 299 | userActions.compactMap(\.home) // [HomeAction.onAppear] 300 | ``` 301 | 302 | #### Dynamic case lookup 303 | 304 | Because case key paths are bona fide key paths under the hood, they can be used in the same 305 | applications, like dynamic member lookup. For example, we can extend SwiftUI's binding type to enum 306 | cases by extending it with a subscript: 307 | 308 | ```swift 309 | extension Binding { 310 | subscript( 311 | dynamicMember keyPath: CaseKeyPath 312 | ) -> Binding? { 313 | guard let member = self.wrappedValue[case: keyPath] 314 | else { return nil } 315 | return Binding( 316 | get: { self.wrappedValue[case: keyPath] ?? member }, 317 | set: { self.wrappedValue[case: keyPath] = $0 } 318 | ) 319 | } 320 | } 321 | 322 | @CasePathable enum ItemStatus { 323 | case inStock(quantity: Int) 324 | case outOfStock(isOnBackOrder: Bool) 325 | } 326 | 327 | struct ItemStatusView: View { 328 | @Binding var status: ItemStatus 329 | 330 | var body: some View { 331 | switch self.status { 332 | case .inStock: 333 | self.$status.inStock.map { $quantity in 334 | Section { 335 | Stepper("Quantity: \(quantity)", value: $quantity) 336 | Button("Mark as sold out") { 337 | self.item.status = .outOfStock(isOnBackOrder: false) 338 | } 339 | } header: { 340 | Text("In stock") 341 | } 342 | } 343 | case .outOfStock: 344 | self.$status.outOfStock.map { $isOnBackOrder in 345 | Section { 346 | Toggle("Is on back order?", isOn: $isOnBackOrder) 347 | Button("Is back in stock!") { 348 | self.item.status = .inStock(quantity: 1) 349 | } 350 | } header: { 351 | Text("Out of stock") 352 | } 353 | } 354 | } 355 | } 356 | } 357 | ``` 358 | 359 | > **Note** 360 | > The above is a simplified version of the subscript that ships in our 361 | > [SwiftUINavigation](https://github.com/pointfreeco/swiftui-navigation) library. 362 | 363 | #### Computed paths 364 | 365 | Key paths are created for every property, even computed ones, so what is the equivalent for case 366 | paths? Well, "computed" case paths can be created by extending the case-pathable enum's 367 | `AllCasePaths` type with properties that implement the `embed` and `extract` functionality of a 368 | custom case: 369 | 370 | ```swift 371 | @CasePathable 372 | enum Authentication { 373 | case authenticated(accessToken: String) 374 | case unauthenticated 375 | } 376 | 377 | extension Authentication.AllCasePaths { 378 | var encrypted: AnyCasePath { 379 | AnyCasePath( 380 | embed: { decryptedToken in 381 | .authenticated(token: encrypt(decryptedToken)) 382 | }, 383 | extract: { authentication in 384 | guard 385 | case let .authenticated(encryptedToken) = authentication, 386 | let decryptedToken = decrypt(token) 387 | else { return nil } 388 | return decryptedToken 389 | } 390 | ) 391 | } 392 | } 393 | 394 | \Authentication.Cases.encrypted 395 | // CaseKeyPath 396 | ``` 397 | 398 | ## Case studies 399 | 400 | * [**SwiftUINavigation**](https://github.com/pointfreeco/swiftui-navigation) uses case paths to 401 | power SwiftUI bindings, including navigation, with enums. 402 | 403 | * [**The Composable Architecture**](https://github.com/pointfreeco/swift-composable-architecture) 404 | allows you to break large features down into smaller ones that can be glued together user key 405 | paths and case paths. 406 | 407 | * [**Parsing**](https://github.com/pointfreeco/swift-parsing) uses case paths to turn unstructured 408 | data into enums and back again. 409 | 410 | Do you have a project that uses case paths that you'd like to share? Please 411 | [open a PR](https://github.com/pointfreeco/swift-case-paths/edit/main/README.md) with a link to it! 412 | 413 | ## Community 414 | 415 | If you want to discuss this library or have a question about how to use it to solve a particular 416 | problem, there are a number of places you can discuss with fellow 417 | [Point-Free](http://www.pointfree.co) enthusiasts: 418 | 419 | * For long-form discussions, we recommend the 420 | [discussions](http://github.com/pointfreeco/swift-case-paths/discussions) tab of this repo. 421 | * For casual chat, we recommend the 422 | [Point-Free Community Slack](http://pointfree.co/slack-invite). 423 | 424 | ## Documentation 425 | 426 | The latest documentation for CasePaths' APIs is available 427 | [here](https://swiftpackageindex.com/pointfreeco/swift-case-paths/main/documentation/casepaths). 428 | 429 | ## Credit and thanks 430 | 431 | Special thanks to [Giuseppe Lanza](https://github.com/gringoireDM), whose 432 | [EnumKit](https://github.com/gringoireDM/EnumKit) inspired the original, reflection-based solution 433 | this library used to power case paths. 434 | 435 | ## Interested in learning more? 436 | 437 | These concepts (and more) are explored thoroughly in [Point-Free](https://www.pointfree.co), a video 438 | series exploring functional programming and Swift hosted by 439 | [Brandon Williams](https://github.com/mbrandonw) and 440 | [Stephen Celis](https://github.com/stephencelis). 441 | 442 | The design of this library was explored in the following [Point-Free](https://www.pointfree.co) 443 | episodes: 444 | 445 | * [Episode 87](https://www.pointfree.co/episodes/ep87-the-case-for-case-paths-introduction): The 446 | Case for Case Paths: Introduction 447 | * [Episode 88](https://www.pointfree.co/episodes/ep88-the-case-for-case-paths-properties): The 448 | Case for Case Paths: Properties 449 | * [Episode 89](https://www.pointfree.co/episodes/ep89-case-paths-for-free): Case Paths for Free 450 | 451 | 452 | video poster image 453 | 454 | 455 | ## License 456 | 457 | All modules are released under the MIT license. See [LICENSE](LICENSE) for details. 458 | -------------------------------------------------------------------------------- /Sources/CasePaths/Documentation.docc/CasePathableMacro.md: -------------------------------------------------------------------------------- 1 | # ``CasePaths/CasePathable()`` 2 | 3 | ## Further reading 4 | 5 | See the [`CasePathsCore`](../casepathscore) module for the `CasePathable` protocol and other core 6 | library types. 7 | -------------------------------------------------------------------------------- /Sources/CasePaths/Documentation.docc/CasePaths.md: -------------------------------------------------------------------------------- 1 | # ``CasePaths`` 2 | 3 | Case paths bring the power and ergonomics of key paths to enums. 4 | 5 | ## Overview 6 | 7 | This module exports the core functionality of the Case Paths library, as well as the `@CasePathable` 8 | macro. 9 | 10 | To read the core documentation, see the [`CasePathsCore`](../casepathscore) module. 11 | 12 | ## Topics 13 | 14 | ### Creating case paths 15 | 16 | - ``CasePathable()`` 17 | 18 | ### Deprecated interfaces 19 | 20 | - 21 | -------------------------------------------------------------------------------- /Sources/CasePaths/Documentation.docc/Deprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecated interfaces 2 | 3 | Review unsupported CasePaths APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. See the replacement that you should use instead. 8 | 9 | ## Topics 10 | 11 | ### Functions 12 | 13 | - ``extract(case:from:)-262ip`` 14 | - ``extract(case:from:)-ylkc`` 15 | - ``extract(_:)-34fjh`` 16 | - ``extract(_:)-3twft`` 17 | 18 | ### Creating paths 19 | 20 | - ``CasePathsCore/AnyCasePath`` 21 | 22 | ### Type aliases 23 | 24 | - ``CasePath`` 25 | 26 | ### Test helpers 27 | 28 | - ``XCTModify(_:case:_:_:file:line:)-14526`` 29 | - ``XCTModify(_:case:_:_:file:line:)-1pef3`` 30 | - ``XCTModify(_:_:_:file:line:)`` 31 | -------------------------------------------------------------------------------- /Sources/CasePaths/Documentation.docc/XCTModify.md: -------------------------------------------------------------------------------- 1 | # ``CasePaths/XCTModify(_:case:_:_:file:line:)-14526`` 2 | 3 | ## Topics 4 | 5 | ### Testing optionals 6 | 7 | - ``XCTModify(_:_:_:file:line:)`` 8 | 9 | ### Deprecated test helpers 10 | 11 | - 12 | -------------------------------------------------------------------------------- /Sources/CasePaths/Documentation.docc/XCTModifyDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecated test helpers 2 | 3 | Review unsupported CasePaths APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. See the replacement that you should use instead. 8 | 9 | ## Topics 10 | 11 | ### XCTModify 12 | 13 | - ``XCTModify(_:case:_:_:file:line:)-1pef3`` 14 | 15 | ### XCTUnwrap 16 | 17 | - ``XCTUnwrap(_:case:_:file:line:)`` 18 | -------------------------------------------------------------------------------- /Sources/CasePaths/EnumReflection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension AnyCasePath { 4 | /// Returns a case path for the given embed function. 5 | /// 6 | /// This initializer generates a case path with an extract function that dynamically resolves 7 | /// given an enum embed function. 8 | /// 9 | /// > Important: This operation is provided for backwards compatibility. Avoid introducing it to 10 | /// > your code and instead favor using types that employ the ``CasePathable()`` macro. 11 | /// 12 | /// - Parameter embed: An embed function. 13 | @available(iOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 14 | @available(macOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 15 | @available(tvOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 16 | @available(watchOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 17 | public init(unsafe embed: @escaping @Sendable (Value) -> Root) { 18 | func open(_: Wrapped.Type) -> @Sendable (Root) -> Value? { 19 | optionalPromotedExtractHelp(unsafeBitCast(embed, to: (@Sendable (Value) -> Wrapped?).self)) 20 | as! @Sendable (Root) -> Value? 21 | } 22 | @UncheckedSendable var embed = embed 23 | let extract = 24 | ((_Witness.self as? _AnyOptional.Type)?.wrappedType) 25 | .map { _openExistential($0, do: open) } 26 | ?? extractHelp { [$embed] in $embed.wrappedValue($0) } 27 | self.init( 28 | embed: { [$embed] in $embed.wrappedValue($0) }, 29 | extract: extract 30 | ) 31 | } 32 | 33 | /// Returns a void case path for a case with no associated value. 34 | /// 35 | /// > Important: This operation is provided for backwards compatibility. Avoid introducing it to 36 | /// > your code and instead favor using types that employ the ``CasePathable()`` macro. 37 | @available(iOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 38 | @available(macOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 39 | @available(tvOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 40 | @available(watchOS, deprecated: 9999, message: "Use a 'CasePathable' case key path, instead") 41 | @_disfavoredOverload 42 | public init(unsafe root: @autoclosure @escaping @Sendable () -> Root) where Value == Void { 43 | func open(_: Wrapped.Type) -> @Sendable (Root) -> Void? { 44 | optionalPromotedExtractVoidHelp( 45 | unsafeBitCast(root, to: Wrapped?.self) 46 | ) as! @Sendable (Root) -> Void? 47 | } 48 | let extract = 49 | ((_Witness.self as? _AnyOptional.Type)?.wrappedType) 50 | .map { _openExistential($0, do: open) } 51 | ?? extractVoidHelp(root()) 52 | self.init(embed: root, extract: extract) 53 | } 54 | } 55 | 56 | // MARK: - Extraction helpers 57 | 58 | private struct Cache: Sendable { 59 | var tag: UInt32? 60 | var strategy: (isIndirect: Bool, associatedValueType: Any.Type)? 61 | } 62 | 63 | func extractHelp( 64 | _ embed: @escaping @Sendable (Value) -> Root 65 | ) -> @Sendable (Root) -> Value? { 66 | guard 67 | let metadata = EnumMetadata(Root.self), 68 | metadata.typeDescriptor.fieldDescriptor != nil 69 | else { 70 | assertionFailure("embed parameter must be a valid enum case initializer") 71 | return { _ in nil } 72 | } 73 | 74 | let cache = LockIsolated(Cache()) 75 | 76 | return { root in 77 | let rootTag = metadata.tag(of: root) 78 | 79 | if case let (cachedTag?, (isIndirect: isIndirect, associatedValueType: associatedValueType)?) = 80 | cache.withLock({ 81 | ($0.tag, $0.strategy) 82 | }) 83 | { 84 | guard rootTag == cachedTag else { return nil } 85 | let value = 86 | EnumMetadata 87 | ._project(root, isIndirect: isIndirect, associatedValueType: associatedValueType)? 88 | .value as? Value 89 | return value 90 | } 91 | 92 | guard 93 | let (value, isIndirect, type) = EnumMetadata._project(root), 94 | let value = value as? Value 95 | else { return nil } 96 | 97 | let embedTag = metadata.tag(of: embed(value)) 98 | cache.withLock { 99 | $0.tag = embedTag 100 | if embedTag == rootTag { 101 | $0.strategy = (isIndirect, type) 102 | } 103 | } 104 | return embedTag == rootTag ? value : nil 105 | } 106 | } 107 | 108 | func optionalPromotedExtractHelp( 109 | _ embed: @escaping @Sendable (Value) -> Root? 110 | ) -> @Sendable (Root?) -> Value? { 111 | guard Root.self != Value.self else { return { $0 as! Value? } } 112 | guard 113 | let metadata = EnumMetadata(Root.self), 114 | metadata.typeDescriptor.fieldDescriptor != nil 115 | else { 116 | assertionFailure("embed parameter must be a valid enum case initializer") 117 | return { _ in nil } 118 | } 119 | 120 | let cachedTag = LockIsolated(nil) 121 | 122 | return { optionalRoot in 123 | guard let root = optionalRoot else { return nil } 124 | 125 | let rootTag = metadata.tag(of: root) 126 | 127 | if let cachedTag = cachedTag.withLock({ $0 }) { 128 | guard rootTag == cachedTag else { return nil } 129 | } 130 | 131 | guard let value = EnumMetadata.project(root) as? Value 132 | else { return nil } 133 | 134 | guard let embedded = embed(value) else { return nil } 135 | let embedTag = metadata.tag(of: embedded) 136 | cachedTag.withLock { $0 = embedTag } 137 | return embedTag == rootTag ? value : nil 138 | } 139 | } 140 | 141 | func extractVoidHelp(_ root: Root) -> @Sendable (Root) -> Void? { 142 | guard 143 | let metadata = EnumMetadata(Root.self), 144 | metadata.typeDescriptor.fieldDescriptor != nil 145 | else { 146 | assertionFailure("value must be a valid enum case") 147 | return { _ in nil } 148 | } 149 | 150 | let cachedTag = metadata.tag(of: root) 151 | return { root in metadata.tag(of: root) == cachedTag ? () : nil } 152 | } 153 | 154 | func optionalPromotedExtractVoidHelp(_ root: Root?) -> @Sendable (Root?) -> Void? { 155 | guard 156 | let root = root, 157 | let metadata = EnumMetadata(Root.self), 158 | metadata.typeDescriptor.fieldDescriptor != nil 159 | else { 160 | assertionFailure("value must be a valid enum case") 161 | return { _ in nil } 162 | } 163 | 164 | let cachedTag = metadata.tag(of: root) 165 | return { root in root.flatMap(metadata.tag(of:)) == cachedTag ? () : nil } 166 | } 167 | 168 | // MARK: - Runtime reflection 169 | 170 | private protocol Metadata { 171 | var ptr: UnsafeRawPointer { get } 172 | } 173 | 174 | extension Metadata { 175 | var valueWitnessTable: ValueWitnessTable { 176 | ValueWitnessTable( 177 | ptr: self.ptr.load(fromByteOffset: -pointerSize, as: UnsafeRawPointer.self) 178 | ) 179 | } 180 | 181 | var kind: MetadataKind { self.ptr.load(as: MetadataKind.self) } 182 | } 183 | 184 | private struct MetadataKind: Equatable { 185 | var rawValue: UInt 186 | 187 | // https://github.com/apple/swift/blob/main/include/swift/ABI/MetadataValues.h 188 | // https://github.com/apple/swift/blob/main/include/swift/ABI/MetadataKind.def 189 | static var enumeration: Self { .init(rawValue: 0x201) } 190 | static var optional: Self { .init(rawValue: 0x202) } 191 | static var tuple: Self { .init(rawValue: 0x301) } 192 | static var existential: Self { .init(rawValue: 0x303) } 193 | } 194 | 195 | @_spi(Reflection) public struct EnumMetadata: Metadata, @unchecked Sendable { 196 | let ptr: UnsafeRawPointer 197 | 198 | fileprivate init(assumingEnum type: Any.Type) { 199 | self.ptr = unsafeBitCast(type, to: UnsafeRawPointer.self) 200 | } 201 | 202 | @_spi(Reflection) public init?(_ type: Any.Type) { 203 | self.init(assumingEnum: type) 204 | guard self.kind == .enumeration || self.kind == .optional else { return nil } 205 | } 206 | 207 | fileprivate var genericArguments: GenericArgumentVector? { 208 | guard typeDescriptor.flags.contains(.isGeneric) else { return nil } 209 | return .init(ptr: self.ptr.advanced(by: 2 * pointerSize)) 210 | } 211 | 212 | @_spi(Reflection) public var typeDescriptor: EnumTypeDescriptor { 213 | EnumTypeDescriptor( 214 | ptr: self.ptr.load(fromByteOffset: pointerSize, as: UnsafeRawPointer.self) 215 | ) 216 | } 217 | 218 | @_spi(Reflection) public func tag(of value: Enum) -> UInt32 { 219 | withUnsafePointer(to: value) { 220 | self.valueWitnessTable.getEnumTag($0, self.ptr) 221 | } 222 | } 223 | } 224 | 225 | extension EnumMetadata { 226 | @_spi(Reflection) public func associatedValueType(forTag tag: UInt32) -> Any.Type { 227 | guard 228 | let typeName = self.typeDescriptor.fieldDescriptor?.field(atIndex: tag).typeName, 229 | let type = swift_getTypeByMangledNameInContext( 230 | typeName.ptr, typeName.length, 231 | genericContext: self.typeDescriptor.ptr, 232 | genericArguments: self.genericArguments?.ptr 233 | ) 234 | else { 235 | return Void.self 236 | } 237 | 238 | return type 239 | } 240 | 241 | @_spi(Reflection) public func caseName(forTag tag: UInt32) -> String? { 242 | self.typeDescriptor.fieldDescriptor?.field(atIndex: tag).name 243 | } 244 | } 245 | 246 | @_silgen_name("swift_getTypeByMangledNameInContext") 247 | private func swift_getTypeByMangledNameInContext( 248 | _ name: UnsafePointer, 249 | _ nameLength: UInt, 250 | genericContext: UnsafeRawPointer?, 251 | genericArguments: UnsafeRawPointer? 252 | ) 253 | -> Any.Type? 254 | 255 | extension EnumMetadata { 256 | func destructivelyProjectPayload(of value: UnsafeMutableRawPointer) { 257 | self.valueWitnessTable.destructiveProjectEnumData(value, ptr) 258 | } 259 | 260 | func destructivelyInjectTag(_ tag: UInt32, intoPayload payload: UnsafeMutableRawPointer) { 261 | self.valueWitnessTable.destructiveInjectEnumData(payload, tag, ptr) 262 | } 263 | 264 | @_spi(Reflection) public static func project(_ root: Enum) -> Any? { 265 | Self._project(root)?.value 266 | } 267 | 268 | fileprivate static func _project( 269 | _ root: Enum, 270 | isIndirect: Bool? = nil, 271 | associatedValueType: Any.Type? = nil 272 | ) -> (value: Any, isIndirect: Bool, associatedValueType: Any.Type)? { 273 | guard let metadata = Self(Enum.self) 274 | else { return nil } 275 | 276 | let tag = metadata.tag(of: root) 277 | guard 278 | let isIndirect = isIndirect 279 | ?? metadata 280 | .typeDescriptor 281 | .fieldDescriptor? 282 | .field(atIndex: tag) 283 | .flags 284 | .contains(.isIndirectCase) 285 | else { return nil } 286 | 287 | var root = root 288 | return withUnsafeMutableBytes(of: &root) { rawBuffer in 289 | guard let pointer = rawBuffer.baseAddress 290 | else { return nil } 291 | metadata.destructivelyProjectPayload(of: pointer) 292 | defer { metadata.destructivelyInjectTag(tag, intoPayload: pointer) } 293 | func open(_ type: T.Type) -> T { 294 | isIndirect 295 | ? pointer 296 | .load(as: UnsafeRawPointer.self) // Load the heap object pointer. 297 | .advanced(by: 2 * pointerSize) // Skip the heap object header. 298 | .load(as: type) 299 | : pointer.load(as: type) 300 | } 301 | let type: Any.Type 302 | if let associatedValueType = associatedValueType { 303 | type = associatedValueType 304 | } else { 305 | var associatedValueType = metadata.associatedValueType(forTag: tag) 306 | if let tupleMetadata = TupleMetadata(associatedValueType), tupleMetadata.elementCount == 1 { 307 | associatedValueType = tupleMetadata.element(at: 0).type 308 | } 309 | type = associatedValueType 310 | } 311 | let value: Any = _openExistential(type, do: open) 312 | return (value: value, isIndirect: isIndirect, associatedValueType: type) 313 | } 314 | } 315 | } 316 | 317 | @_spi(Reflection) public struct EnumTypeDescriptor: Equatable { 318 | let ptr: UnsafeRawPointer 319 | 320 | var flags: Flags { Flags(rawValue: self.ptr.load(as: UInt32.self)) } 321 | 322 | fileprivate var fieldDescriptor: FieldDescriptor? { 323 | self.ptr 324 | .advanced(by: 4 * 4) 325 | .loadRelativePointer() 326 | .map(FieldDescriptor.init) 327 | } 328 | 329 | var payloadCaseCount: UInt32 { self.ptr.load(fromByteOffset: 5 * 4, as: UInt32.self) & 0xFFFFFF } 330 | 331 | var emptyCaseCount: UInt32 { self.ptr.load(fromByteOffset: 6 * 4, as: UInt32.self) } 332 | } 333 | 334 | extension EnumTypeDescriptor { 335 | struct Flags: OptionSet { 336 | let rawValue: UInt32 337 | 338 | static var isGeneric: Self { .init(rawValue: 0x80) } 339 | } 340 | } 341 | 342 | private struct TupleMetadata: Metadata { 343 | let ptr: UnsafeRawPointer 344 | 345 | init?(_ type: Any.Type) { 346 | self.ptr = unsafeBitCast(type, to: UnsafeRawPointer.self) 347 | guard self.kind == .tuple else { return nil } 348 | } 349 | 350 | var elementCount: UInt { 351 | self.ptr 352 | .advanced(by: pointerSize) // kind 353 | .load(as: UInt.self) 354 | } 355 | 356 | var labels: UnsafePointer? { 357 | self.ptr 358 | .advanced(by: pointerSize) // kind 359 | .advanced(by: pointerSize) // elementCount 360 | .load(as: UnsafePointer?.self) 361 | } 362 | 363 | func element(at i: Int) -> Element { 364 | Element( 365 | ptr: 366 | self.ptr 367 | .advanced(by: pointerSize) // kind 368 | .advanced(by: pointerSize) // elementCount 369 | .advanced(by: pointerSize) // labels pointer 370 | .advanced(by: i * 2 * pointerSize) 371 | ) 372 | } 373 | } 374 | 375 | extension TupleMetadata { 376 | struct Element: Equatable { 377 | let ptr: UnsafeRawPointer 378 | 379 | var type: Any.Type { self.ptr.load(as: Any.Type.self) } 380 | 381 | var offset: UInt32 { self.ptr.load(fromByteOffset: pointerSize, as: UInt32.self) } 382 | 383 | static func == (lhs: Element, rhs: Element) -> Bool { 384 | lhs.type == rhs.type && lhs.offset == rhs.offset 385 | } 386 | } 387 | } 388 | 389 | extension TupleMetadata { 390 | func hasSameLayout(as other: TupleMetadata) -> Bool { 391 | self.elementCount == other.elementCount 392 | && (0.. FieldRecord { 416 | FieldRecord( 417 | ptr: self.ptr.advanced(by: 2 * 4 + 2 * 2 + 4).advanced(by: Int(i) * recordSize) 418 | ) 419 | } 420 | } 421 | 422 | private struct FieldRecord { 423 | let ptr: UnsafeRawPointer 424 | 425 | var flags: Flags { Flags(rawValue: self.ptr.load(as: UInt32.self)) } 426 | 427 | var typeName: MangledTypeName? { 428 | self.ptr 429 | .advanced(by: 4) 430 | .loadRelativePointer() 431 | .map { MangledTypeName(ptr: $0.assumingMemoryBound(to: UInt8.self)) } 432 | } 433 | 434 | var name: String? { 435 | self.ptr 436 | .advanced(by: 4) 437 | .advanced(by: 4) 438 | .loadRelativePointer() 439 | .map { String(cString: $0.assumingMemoryBound(to: CChar.self)) } 440 | } 441 | } 442 | 443 | extension FieldRecord { 444 | struct Flags: OptionSet { 445 | var rawValue: UInt32 446 | 447 | static var isIndirectCase: Self { .init(rawValue: 1) } 448 | } 449 | } 450 | 451 | private struct MangledTypeName { 452 | let ptr: UnsafePointer 453 | 454 | var length: UInt { 455 | // https://github.com/apple/swift/blob/main/docs/ABI/Mangling.rst 456 | var ptr = self.ptr 457 | while true { 458 | switch ptr.pointee { 459 | case 0: 460 | return UInt(bitPattern: ptr - self.ptr) 461 | case 0x01...0x17: 462 | // Relative symbolic reference 463 | ptr = ptr.advanced(by: 5) 464 | case 0x18...0x1f: 465 | // Absolute symbolic reference 466 | ptr = ptr.advanced(by: 1 + pointerSize) 467 | default: 468 | ptr = ptr.advanced(by: 1) 469 | } 470 | } 471 | } 472 | } 473 | 474 | private struct ValueWitnessTable { 475 | let ptr: UnsafeRawPointer 476 | 477 | var getEnumTag: @convention(c) (_ value: UnsafeRawPointer, _ metadata: UnsafeRawPointer) -> UInt32 478 | { 479 | self.ptr.advanced(by: 10 * pointerSize + 2 * 4).loadInferredType() 480 | } 481 | 482 | // This witness transforms an enum value into its associated value, in place. 483 | var destructiveProjectEnumData: 484 | @convention(c) (_ value: UnsafeMutableRawPointer, _ metadata: UnsafeRawPointer) -> Void 485 | { 486 | self.ptr.advanced(by: 11 * pointerSize + 2 * 4).loadInferredType() 487 | } 488 | 489 | // This witness transforms an associated value into its enum value, in place. 490 | var destructiveInjectEnumData: 491 | @convention(c) (_ value: UnsafeMutableRawPointer, _ tag: UInt32, _ metadata: UnsafeRawPointer) 492 | -> Void 493 | { 494 | self.ptr.advanced(by: 12 * pointerSize + 2 * 4).loadInferredType() 495 | } 496 | } 497 | 498 | private struct GenericArgumentVector { 499 | let ptr: UnsafeRawPointer 500 | } 501 | 502 | extension GenericArgumentVector { 503 | func type(atIndex i: Int) -> Any.Type { 504 | return ptr.load(fromByteOffset: i * pointerSize, as: Any.Type.self) 505 | } 506 | } 507 | 508 | extension UnsafeRawPointer { 509 | fileprivate func loadInferredType() -> Type { 510 | self.load(as: Type.self) 511 | } 512 | 513 | fileprivate func loadRelativePointer() -> UnsafeRawPointer? { 514 | let offset = Int(load(as: Int32.self)) 515 | return offset == 0 ? nil : self + offset 516 | } 517 | } 518 | 519 | // This is the size of any Unsafe*Pointer and also the size of Int and UInt. 520 | private let pointerSize = MemoryLayout.size 521 | 522 | private protocol _Optional { 523 | associatedtype Wrapped 524 | } 525 | extension Optional: _Optional {} 526 | private enum _Witness {} 527 | private protocol _AnyOptional { 528 | static var wrappedType: Any.Type { get } 529 | } 530 | extension _Witness: _AnyOptional where A: _Optional { 531 | static var wrappedType: Any.Type { 532 | A.Wrapped.self 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /Sources/CasePaths/Internal/Deprecations.swift: -------------------------------------------------------------------------------- 1 | @_spi(CurrentTestCase) import XCTestDynamicOverlay 2 | 3 | #if canImport(ObjectiveC) 4 | import ObjectiveC 5 | #endif 6 | 7 | extension AnyCasePath { 8 | @available( 9 | iOS, deprecated: 9999, 10 | message: "Use 'CasePathable.modify', or 'extract' and 'embed', instead." 11 | ) 12 | @available( 13 | macOS, deprecated: 9999, 14 | message: "Use 'CasePathable.modify', or 'extract' and 'embed', instead." 15 | ) 16 | @available( 17 | tvOS, deprecated: 9999, 18 | message: "Use 'CasePathable.modify', or 'extract' and 'embed', instead." 19 | ) 20 | @available( 21 | watchOS, deprecated: 9999, 22 | message: "Use 'CasePathable.modify', or 'extract' and 'embed', instead." 23 | ) 24 | public func modify( 25 | _ root: inout Root, 26 | _ body: (inout Value) throws -> Result 27 | ) throws -> Result { 28 | guard var value = self.extract(from: root) else { throw ExtractionFailed() } 29 | let result = try body(&value) 30 | root = self.embed(value) 31 | return result 32 | } 33 | 34 | @available(iOS, deprecated: 9999, message: "Chain case key paths together, instead.") 35 | @available(macOS, deprecated: 9999, message: "Chain case key paths together, instead.") 36 | @available(tvOS, deprecated: 9999, message: "Chain case key paths together, instead.") 37 | @available(watchOS, deprecated: 9999, message: "Chain case key paths together, instead.") 38 | public func appending( 39 | path: AnyCasePath 40 | ) -> AnyCasePath { 41 | AnyCasePath( 42 | embed: { self.embed(path.embed($0)) }, 43 | extract: { self.extract(from: $0).flatMap(path.extract) } 44 | ) 45 | } 46 | } 47 | 48 | struct ExtractionFailed: Error {} 49 | 50 | // Deprecated after 1.4.2: 51 | 52 | extension AnyCasePath where Root == Value { 53 | @available(*, deprecated, message: "Use the '\\.self' case key path, instead") 54 | public static var `self`: Self { 55 | .init( 56 | embed: { $0 }, 57 | extract: { .some($0) } 58 | ) 59 | } 60 | } 61 | 62 | extension AnyCasePath where Root: _OptionalProtocol, Value == Root.Wrapped { 63 | @available(*, deprecated, message: "Use the '\\Optional.Cases.some' case key path, instead") 64 | public static var some: Self { 65 | .init(embed: { Root($0) }, extract: { $0.optional }) 66 | } 67 | } 68 | 69 | @_documentation(visibility: private) 70 | public protocol _OptionalProtocol { 71 | associatedtype Wrapped 72 | var optional: Wrapped? { get } 73 | init(_ some: Wrapped) 74 | } 75 | 76 | @_documentation(visibility: private) 77 | extension Optional: _OptionalProtocol { 78 | public var optional: Wrapped? { self } 79 | } 80 | 81 | extension AnyCasePath { 82 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 83 | public init(_ embed: @escaping (Value) -> Root) { 84 | @UncheckedSendable var embed = embed 85 | self.init(unsafe: { [$embed] in $embed.wrappedValue($0) }) 86 | } 87 | } 88 | 89 | extension AnyCasePath where Value == Void { 90 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 91 | @_disfavoredOverload 92 | public init(_ root: @autoclosure @escaping @Sendable () -> Root) { 93 | self.init(unsafe: root()) 94 | } 95 | } 96 | 97 | extension AnyCasePath where Root == Value { 98 | @available(*, deprecated, message: "Use the '\\.self' case key path, instead") 99 | public init(_ type: Root.Type) { 100 | self = .self 101 | } 102 | } 103 | 104 | prefix operator / 105 | 106 | extension AnyCasePath { 107 | @_documentation(visibility: internal) 108 | @available(*, deprecated, message: "Use 'CasePathable.is' with a case key path, instead") 109 | public static func ~= (pattern: AnyCasePath, value: Root) -> Bool { 110 | pattern.extract(from: value) != nil 111 | } 112 | } 113 | 114 | @_documentation(visibility: internal) 115 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 116 | public prefix func / ( 117 | embed: @escaping (Value) -> Root 118 | ) -> AnyCasePath { 119 | @UncheckedSendable var embed = embed 120 | return AnyCasePath( 121 | embed: { [$embed] in $embed.wrappedValue($0) }, 122 | extract: { [$embed] in extractHelp { $embed.wrappedValue($0) }($0) } 123 | ) 124 | } 125 | 126 | @_documentation(visibility: internal) 127 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 128 | public prefix func / ( 129 | embed: @escaping (Value) -> Root? 130 | ) -> AnyCasePath { 131 | @UncheckedSendable var embed = embed 132 | return AnyCasePath( 133 | embed: { [$embed] in $embed.wrappedValue($0) }, 134 | extract: optionalPromotedExtractHelp { [$embed] in $embed.wrappedValue($0) } 135 | ) 136 | } 137 | 138 | @_documentation(visibility: internal) 139 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 140 | public prefix func / ( 141 | root: @autoclosure @escaping @Sendable () -> Root 142 | ) -> AnyCasePath { 143 | .init(embed: root, extract: extractVoidHelp(root())) 144 | } 145 | 146 | @_documentation(visibility: internal) 147 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 148 | public prefix func / ( 149 | root: @autoclosure @escaping @Sendable () -> Root? 150 | ) -> AnyCasePath { 151 | .init(embed: root, extract: optionalPromotedExtractVoidHelp(root())) 152 | } 153 | 154 | @_documentation(visibility: internal) 155 | @available(*, deprecated, message: "Use the '\\.self' case key path, instead") 156 | public prefix func / ( 157 | type: Root.Type 158 | ) -> AnyCasePath { 159 | .self 160 | } 161 | 162 | @_documentation(visibility: internal) 163 | @available(*, deprecated, message: "Use a case key path (like '\\.self' or '\\.some'), instead") 164 | public prefix func / ( 165 | path: AnyCasePath 166 | ) -> AnyCasePath { 167 | path 168 | } 169 | 170 | @_disfavoredOverload 171 | @_documentation(visibility: internal) 172 | @available( 173 | *, deprecated, message: "Use a 'CasePathable' case property via dynamic member lookup, instead" 174 | ) 175 | public prefix func / ( 176 | embed: @escaping (Value) -> Root 177 | ) -> (Root) -> Value? { 178 | (/embed).extract(from:) 179 | } 180 | 181 | @_disfavoredOverload 182 | @_documentation(visibility: internal) 183 | @available( 184 | *, deprecated, message: "Use a 'CasePathable' case property via dynamic member lookup, instead" 185 | ) 186 | public prefix func / ( 187 | embed: @escaping (Value) -> Root? 188 | ) -> (Root?) -> Value? { 189 | (/embed).extract(from:) 190 | } 191 | 192 | @_disfavoredOverload 193 | @_documentation(visibility: internal) 194 | @available( 195 | *, deprecated, message: "Use a 'CasePathable' case property via dynamic member lookup, instead" 196 | ) 197 | public prefix func / ( 198 | root: @autoclosure @escaping @Sendable () -> Root 199 | ) -> (Root) -> Void? { 200 | (/root).extract(from:) 201 | } 202 | 203 | @_disfavoredOverload 204 | @_documentation(visibility: internal) 205 | @available( 206 | *, deprecated, message: "Use a 'CasePathable' case property via dynamic member lookup, instead" 207 | ) 208 | public prefix func / ( 209 | root: @autoclosure @escaping @Sendable () -> Root 210 | ) -> (Root?) -> Void? { 211 | (/root).extract(from:) 212 | } 213 | 214 | precedencegroup CasePathCompositionPrecedence { 215 | associativity: left 216 | } 217 | 218 | infix operator .. : CasePathCompositionPrecedence 219 | 220 | extension AnyCasePath { 221 | @_documentation(visibility: internal) 222 | @available(*, deprecated, message: "Append 'CasePathable' case key paths, instead") 223 | public static func .. ( 224 | lhs: AnyCasePath, 225 | rhs: AnyCasePath 226 | ) -> AnyCasePath { 227 | lhs.appending(path: rhs) 228 | } 229 | 230 | @_documentation(visibility: internal) 231 | @available(*, deprecated, message: "Append 'CasePathable' case key paths, instead") 232 | public static func .. ( 233 | lhs: AnyCasePath, 234 | rhs: @escaping (AppendedValue) -> Value 235 | ) -> AnyCasePath { 236 | lhs.appending(path: /rhs) 237 | } 238 | } 239 | 240 | @_documentation(visibility: internal) 241 | @available(*, deprecated, message: "Chain 'CasePathable' case properties, instead") 242 | public func .. ( 243 | lhs: @escaping (Root) -> Value?, 244 | rhs: @escaping (AppendedValue) -> Value 245 | ) -> (Root) -> AppendedValue? { 246 | return { root in lhs(root).flatMap((/rhs).extract(from:)) } 247 | } 248 | 249 | @available( 250 | *, deprecated, message: "Use XCTest's 'XCTUnwrap' with a 'CasePathable' case property, instead" 251 | ) 252 | public func XCTUnwrap( 253 | _ enum: @autoclosure () throws -> Enum, 254 | case extract: (Enum) -> Case?, 255 | _ message: @autoclosure () -> String = "", 256 | file: StaticString = #filePath, 257 | line: UInt = #line 258 | ) throws -> Case { 259 | let `enum` = try `enum`() 260 | guard let value = extract(`enum`) 261 | else { 262 | #if canImport(ObjectiveC) 263 | _ = XCTCurrentTestCase?.perform(Selector(("setContinueAfterFailure:")), with: false) 264 | #endif 265 | let message = message() 266 | XCTFail( 267 | """ 268 | XCTUnwrap: Expected to extract value of type "\(typeName(Case.self))" from \ 269 | "\(typeName(Enum.self))"\ 270 | \(message.isEmpty ? "" : " - " + message) … 271 | 272 | Actual: 273 | \(String(describing: `enum`)) 274 | """, 275 | file: file, 276 | line: line 277 | ) 278 | throw UnwrappingCase() 279 | } 280 | return value 281 | } 282 | 283 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 284 | public func XCTModify( 285 | _ enum: inout Enum, 286 | case casePath: AnyCasePath, 287 | _ message: @autoclosure () -> String = "", 288 | _ body: (inout Case) throws -> Void, 289 | file: StaticString = #filePath, 290 | line: UInt = #line 291 | ) { 292 | _XCTModify(&`enum`, case: casePath, message(), body, file: file, line: line) 293 | } 294 | 295 | // Deprecated after 1.0.0: 296 | 297 | /// A type-erased case path that supports embedding a value in a root and attempting to extract a 298 | /// root's embedded value. 299 | /// 300 | /// This type has been renamed to `AnyCasePath` and is primarily employed by the ``CasePathable()`` 301 | /// macro to derive `CaseKeyPath`s from an enum's cases. 302 | @available(*, deprecated, renamed: "AnyCasePath") 303 | public typealias CasePath = AnyCasePath 304 | 305 | @available(*, deprecated, message: "Use 'CustomDebugStringConvertible.debugDescription', instead") 306 | extension AnyCasePath: CustomStringConvertible { 307 | public var description: String { 308 | "AnyCasePath<\(typeName(Root.self)), \(typeName(Value.self))>" 309 | } 310 | } 311 | 312 | extension AnyCasePath where Root == Void { 313 | /// Returns a case path that always successfully extracts the given constant value. 314 | /// 315 | /// - Parameter value: A constant value. 316 | /// - Returns: A case path from `()` to `value`. 317 | @available(*, deprecated) 318 | public static func constant(_ value: @autoclosure @escaping @Sendable () -> Value) -> Self { 319 | .init( 320 | embed: { _ in () }, 321 | extract: { .some(value()) } 322 | ) 323 | } 324 | } 325 | 326 | extension AnyCasePath where Value == Never { 327 | /// The never case path for `Root`: a case path that always fails to extract the a value of the 328 | /// uninhabited `Never` type. 329 | @available(*, deprecated) 330 | public static var never: Self { 331 | @Sendable func absurd(_ never: Never) -> A {} 332 | return .init( 333 | embed: absurd, 334 | extract: { _ in nil } 335 | ) 336 | } 337 | } 338 | 339 | extension AnyCasePath where Value: RawRepresentable, Root == Value.RawValue { 340 | /// Returns a case path for `RawRepresentable` types: a case path that attempts to extract a value 341 | /// that can be represented by a raw value from a raw value. 342 | @available(*, deprecated) 343 | public static var rawValue: Self { 344 | .init( 345 | embed: { $0.rawValue }, 346 | extract: { Value(rawValue: $0) } 347 | ) 348 | } 349 | } 350 | 351 | extension AnyCasePath where Value: LosslessStringConvertible, Root == String { 352 | /// Returns a case path for `LosslessStringConvertible` types: a case path that attempts to 353 | /// extract a value that can be represented by a lossless string from a string. 354 | @available(*, deprecated) 355 | public static var description: Self { 356 | .init( 357 | embed: { $0.description }, 358 | extract: { Value($0) } 359 | ) 360 | } 361 | } 362 | 363 | // Deprecated after 0.5.0: 364 | 365 | extension AnyCasePath { 366 | /// Returns a case path that extracts values associated with a given enum case initializer. 367 | /// 368 | /// - Note: This function is only intended to be used with enum case initializers. Its behavior is 369 | /// otherwise undefined. 370 | /// - Parameter embed: An enum case initializer. 371 | /// - Returns: A case path that extracts associated values from enum cases. 372 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 373 | public static func `case`(_ embed: @escaping @Sendable (Value) -> Root) -> Self { 374 | self.init( 375 | embed: embed, 376 | extract: { CasePaths.extract(embed)($0) } 377 | ) 378 | } 379 | } 380 | 381 | extension AnyCasePath where Value == Void { 382 | /// Returns a case path that successfully extracts `()` from a given enum case with no associated 383 | /// values. 384 | /// 385 | /// - Note: This function is only intended to be used with enum cases that have no associated 386 | /// values. Its behavior is otherwise undefined. 387 | /// - Parameter value: An enum case with no associated values. 388 | /// - Returns: A case path that extracts `()` if the case matches, otherwise `nil`. 389 | @available(*, deprecated, message: "Use a 'CasePathable' case key path, instead") 390 | public static func `case`(_ value: @autoclosure @escaping @Sendable () -> Root) -> Self { 391 | Self( 392 | embed: value, 393 | extract: extractVoidHelp(value()) 394 | ) 395 | } 396 | } 397 | 398 | /// Attempts to extract values associated with a given enum case initializer from a given root enum. 399 | /// 400 | /// ```swift 401 | /// extract(case: Result.success, from: .success(42)) 402 | /// // 42 403 | /// extract(case: Result.success, from: .failure(MyError()) 404 | /// // nil 405 | /// ``` 406 | /// 407 | /// - Note: This function is only intended to be used with enum case initializers. Its behavior is 408 | /// otherwise undefined. 409 | /// - Parameters: 410 | /// - embed: An enum case initializer. 411 | /// - root: A root enum value. 412 | /// - Returns: Values if they can be extracted from the given enum case initializer and root enum, 413 | /// otherwise `nil`. 414 | @available(*, deprecated, message: "Use a '@CasePathable' case property, instead") 415 | public func extract( 416 | case embed: @escaping @Sendable (Value) -> Root, 417 | from root: Root 418 | ) -> Value? { 419 | CasePaths.extract(embed)(root) 420 | } 421 | 422 | /// Attempts to extract values associated with a given enum case initializer from a given root enum. 423 | /// 424 | /// ```swift 425 | /// extract(case: Result.success, from: .success(42)) 426 | /// // 42 427 | /// extract(case: Result.success, from: .failure(MyError()) 428 | /// // nil 429 | /// ``` 430 | /// 431 | /// - Note: This function is only intended to be used with enum case initializers. Its behavior is 432 | /// otherwise undefined. 433 | /// - Parameters: 434 | /// - embed: An enum case initializer. 435 | /// - root: A root enum value. 436 | /// - Returns: Values if they can be extracted from the given enum case initializer and root enum, 437 | /// otherwise `nil`. 438 | @available(*, deprecated, message: "Use a '@CasePathable' case property, instead") 439 | public func extract( 440 | case embed: @escaping @Sendable (Value) -> Root?, 441 | from root: Root? 442 | ) -> Value? { 443 | CasePaths.extract(embed)(root) 444 | } 445 | 446 | /// Returns a function that can attempt to extract associated values from the given enum case 447 | /// initializer. 448 | /// 449 | /// Use this function to create new transform functions to pass to higher-order methods like 450 | /// `compactMap`: 451 | /// 452 | /// ```swift 453 | /// [Result.success(42), .failure(MyError()] 454 | /// .compactMap(extract(Result.success)) 455 | /// // [42] 456 | /// ``` 457 | /// 458 | /// - Note: This function is only intended to be used with enum case initializers. Its behavior is 459 | /// otherwise undefined. 460 | /// - Parameter embed: An enum case initializer. 461 | /// - Returns: A function that can attempt to extract associated values from an enum. 462 | @available(*, deprecated, message: "Use a '@CasePathable' case property, instead") 463 | public func extract(_ embed: @escaping @Sendable (Value) -> Root) -> (Root) -> Value? { 464 | extractHelp(embed) 465 | } 466 | 467 | /// Returns a function that can attempt to extract associated values from the given enum case 468 | /// initializer. 469 | /// 470 | /// Use this function to create new transform functions to pass to higher-order methods like 471 | /// `compactMap`: 472 | /// 473 | /// ```swift 474 | /// [Result.success(42), .failure(MyError()] 475 | /// .compactMap(extract(Result.success)) 476 | /// // [42] 477 | /// ``` 478 | /// 479 | /// - Note: This function is only intended to be used with enum case initializers. Its behavior is 480 | /// otherwise undefined. 481 | /// - Parameter embed: An enum case initializer. 482 | /// - Returns: A function that can attempt to extract associated values from an enum. 483 | @available(*, deprecated, message: "Use a '@CasePathable' case property, instead") 484 | public func extract( 485 | _ embed: @escaping @Sendable (Value) -> Root? 486 | ) -> @Sendable (Root?) -> Value? { 487 | optionalPromotedExtractHelp(embed) 488 | } 489 | -------------------------------------------------------------------------------- /Sources/CasePaths/Internal/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import CasePathsCore 2 | -------------------------------------------------------------------------------- /Sources/CasePaths/Internal/LockIsolated.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class LockIsolated: @unchecked Sendable { 4 | private var _value: Value 5 | private let lock = NSRecursiveLock() 6 | init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { 7 | self._value = try value() 8 | } 9 | func withLock( 10 | _ operation: @Sendable (inout Value) throws -> T 11 | ) rethrows -> T { 12 | lock.lock() 13 | defer { lock.unlock() } 14 | var value = _value 15 | defer { _value = value } 16 | return try operation(&value) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CasePaths/Macros.swift: -------------------------------------------------------------------------------- 1 | /// Defines and implements conformance of the CasePathable protocol. 2 | /// 3 | /// This macro conforms the type to the `CasePathable` protocol, and adds `CaseKeyPath` support for 4 | /// all its cases. 5 | /// 6 | /// For example, the following code applies the `CasePathable` macro to the type `UserAction`: 7 | /// 8 | /// ```swift 9 | /// @CasePathable 10 | /// enum UserAction { 11 | /// case home(HomeAction) 12 | /// case settings(SettingsAction) 13 | /// } 14 | /// ``` 15 | /// 16 | /// This macro application extends the type with the ability to derive case key paths from each 17 | /// of its cases using a familiar key path expression: 18 | /// 19 | /// ```swift 20 | /// // Case key paths can be inferred using the same name as the case: 21 | /// _: CaseKeyPath = \.home 22 | /// _: CaseKeyPath = \.settings 23 | /// 24 | /// // Or they can be fully qualified under the type's `Cases`: 25 | /// \UserAction.Cases.home // CasePath 26 | /// \UserAction.Cases.settings // CasePath 27 | /// ``` 28 | @attached(extension, conformances: CasePathable, CasePathIterable) 29 | @attached(member, names: named(AllCasePaths), named(allCasePaths), named(_$Element)) 30 | public macro CasePathable() = 31 | #externalMacro( 32 | module: "CasePathsMacros", type: "CasePathableMacro" 33 | ) 34 | -------------------------------------------------------------------------------- /Sources/CasePaths/XCTestSupport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @_spi(CurrentTestCase) import XCTestDynamicOverlay 3 | 4 | /// Asserts that an enum value matches a particular case and modifies the associated value in place. 5 | @available(*, deprecated, message: "Use 'CasePathable.modify' to mutate an expected case, instead.") 6 | public func XCTModify( 7 | _ optional: inout Wrapped?, 8 | _ message: @autoclosure () -> String = "", 9 | _ body: (inout Wrapped) throws -> Void, 10 | file: StaticString = #filePath, 11 | line: UInt = #line 12 | ) { 13 | XCTModify(&optional, case: .some, message(), body, file: file, line: line) 14 | } 15 | 16 | /// Asserts that an enum value matches a particular case and modifies the associated value in place. 17 | @available(*, deprecated, message: "Use 'CasePathable.modify' to mutate an expected case, instead.") 18 | public func XCTModify( 19 | _ enum: inout Enum, 20 | case keyPath: CaseKeyPath, 21 | _ message: @autoclosure () -> String = "", 22 | _ body: (inout Case) throws -> Void, 23 | file: StaticString = #filePath, 24 | line: UInt = #line 25 | ) { 26 | _XCTModify(&`enum`, case: AnyCasePath(keyPath), message(), body, file: file, line: line) 27 | } 28 | 29 | func _XCTModify( 30 | _ enum: inout Enum, 31 | case casePath: AnyCasePath, 32 | _ message: @autoclosure () -> String = "", 33 | _ body: (inout Case) throws -> Void, 34 | file: StaticString = #filePath, 35 | line: UInt = #line 36 | ) { 37 | guard var value = casePath.extract(from: `enum`) 38 | else { 39 | #if canImport(ObjectiveC) 40 | _ = XCTCurrentTestCase?.perform(Selector(("setContinueAfterFailure:")), with: false) 41 | #endif 42 | let message = message() 43 | XCTFail( 44 | """ 45 | XCTModify: Expected to extract value of type "\(typeName(Case.self))" from \ 46 | "\(typeName(Enum.self))"\ 47 | \(message.isEmpty ? "" : " - " + message) … 48 | 49 | Actual: 50 | \(`enum`) 51 | """, 52 | file: file, 53 | line: line 54 | ) 55 | return 56 | } 57 | let before = value 58 | do { 59 | try body(&value) 60 | } catch { 61 | XCTFail("Threw error: \(error)", file: file, line: line) 62 | return 63 | } 64 | 65 | if XCTModifyLocals.isExhaustive, 66 | let isEqual = _isEqual(before, value), 67 | isEqual 68 | { 69 | XCTFail( 70 | """ 71 | XCTModify: Expected "\(typeName(Case.self))" value to be modified but it was unchanged. 72 | """ 73 | ) 74 | } 75 | 76 | `enum` = casePath.embed(value) 77 | } 78 | 79 | @_spi(Internals) public enum XCTModifyLocals { 80 | @TaskLocal public static var isExhaustive = true 81 | } 82 | 83 | struct UnwrappingCase: Error {} 84 | 85 | func _isEqual(_ lhs: Any, _ rhs: Any) -> Bool? { 86 | (lhs as? any Equatable)?.isEqual(other: rhs) 87 | } 88 | 89 | extension Equatable { 90 | fileprivate func isEqual(other: Any) -> Bool { 91 | self == other as? Self 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/AnyCasePath.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type-erased case path that supports embedding a value in a root and attempting to extract a 4 | /// root's embedded value. 5 | /// 6 | /// This type defines key path-like semantics for enum cases, and is used to derive ``CaseKeyPath``s 7 | /// from types that conform to ``CasePathable``. 8 | @dynamicMemberLookup 9 | public struct AnyCasePath: Sendable { 10 | private let _embed: @Sendable (Value) -> Root 11 | private let _extract: @Sendable (Root) -> Value? 12 | 13 | /// Creates a type-erased case path from a pair of functions. 14 | /// 15 | /// - Parameters: 16 | /// - embed: A function that always succeeds in embedding a value in a root. 17 | /// - extract: A function that can optionally fail in extracting a value from a root. 18 | public init( 19 | embed: @escaping @Sendable (Value) -> Root, 20 | extract: @escaping @Sendable (Root) -> Value? 21 | ) { 22 | self._embed = embed 23 | self._extract = extract 24 | } 25 | 26 | public static func _$embed( 27 | _ embed: @escaping (Value) -> Root, 28 | extract: @escaping @Sendable (Root) -> Value? 29 | ) -> Self { 30 | #if swift(>=5.10) 31 | nonisolated(unsafe) let embed = embed 32 | return Self(embed: { embed($0) }, extract: extract) 33 | #else 34 | @UncheckedSendable var embed = embed 35 | return Self(embed: { [$embed] in $embed.wrappedValue($0) }, extract: extract) 36 | #endif 37 | } 38 | 39 | /// Returns a root by embedding a value. 40 | /// 41 | /// - Parameter value: A value to embed. 42 | /// - Returns: A root that embeds `value`. 43 | public func embed(_ value: Value) -> Root { 44 | self._embed(value) 45 | } 46 | 47 | /// Attempts to extract a value from a root. 48 | /// 49 | /// - Parameter root: A root to extract from. 50 | /// - Returns: A value if it can be extracted from the given root, otherwise `nil`. 51 | public func extract(from root: Root) -> Value? { 52 | self._extract(root) 53 | } 54 | } 55 | 56 | extension AnyCasePath where Root == Value { 57 | /// The identity case path. 58 | /// 59 | /// A case path that: 60 | /// 61 | /// * Given a value to embed, returns the given value. 62 | /// * Given a value to extract, returns the given value. 63 | public init() where Root == Value { 64 | self.init(embed: { $0 }, extract: { $0 }) 65 | } 66 | } 67 | 68 | extension AnyCasePath: CustomDebugStringConvertible { 69 | public var debugDescription: String { 70 | "AnyCasePath<\(typeName(Root.self)), \(typeName(Value.self))>" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/CasePathIterable.swift: -------------------------------------------------------------------------------- 1 | /// A type that provides a collection of all of its case paths. 2 | /// 3 | /// The `@CasePathable` macro automatically generates a conformance to this protocol. 4 | /// 5 | /// You can iterate over ``CasePathable/allCasePaths`` to get access to each individual case path: 6 | /// 7 | /// ```swift 8 | /// @CasePathable enum Field { 9 | /// case title(String) 10 | /// case body(String 11 | /// case isLive 12 | /// } 13 | /// 14 | /// Array(Field.allCasePaths) // [\.title, \.body, \.isLive] 15 | /// ``` 16 | public protocol CasePathIterable: CasePathable 17 | where AllCasePaths: Sequence, AllCasePaths.Element == PartialCaseKeyPath {} 18 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/CasePathReflectable.swift: -------------------------------------------------------------------------------- 1 | /// A type that can reflect a case path from a given case. 2 | /// 3 | /// The `@CasePathable` macro automatically generates a conformance to this protocol on the enum's 4 | /// ``CasePathable/AllCasePaths`` type. 5 | /// 6 | /// You can look up an enum's case path by passing it to ``CasePathReflectable/subscript(_:)``: 7 | /// 8 | /// ```swift 9 | /// @CasePathable 10 | /// enum Field { 11 | /// case title(String) 12 | /// case body(String) 13 | /// case isLive 14 | /// } 15 | /// 16 | /// Field.allCasePaths[.title("Hello, Blob!")] // \.title 17 | /// ``` 18 | public protocol CasePathReflectable { 19 | /// The enum type that can be reflected. 20 | associatedtype Root: CasePathable 21 | 22 | /// Returns the case key path for a given root value. 23 | /// 24 | /// - Parameter root: An root value. 25 | /// - Returns: A case path to the root value. 26 | subscript(root: Root) -> PartialCaseKeyPath { get } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/CasePathable.swift: -------------------------------------------------------------------------------- 1 | import IssueReporting 2 | 3 | /// A type that provides a collection of all of its case paths. 4 | /// 5 | /// Use the `@CasePathable` macro to automatically add case paths, and this conformance, to an enum. 6 | /// 7 | /// It is also possible, though less common, to manually conform a type to `CasePathable`. For 8 | /// example, the `Result` type is extended to be case-pathable with the following extension: 9 | /// 10 | /// ```swift 11 | /// extension Result: CasePathable { 12 | /// public struct AllCasePaths { 13 | /// var success: AnyCasePath { 14 | /// AnyCasePath( 15 | /// embed: { .success($0) }, 16 | /// extract: { 17 | /// guard case let .success(value) = $0 else { return nil } 18 | /// return value 19 | /// } 20 | /// ) 21 | /// } 22 | /// 23 | /// var failure: AnyCasePath { 24 | /// AnyCasePath( 25 | /// embed: { .failure($0) }, 26 | /// extract: { 27 | /// guard case let .failure(value) = $0 else { return nil } 28 | /// return value 29 | /// } 30 | /// ) 31 | /// } 32 | /// } 33 | /// 34 | /// public static var allCasePaths: AllCasePaths { AllCasePaths() } 35 | /// } 36 | /// ``` 37 | public protocol CasePathable { 38 | /// A type that can represent a collection of all case paths of this type. 39 | associatedtype AllCasePaths 40 | 41 | /// A collection of all case paths of this type. 42 | static var allCasePaths: AllCasePaths { get } 43 | } 44 | 45 | /// A type that is used to distinguish case key paths from key paths by wrapping the enum and 46 | /// associated value types. 47 | @_documentation(visibility: internal) 48 | @dynamicMemberLookup 49 | public struct Case: Sendable { 50 | fileprivate let _embed: @Sendable (Value) -> Any 51 | fileprivate let _extract: @Sendable (Any) -> Value? 52 | } 53 | 54 | extension Case { 55 | public init( 56 | embed: @escaping @Sendable (Value) -> Root, 57 | extract: @escaping @Sendable (Root) -> Value? 58 | ) { 59 | self._embed = embed 60 | self._extract = { @Sendable in ($0 as? Root).flatMap(extract) } 61 | } 62 | 63 | public init() { 64 | self.init(embed: { $0 }, extract: { $0 }) 65 | } 66 | 67 | public init(_ keyPath: CaseKeyPath) { 68 | self = Case()[keyPath: keyPath] 69 | } 70 | 71 | public subscript( 72 | dynamicMember keyPath: KeyPath> 73 | ) -> Case 74 | where Value: CasePathable { 75 | let keyPath = keyPath.unsafeSendable() 76 | return Case( 77 | embed: { 78 | _embed(Value.allCasePaths[keyPath: keyPath].embed($0)) 79 | }, 80 | extract: { 81 | _extract(from: $0).flatMap(Value.allCasePaths[keyPath: keyPath].extract) 82 | } 83 | ) 84 | } 85 | 86 | public func _embed(_ value: Value) -> Any { 87 | self._embed(value) 88 | } 89 | 90 | public func _extract(from root: Any) -> Value? { 91 | self._extract(root) 92 | } 93 | } 94 | 95 | private protocol _AnyCase { 96 | func extractAny(from root: Any) -> Any? 97 | } 98 | 99 | extension Case: _AnyCase { 100 | fileprivate func extractAny(from root: Any) -> Any? { 101 | self._extract(from: root) 102 | } 103 | } 104 | 105 | /// A key path to the associated value of an enum case. 106 | /// 107 | /// The most common way to make an instance of this type is by applying the `@CasePathable` macro 108 | /// to an enum and using a key path expression like `\SomeEnum.Cases.someCase`, or simply 109 | /// `\.someCase` where the type can be inferred. 110 | /// 111 | /// To extract an associated value from an enum using a case key path, pass the key path to the 112 | /// ``CasePathable/subscript(case:)-3yqx3``. For example: 113 | /// 114 | /// ```swift 115 | /// @CasePathable 116 | /// enum SomeEnum { 117 | /// case someCase(Int) 118 | /// case anotherCase(String) 119 | /// } 120 | /// 121 | /// let e = SomeEnum.someCase(12) 122 | /// let pathToCase = \SomeEnum.Cases.someCase 123 | /// 124 | /// let value = e[case: pathToCase] 125 | /// // value is Optional(12) 126 | /// 127 | /// let anotherValue = e[case: \.anotherCase] 128 | /// // anotherValue is nil 129 | /// ``` 130 | /// 131 | /// To replace an associated value, assign it through ``CasePathable/subscript(case:)-2t4f8``. If 132 | /// the given path does not match the given enum case, the replacement will fail. For 133 | /// example: 134 | /// 135 | /// ```swift 136 | /// var e = SomeEnum.someCase(12) 137 | /// 138 | /// e[case: \.someCase] = 24 139 | /// // e is SomeEnum.someCase(24) 140 | /// 141 | /// e[case: \.anotherCase] = "Hello!" 142 | /// // Assignment fails: e is still SomeEnum.someCase(24) 143 | /// ``` 144 | /// 145 | /// To produce a whole instance from a case key path, call the key path directly with the associated 146 | /// value you'd like to embed (via ``Swift/KeyPath/callAsFunction(_:)``): 147 | /// 148 | /// ```swift 149 | /// let pathToCase = \SomeEnum.Cases.someCase 150 | /// 151 | /// let e = pathToCase(12) 152 | /// // e is SomeEnum.someCase(12) 153 | /// ``` 154 | /// 155 | /// The path can contain multiple case names, separated by periods, to refer to a case of a case's 156 | /// value. This code uses the key path expression `\OuterEnum.Cases.outer.someCase` to access the 157 | /// `someCase` associated value of the `OuterEnum` type's `outer` case: 158 | /// 159 | /// ```swift 160 | /// @CasePathable 161 | /// enum OuterEnum { 162 | /// case outer(SomeEnum) 163 | /// } 164 | /// 165 | /// var nested = OuterEnum.outer(.someCase(24)) 166 | /// let nestedCaseKeyPath = \OuterEnum.Cases.outer.someCase 167 | /// 168 | /// let nestedValue = nested[case: nestedCaseKeyPath] 169 | /// // nestedValue is Optional(24) 170 | /// 171 | /// nested[case: \.outer.someCase] = 42 172 | /// // nested is now OuterEnum.outer(.someCase(42)) 173 | /// ``` 174 | /// 175 | /// Key paths have the identity key path `\SomeStructure.self`, and so case key paths have the 176 | /// identity case key path `\SomeEnum.Cases.self`. It refers to the whole enum and can be passed to 177 | /// a function that takes case key paths when you want to extract, change, or replace all of the 178 | /// data stored in an enum in a single step. 179 | public typealias CaseKeyPath = KeyPath, Case> 180 | 181 | extension CaseKeyPath { 182 | /// Embeds a value in an enum at this case key path's case. 183 | /// 184 | /// Given a case key path to an enum case, one can produce a whole new root value to that case by 185 | /// invoking the key path like a function with an associated value to embed. For example: 186 | /// 187 | /// ```swift 188 | /// @CasePathable 189 | /// enum SomeEnum { 190 | /// case someCase(Int) 191 | /// } 192 | /// 193 | /// let path = \SomeEnum.Cases.someCase 194 | /// 195 | /// let e = path(12) 196 | /// // e is SomeEnum.someCase(12) 197 | /// ``` 198 | /// 199 | /// See ``Swift/KeyPath/callAsFunction()`` for cases with no associated values. 200 | /// 201 | /// - Parameter value: A value to embed. 202 | /// - Returns: An enum for the case of this key path that holds the given value. 203 | public func callAsFunction(_ value: AssociatedValue) -> Enum 204 | where Root == Case, Value == Case { 205 | Case(self)._embed(value) as! Enum 206 | } 207 | 208 | /// Returns an enum for this case key path's case. 209 | /// 210 | /// Given a case key path to an enum case with no associated value, one can produce a whole new 211 | /// root value to that case by invoking the key path like a function. For example: 212 | /// 213 | /// ```swift 214 | /// @CasePathable 215 | /// enum SomeEnum { 216 | /// case someCase 217 | /// } 218 | /// 219 | /// let path = \SomeEnum.Cases.someCase 220 | /// 221 | /// let e = path() 222 | /// // e is SomeEnum.someCase 223 | /// ``` 224 | /// 225 | /// See ``Swift/KeyPath/callAsFunction(_:)`` for cases with associated values. 226 | /// 227 | /// - Returns: An enum for the case of this key path. 228 | public func callAsFunction() -> Enum 229 | where Root == Case, Value == Case { 230 | Case(self)._embed(()) as! Enum 231 | } 232 | 233 | /// Whether an argument matches the case key path's case. 234 | /// 235 | /// ```swift 236 | /// @CasePathable enum UserAction { 237 | /// case settings(SettingsAction) 238 | /// } 239 | /// @CasePathable enum SettingsAction { 240 | /// case store(StoreAction) 241 | /// } 242 | /// @CasePathable enum StoreAction { 243 | /// case subscribeButtonTapped 244 | /// } 245 | /// 246 | /// switch userAction { 247 | /// case \.settings.store.subscribeButtonTapped: 248 | /// // ... 249 | /// } 250 | /// 251 | /// // Equivalent to: 252 | /// 253 | /// switch userAction { 254 | /// case .settings(.store(.subscribeButtonTapped)): 255 | /// // ... 256 | /// } 257 | /// ``` 258 | /// 259 | /// - Parameters: 260 | /// - lhs: A case key path. 261 | /// - rhs: An enum. 262 | public static func ~= (lhs: KeyPath, rhs: Enum) -> Bool 263 | where Root == Case, Value == Case { 264 | rhs[case: lhs] != nil 265 | } 266 | } 267 | 268 | /// A partially type-erased key path, from a concrete root enum to any resulting value type. 269 | public typealias PartialCaseKeyPath = PartialKeyPath> 270 | 271 | extension _AppendKeyPath { 272 | /// Attempts to embeds any value in an enum at this case key path's case. 273 | /// 274 | /// - Parameter value: A value to embed. If the value type does not match the case path's value 275 | /// type, the operation will fail. 276 | /// - Returns: An enum for the case of this key path that holds the given value, or `nil`. 277 | @_disfavoredOverload 278 | public func callAsFunction( 279 | _ value: Any 280 | ) -> Enum? 281 | where Self == PartialCaseKeyPath { 282 | func open(_ value: AnyAssociatedValue) -> Enum? { 283 | (Case()[keyPath: self] as? Case)?._embed(value) as? Enum 284 | ?? (Case()[keyPath: self] as? Case)?._embed(value) as? Enum 285 | } 286 | return _openExistential(value, do: open) 287 | } 288 | } 289 | 290 | extension CasePathable { 291 | /// A namespace that can be used to derive case key paths from case-pathable enums. 292 | /// 293 | /// One can fully-qualify a ``CaseKeyPath`` for a type conforming to ``CasePathable`` through this 294 | /// namespace. For example: 295 | /// 296 | /// ```swift 297 | /// @CasePathable 298 | /// enum SomeEnum { 299 | /// case someCase(Int) 300 | /// } 301 | /// 302 | /// \SomeEnum.Cases.someCase // CaseKeyPath 303 | /// ``` 304 | public typealias Cases = Case 305 | 306 | /// Attempts to extract the associated value from a root enum using a case key path. 307 | /// 308 | /// For example: 309 | /// 310 | /// ```swift 311 | /// @CasePathable 312 | /// enum SomeEnum { 313 | /// case someCase(Int) 314 | /// case anotherCase(String) 315 | /// } 316 | /// 317 | /// let e = SomeEnum.someCase(12) 318 | /// 319 | /// e[case: \.someCase] // Optional(12) 320 | /// e[case: \.anotherCase] // nil 321 | /// ``` 322 | /// 323 | /// See ``CasePathable/subscript(case:)-2t4f8`` for replacing an associated value in a root 324 | /// enum, and see ``Swift/KeyPath/callAsFunction(_:)`` for embedding an associated value in a 325 | /// brand new root enum. 326 | public subscript(case keyPath: CaseKeyPath) -> Value? { 327 | Case(keyPath)._extract(from: self) 328 | } 329 | 330 | /// Attempts to extract the associated value from a root enum using a partial case key path. 331 | @_disfavoredOverload 332 | public subscript(case keyPath: PartialCaseKeyPath) -> Any? { 333 | (Case()[keyPath: keyPath] as? any _AnyCase)?.extractAny(from: self) 334 | } 335 | 336 | /// Replaces the associated value of a root enum at a case key path when the case matches. 337 | /// 338 | /// For example: 339 | /// 340 | /// ```swift 341 | /// @CasePathable 342 | /// enum SomeEnum { 343 | /// case someCase(Int) 344 | /// case anotherCase(String) 345 | /// } 346 | /// 347 | /// var e = SomeEnum.someCase(12) 348 | /// 349 | /// e[case: \.someCase] = 24 350 | /// // e is SomeEnum.someCase(24) 351 | /// 352 | /// e[case: \.anotherCase] = "Hello!" 353 | /// // e is still SomeEnum.someCase(24) 354 | /// ``` 355 | /// 356 | /// See ``CasePathable/subscript(case:)-3yqx3`` for extracting an associated value from a root 357 | /// enum, and see ``Swift/KeyPath/callAsFunction(_:)`` for embedding an associated value in a 358 | /// brand new root enum. 359 | @_disfavoredOverload 360 | public subscript(case keyPath: CaseKeyPath) -> Value { 361 | @available(*, unavailable) 362 | get { fatalError() } 363 | set { 364 | let `case` = Case(keyPath) 365 | guard `case`._extract(from: self) != nil else { return } 366 | self = `case`._embed(newValue) as! Self 367 | } 368 | } 369 | 370 | /// Extracts the associated value of a case via dynamic member lookup. 371 | /// 372 | /// Simply annotate the base type with `@dynamicMemberLookup` to enable this functionality: 373 | /// 374 | /// ```swift 375 | /// @CasePathable 376 | /// @dynamicMemberLookup 377 | /// enum UserAction { 378 | /// case home(HomeAction) 379 | /// case settings(SettingsAction) 380 | /// } 381 | /// 382 | /// let userAction: UserAction = .home(.onAppear) 383 | /// userAction.home // Optional(HomeAction.onAppear) 384 | /// userAction.settings // nil 385 | /// 386 | /// let userActions: [UserAction] = [.home(.onAppear), .settings(.subscribeButtonTapped)] 387 | /// userActions.compactMap(\.home) // [HomeAction.onAppear] 388 | /// userActions.compactMap(\.settings) // [SettingsAction.subscribeButtonTapped] 389 | /// ``` 390 | public subscript( 391 | dynamicMember keyPath: KeyPath> 392 | ) -> Value? { 393 | get { Self.allCasePaths[keyPath: keyPath].extract(from: self) } 394 | @available(*, unavailable, message: "Write 'enum = .case(value)', not 'enum.case = value'") 395 | set { 396 | let casePath = Self.allCasePaths[keyPath: keyPath] 397 | guard casePath.extract(from: self) != nil else { 398 | return 399 | } 400 | if let newValue { 401 | self = casePath.embed(newValue) 402 | } 403 | } 404 | } 405 | 406 | /// Embeds the associated value of a case via dynamic member lookup. 407 | @_disfavoredOverload 408 | public subscript( 409 | dynamicMember keyPath: KeyPath> 410 | ) -> Value { 411 | @available(*, unavailable) 412 | get { Self.allCasePaths[keyPath: keyPath].extract(from: self)! } 413 | set { 414 | let casePath = Self.allCasePaths[keyPath: keyPath] 415 | guard casePath.extract(from: self) != nil else { 416 | return 417 | } 418 | self = casePath.embed(newValue) 419 | } 420 | } 421 | 422 | /// Tests the associated value of a case. 423 | /// 424 | /// ```swift 425 | /// @CasePathable 426 | /// enum UserAction { 427 | /// case home(HomeAction) 428 | /// case settings(SettingsAction) 429 | /// } 430 | /// 431 | /// let userAction: UserAction = .home(.onAppear) 432 | /// userAction.is(\.home) // true 433 | /// userAction.is(\.settings) // false 434 | /// 435 | /// let userActions: [UserAction] = [.home(.onAppear), .settings(.subscribeButtonTapped)] 436 | /// userActions.filter { $0.is(\.home) } // [UserAction.home(.onAppear)] 437 | /// userActions.filter { $0.is(\.settings) } // [UserAction.settings(.subscribeButtonTapped)] 438 | /// ``` 439 | public func `is`(_ keyPath: PartialCaseKeyPath) -> Bool { 440 | self[case: keyPath] != nil 441 | } 442 | 443 | /// Unwraps and yields a mutable associated value to a closure. 444 | /// 445 | /// > Warning: If the enum's case does not match the given case key path, the mutation will not be 446 | /// > applied, and a runtime warning will be logged. To suppress these warnings, limit calls to 447 | /// > `modify` to instances in which you have already checked the enum case. For example: 448 | /// > 449 | /// > ```swift 450 | /// > switch e { 451 | /// > case .someCase: 452 | /// > e.modify(\.someCase) { int in 453 | /// > int += 1 454 | /// > } 455 | /// > case .anotherCase: 456 | /// > e.modify(\.anotherCase) { string in 457 | /// > string.append("!") 458 | /// > } 459 | /// > } 460 | /// > ``` 461 | /// 462 | /// - Parameters: 463 | /// - keyPath: A case key path to an associated value. 464 | /// - yield: A closure given mutable access to that associated value. 465 | /// - fileID: The fileID where the modify occurs. 466 | /// - filePath: The filePath where the modify occurs. 467 | /// - line: The line where the modify occurs. 468 | /// - column: The column where the modify occurs. 469 | public mutating func modify( 470 | _ keyPath: CaseKeyPath, 471 | yield: (inout Value) -> Void, 472 | fileID: StaticString = #fileID, 473 | filePath: StaticString = #filePath, 474 | line: UInt = #line, 475 | column: UInt = #column 476 | ) { 477 | let `case` = Case(keyPath) 478 | guard var value = `case`._extract(from: self) else { 479 | reportIssue( 480 | """ 481 | Can't modify '\(String(describing: self))' via 'CaseKeyPath<\(Self.self), \(Value.self)>' \ 482 | (aka '\(String(reflecting: keyPath))') 483 | """, 484 | fileID: fileID, 485 | filePath: filePath, 486 | line: line, 487 | column: column 488 | ) 489 | return 490 | } 491 | yield(&value) 492 | self = `case`._embed(value) as! Self 493 | } 494 | } 495 | 496 | extension AnyCasePath { 497 | /// Creates a type-erased case path for given case key path. 498 | /// 499 | /// - Parameter keyPath: A case key path. 500 | public init(_ keyPath: CaseKeyPath) { 501 | let `case` = Case(keyPath) 502 | self.init( 503 | embed: { `case`._embed($0) as! Root }, 504 | extract: { `case`._extract(from: $0) } 505 | ) 506 | } 507 | } 508 | 509 | extension AnyCasePath where Value: CasePathable { 510 | /// Returns a new case path created by appending the case path at the given key path to this one. 511 | /// 512 | /// This subscript is automatically invoked by case key path expressions via dynamic member 513 | /// lookup, and should not be invoked directly. 514 | /// 515 | /// - Parameter keyPath: A key path to a case-pathable case path. 516 | public subscript( 517 | dynamicMember keyPath: KeyPath> 518 | ) -> AnyCasePath { 519 | let keyPath = keyPath.unsafeSendable() 520 | return AnyCasePath( 521 | embed: { 522 | embed(Value.allCasePaths[keyPath: keyPath].embed($0)) 523 | }, 524 | extract: { 525 | extract(from: $0).flatMap( 526 | Value.allCasePaths[keyPath: keyPath].extract(from:) 527 | ) 528 | } 529 | ) 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Documentation.docc/CasePathsCore.md: -------------------------------------------------------------------------------- 1 | # ``CasePathsCore`` 2 | 3 | Case paths bring the power and ergonomics of key paths to enums. 4 | 5 | ## Overview 6 | 7 | This module contains the core functionality of the Case Paths library, minus the `@CasePathable` 8 | macro, and is automatically imported when you `import CasePaths` 9 | 10 | See the [`CasePaths`](../casepaths) module for information about the `@CasePathable` macro and 11 | other non-core functionality. 12 | 13 | ## Topics 14 | 15 | ### Creating case paths 16 | 17 | - ``CasePathable`` 18 | - ``CaseKeyPath`` 19 | 20 | ### Swift support 21 | 22 | - ``Swift/Optional`` 23 | - ``Swift/Result`` 24 | - ``Swift/Never`` 25 | 26 | ### Migration guides 27 | 28 | - 29 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Documentation.docc/Extensions/AnyCasePath.md: -------------------------------------------------------------------------------- 1 | # ``CasePathsCore/AnyCasePath`` 2 | 3 | ## Topics 4 | 5 | ### Creating paths 6 | 7 | - ``init(embed:extract:)`` 8 | - ``init(_:)`` 9 | - ``init()`` 10 | 11 | ### Extracting and embedding values 12 | 13 | - ``extract(from:)`` 14 | - ``embed(_:)`` 15 | 16 | ### Appending paths 17 | 18 | - ``subscript(dynamicMember:)`` 19 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Documentation.docc/Extensions/CaseKeyPath.md: -------------------------------------------------------------------------------- 1 | # ``CaseKeyPath`` 2 | 3 | ## Topics 4 | 5 | ### Creating root values 6 | 7 | - ``Swift/KeyPath/callAsFunction(_:)`` 8 | - ``Swift/KeyPath/callAsFunction()`` 9 | 10 | ### Matching associated values 11 | 12 | - ``Swift/KeyPath/~=(_:_:)`` 13 | 14 | ### Partial case paths 15 | 16 | - ``PartialCaseKeyPath`` 17 | - ``CasePathable/subscript(case:)-73fim`` 18 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Documentation.docc/Extensions/CasePathable.md: -------------------------------------------------------------------------------- 1 | # ``CasePathsCore/CasePathable`` 2 | 3 | ## Topics 4 | 5 | ### Deriving case key paths 6 | 7 | - ``Cases`` 8 | 9 | ### Extracting, replacing, and modifying values 10 | 11 | - ``subscript(case:)-3yqx3`` 12 | - ``subscript(case:)-2t4f8`` 13 | - ``modify(_:yield:fileID:filePath:line:column:)`` 14 | 15 | ### Case properties 16 | 17 | - ``is(_:)`` 18 | - ``subscript(dynamicMember:)-emck`` 19 | - ``subscript(dynamicMember:)-dm5y`` 20 | 21 | ### Iteration and reflection 22 | 23 | - ``CasePathIterable`` 24 | - ``CasePathReflectable`` 25 | 26 | ### Manual conformances 27 | 28 | - ``AllCasePaths`` 29 | - ``allCasePaths`` 30 | - ``AnyCasePath`` 31 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Documentation.docc/MigrationGuides.md: -------------------------------------------------------------------------------- 1 | # Migration guides 2 | 3 | Learn how to upgrade your application to the newest version of Case Paths. 4 | 5 | ## Overview 6 | 7 | Case Paths is under constant development, and we are always looking for ways to simplify the 8 | library, and make it more powerful. As such, we often need to deprecate certain APIs in favor of 9 | newer ones. We recommend people update their code as quickly as possible to the newest APIs, and 10 | these guides contain tips to do so. 11 | 12 | > Important: Before following any particular migration guide be sure you have followed all the 13 | > preceding migration guides. 14 | 15 | ## Topics 16 | 17 | - 18 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Documentation.docc/MigrationGuides/MigratingTo1.1.md: -------------------------------------------------------------------------------- 1 | # Migrating to 1.1 2 | 3 | Learn how to migrate existing case path code to utilize the new `@CasePathable` macro and 4 | ``CaseKeyPath``s. 5 | 6 | ## Overview 7 | 8 | CasePaths 1.1 introduces new APIs for deriving case paths that are safer, more ergonomic, more 9 | performant, and more powerful than the existing APIs. 10 | 11 | In past versions of the library, the primary way to derive a case path was via the form: 12 | 13 | ``` 14 | /<#enum name#>.<#case#> 15 | ``` 16 | 17 | It kind of looks like a key path with the `\` tilting the wrong way, but is actually an invocation 18 | of a `/` prefix operator with an `Enum.case` initializer. Given just this initializer, the function 19 | uses runtime reflection to produce a `CasePath` value. 20 | 21 | So given an enum: 22 | 23 | ```swift 24 | enum UserAction { 25 | case home(HomeAction) 26 | } 27 | ``` 28 | 29 | One can produce a case path: 30 | 31 | ```swift 32 | /UserAction.home 33 | ``` 34 | 35 | While the library has strived to optimize this reflection mechanism and work around bugs in the 36 | runtime, it now offers a much better solution that is free of reflection-based code. 37 | 38 | Deriving case paths is now a two-step process that is still mostly free of boilerplate: 39 | 40 | 1. You attach the `@CasePathable` macro to your enum: 41 | 42 | ```swift 43 | @CasePathable 44 | enum UserAction { 45 | case home(HomeAction) 46 | } 47 | ``` 48 | 49 | 2. You derive a case path using an actual key path expression: 50 | 51 | ```swift 52 | \UserAction.Cases.home 53 | ``` 54 | 55 | This key path expression returns a ``CaseKeyPath``, which is a special kind of key path for enums 56 | that can extract, modify, and embed the associated value of an enum case. 57 | 58 | ### Passing case key paths to APIs that take case paths 59 | 60 | While libraries that use case paths should be updated to take ``CaseKeyPath``s directly, and should 61 | deprecate APIs that take `CasePath`s (now ``AnyCasePath``s), you can continue to use these existing 62 | APIs by converting case key paths to type-erased case paths via ``AnyCasePath/init(_:)``: 63 | 64 | ```swift 65 | // Before: 66 | action: /UserAction.home 67 | 68 | // After: 69 | action: AnyCasePath(\.home) 70 | ``` 71 | 72 | And when a library begins to provide APIs that take case key paths, you can pass a key path 73 | expression directly: 74 | 75 | ```swift 76 | action: \.home 77 | ``` 78 | 79 | ### Working with case key paths 80 | 81 | If you maintain APIs that take `CasePath` (now ``AnyCasePath``) values, you should introduce new 82 | APIs that take ``CaseKeyPath``s instead. ``CaseKeyPath``s have all the functionality of 83 | ``AnyCasePath``s (and more), but you work with them more like regular key paths: 84 | 85 | #### Extracting associated values 86 | 87 | ```swift 88 | // Before: 89 | casePath.extract(from: root) 90 | 91 | // After: 92 | root[case: casePath] 93 | ``` 94 | 95 | #### Embedding associated values 96 | 97 | ```swift 98 | // Before: 99 | casePath.embed(value) 100 | 101 | // After: 102 | casePath(value) 103 | ``` 104 | 105 | Case key paths can also replace an enum's existing associated value via 106 | ``CasePathable/subscript(case:)-2t4f8``: 107 | 108 | ```swift 109 | root[case: casePath] = value 110 | ``` 111 | 112 | #### Modifying associated values 113 | 114 | ```swift 115 | // Before: 116 | casePath.modify(&root) { 117 | $0.count += 1 118 | } 119 | 120 | // After: 121 | root.modify(casePath) { 122 | $0.count += 1 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Internal/KeyPath+Sendable.swift: -------------------------------------------------------------------------------- 1 | #if compiler(>=6) 2 | public typealias _SendableKeyPath = any Sendable & KeyPath 3 | #else 4 | public typealias _SendableKeyPath = KeyPath 5 | #endif 6 | 7 | // NB: Dynamic member lookup does not currently support sendable key paths and even breaks 8 | // autocomplete. 9 | // 10 | // * https://github.com/swiftlang/swift/issues/77035 11 | // * https://github.com/swiftlang/swift/issues/77105 12 | extension _AppendKeyPath { 13 | @_transparent 14 | package func unsafeSendable() -> _SendableKeyPath 15 | where Self == KeyPath { 16 | #if compiler(>=6) 17 | unsafeBitCast(self, to: _SendableKeyPath.self) 18 | #else 19 | self 20 | #endif 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Internal/TypeName.swift: -------------------------------------------------------------------------------- 1 | // NB: This is adapted from Custom Dump and should ideally be kept in sync. 2 | package func typeName( 3 | _ type: Any.Type, 4 | qualified: Bool = true, 5 | genericsAbbreviated: Bool = false // NB: This defaults to `true` in Custom Dump 6 | ) -> String { 7 | var name = _typeName(type, qualified: qualified) 8 | .replacingOccurrences( 9 | of: #"\(unknown context at \$[[:xdigit:]]+\)\."#, 10 | with: "", 11 | options: .regularExpression 12 | ) 13 | for _ in 1...10 { // NB: Only handle so much nesting 14 | let abbreviated = 15 | name 16 | .replacingOccurrences( 17 | of: #"\bSwift.Optional<([^><]+)>"#, 18 | with: "$1?", 19 | options: .regularExpression 20 | ) 21 | .replacingOccurrences( 22 | of: #"\bSwift.Array<([^><]+)>"#, 23 | with: "[$1]", 24 | options: .regularExpression 25 | ) 26 | .replacingOccurrences( 27 | of: #"\bSwift.Dictionary<([^,<]+), ([^><]+)>"#, 28 | with: "[$1: $2]", 29 | options: .regularExpression 30 | ) 31 | if abbreviated == name { break } 32 | name = abbreviated 33 | } 34 | name = name.replacingOccurrences( 35 | of: #"\w+\.([\w.]+)"#, 36 | with: "$1", 37 | options: .regularExpression 38 | ) 39 | if genericsAbbreviated { 40 | name = name.replacingOccurrences( 41 | of: #"<.+>"#, 42 | with: "", 43 | options: .regularExpression 44 | ) 45 | } 46 | return name 47 | } 48 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Internal/UncheckedSendable.swift: -------------------------------------------------------------------------------- 1 | @propertyWrapper 2 | package struct UncheckedSendable: @unchecked Sendable { 3 | package var wrappedValue: Value 4 | package init(wrappedValue value: Value) { 5 | self.wrappedValue = value 6 | } 7 | package var projectedValue: Self { self } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Never+CasePathable.swift: -------------------------------------------------------------------------------- 1 | extension Never: CasePathable, CasePathIterable { 2 | public struct AllCasePaths: CasePathReflectable, Sendable { 3 | public subscript(root: Never) -> PartialCaseKeyPath { 4 | \.never 5 | } 6 | } 7 | 8 | public static var allCasePaths: AllCasePaths { 9 | AllCasePaths() 10 | } 11 | } 12 | 13 | extension Case where Value: CasePathable { 14 | /// A case path that can never embed or extract a value. 15 | /// 16 | /// This property can chain any case path into a `Never` value, which, as an uninhabited type, 17 | /// cannot be embedded nor extracted from an enum. 18 | public var never: Case { 19 | @Sendable func absurd(_: Never) -> T {} 20 | return Case(embed: absurd, extract: { (_: Value) in nil }) 21 | } 22 | } 23 | 24 | extension Case { 25 | @available(*, deprecated, message: "This enum must be '@CasePathable' to enable key path syntax") 26 | public var never: Case { 27 | @Sendable func absurd(_: Never) -> T {} 28 | return Case(embed: absurd, extract: { (_: Value) in nil }) 29 | } 30 | } 31 | 32 | extension Never.AllCasePaths: Sequence { 33 | public func makeIterator() -> some IteratorProtocol> { 34 | [].makeIterator() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Optional+CasePathable.swift: -------------------------------------------------------------------------------- 1 | extension Optional: CasePathable, CasePathIterable { 2 | @dynamicMemberLookup 3 | public struct AllCasePaths: CasePathReflectable, Sendable { 4 | public subscript(root: Optional) -> PartialCaseKeyPath { 5 | switch root { 6 | case .none: return \.none 7 | case .some: return \.some 8 | } 9 | } 10 | 11 | /// A case path to the absence of a value. 12 | public var none: AnyCasePath { 13 | AnyCasePath( 14 | embed: { .none }, 15 | extract: { 16 | guard case .none = $0 else { return nil } 17 | return () 18 | } 19 | ) 20 | } 21 | 22 | /// A case path to the presence of a value. 23 | public var some: AnyCasePath { 24 | AnyCasePath( 25 | embed: { .some($0) }, 26 | extract: { 27 | guard case let .some(value) = $0 else { return nil } 28 | return value 29 | } 30 | ) 31 | } 32 | 33 | /// A case path to an optional-chained value. 34 | @_disfavoredOverload 35 | public subscript( 36 | dynamicMember keyPath: KeyPath> 37 | ) -> AnyCasePath 38 | where Wrapped: CasePathable { 39 | let casePath = Wrapped.allCasePaths[keyPath: keyPath] 40 | return AnyCasePath( 41 | embed: { $0.map(casePath.embed) }, 42 | extract: { 43 | guard case let .some(wrapped) = $0, let member = casePath.extract(from: wrapped) 44 | else { return .none } 45 | return member 46 | } 47 | ) 48 | } 49 | } 50 | 51 | public static var allCasePaths: AllCasePaths { 52 | AllCasePaths() 53 | } 54 | } 55 | 56 | extension Case { 57 | /// A case path to the presence of a nested value. 58 | /// 59 | /// This subscript can chain into an optional's wrapped value without explicitly specifying each 60 | /// `some` component. 61 | @_disfavoredOverload 62 | public subscript( 63 | dynamicMember keyPath: KeyPath> 64 | ) -> Case 65 | where Value: CasePathable { 66 | self[dynamicMember: keyPath].some 67 | } 68 | } 69 | 70 | extension Optional.AllCasePaths: Sequence { 71 | public func makeIterator() -> some IteratorProtocol> { 72 | [\.none, \.some].makeIterator() 73 | } 74 | } 75 | 76 | extension Optional where Wrapped: CasePathable { 77 | @_disfavoredOverload 78 | @_documentation(visibility: internal) 79 | public func `is`(_ keyPath: PartialCaseKeyPath) -> Bool { 80 | self?[case: keyPath] != nil 81 | } 82 | 83 | @_disfavoredOverload 84 | @_documentation(visibility: internal) 85 | public mutating func modify( 86 | _ keyPath: CaseKeyPath, 87 | yield: (inout Value) -> Void, 88 | fileID: StaticString = #fileID, 89 | filePath: StaticString = #filePath, 90 | line: UInt = #line, 91 | column: UInt = #column 92 | ) { 93 | modify( 94 | (\Cases.some).appending(path: keyPath), 95 | yield: yield, 96 | fileID: fileID, 97 | filePath: filePath, 98 | line: line, 99 | column: column 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/CasePathsCore/Result+CasePathable.swift: -------------------------------------------------------------------------------- 1 | extension Result: CasePathable, CasePathIterable { 2 | public struct AllCasePaths: CasePathReflectable, Sendable { 3 | public subscript(root: Result) -> PartialCaseKeyPath { 4 | switch root { 5 | case .success: return \.success 6 | case .failure: return \.failure 7 | } 8 | } 9 | 10 | /// A success case path, for embedding or extracting a `Success` value. 11 | public var success: AnyCasePath { 12 | AnyCasePath( 13 | embed: { .success($0) }, 14 | extract: { 15 | guard case let .success(value) = $0 else { return nil } 16 | return value 17 | } 18 | ) 19 | } 20 | 21 | /// A failure case path, for embedding or extracting a `Failure` value. 22 | public var failure: AnyCasePath { 23 | AnyCasePath( 24 | embed: { .failure($0) }, 25 | extract: { 26 | guard case let .failure(value) = $0 else { return nil } 27 | return value 28 | } 29 | ) 30 | } 31 | } 32 | 33 | public static var allCasePaths: AllCasePaths { 34 | AllCasePaths() 35 | } 36 | } 37 | 38 | extension Result.AllCasePaths: Sequence { 39 | public func makeIterator() -> some IteratorProtocol> { 40 | [\.success, \.failure].makeIterator() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/CasePathsMacros/CasePathableMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftDiagnostics 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | public struct CasePathableMacro { 7 | static let moduleName = "CasePaths" 8 | static let casePathTypeName = "AnyCasePath" 9 | } 10 | 11 | extension CasePathableMacro: ExtensionMacro { 12 | public static func expansion( 13 | of node: AttributeSyntax, 14 | attachedTo declaration: D, 15 | providingExtensionsOf type: T, 16 | conformingTo protocols: [TypeSyntax], 17 | in context: C 18 | ) throws -> [ExtensionDeclSyntax] { 19 | // if protocols.isEmpty { 20 | // return [] 21 | // } 22 | guard let enumDecl = declaration.as(EnumDeclSyntax.self) 23 | else { 24 | // TODO: Diagnostic? 25 | return [] 26 | } 27 | var conformances: [String] = [] 28 | if let inheritanceClause = enumDecl.inheritanceClause { 29 | for type in ["CasePathable", "CasePathIterable"] { 30 | if !inheritanceClause.inheritedTypes.contains(where: { 31 | [type, type.qualified].contains($0.type.trimmedDescription) 32 | }) { 33 | conformances.append("\(moduleName).\(type)") 34 | } 35 | } 36 | } else { 37 | conformances = ["CasePathable", "CasePathIterable"].qualified 38 | } 39 | guard !conformances.isEmpty else { return [] } 40 | return [ 41 | DeclSyntax( 42 | """ 43 | \(declaration.attributes.availability)extension \(type.trimmed): \ 44 | \(raw: conformances.joined(separator: ", ")) {} 45 | """ 46 | ) 47 | .cast(ExtensionDeclSyntax.self) 48 | ] 49 | } 50 | } 51 | 52 | extension CasePathableMacro: MemberMacro { 53 | public static func expansion< 54 | Declaration: DeclGroupSyntax, Context: MacroExpansionContext 55 | >( 56 | of node: AttributeSyntax, 57 | providingMembersOf declaration: Declaration, 58 | in context: Context 59 | ) throws -> [DeclSyntax] { 60 | guard let enumDecl = declaration.as(EnumDeclSyntax.self) 61 | else { 62 | throw DiagnosticsError( 63 | diagnostics: [ 64 | CasePathableMacroDiagnostic 65 | .notAnEnum(declaration) 66 | .diagnose(at: declaration.keyword) 67 | ] 68 | ) 69 | } 70 | let enumName = enumDecl.name.trimmed 71 | 72 | let enumCaseDecls = enumDecl.memberBlock.members 73 | .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } 74 | 75 | var seenCaseNames: Set = [] 76 | for enumCaseDecl in enumCaseDecls { 77 | let name = enumCaseDecl.name.text 78 | if seenCaseNames.contains(name) { 79 | throw DiagnosticsError( 80 | diagnostics: [ 81 | CasePathableMacroDiagnostic.overloadedCaseName(name).diagnose( 82 | at: Syntax(enumCaseDecl.name)) 83 | ] 84 | ) 85 | } 86 | seenCaseNames.insert(name) 87 | } 88 | 89 | let selfRewriter = SelfRewriter(selfEquivalent: enumName) 90 | let memberBlock = selfRewriter.rewrite(enumDecl.memberBlock).cast(MemberBlockSyntax.self) 91 | let rootSubscriptCases = generateCases(from: memberBlock.members, enumName: enumName) { 92 | "if root.is(\\.\(raw: $0.name.text)) { return \\.\(raw: $0.name.text) }" 93 | } 94 | let elementRewriter = ElementRewriter() 95 | let casePaths = generateDeclSyntax(from: memberBlock.members, enumName: enumName).map { 96 | elementRewriter.rewrite($0) 97 | } 98 | let allCases = generateCases(from: memberBlock.members, enumName: enumName) { 99 | "allCasePaths.append(\\.\(raw: $0.name.text))" 100 | } 101 | 102 | let subscriptReturn = allCases.isEmpty ? #"\.never"# : #"return \.never"# 103 | 104 | var decls: [DeclSyntax] = [ 105 | """ 106 | public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { 107 | public subscript(root: \(enumName)) -> CasePaths.PartialCaseKeyPath<\(enumName)> { 108 | \(raw: rootSubscriptCases.map { "\($0.description)\n" }.joined())\(raw: subscriptReturn) 109 | } 110 | \(raw: casePaths.map(\.description).joined(separator: "\n")) 111 | public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath<\(enumName)>]> { 112 | \(raw: allCases.isEmpty ? "let" : "var") allCasePaths: \ 113 | [CasePaths.PartialCaseKeyPath<\(enumName)>] = []\ 114 | \(raw: allCases.map { "\n\($0.description)" }.joined()) 115 | return allCasePaths.makeIterator() 116 | } 117 | } 118 | public static var allCasePaths: AllCasePaths { AllCasePaths() } 119 | """ 120 | ] 121 | 122 | if elementRewriter.didRewriteElement { 123 | decls.append("public typealias _$Element = Element") 124 | } 125 | 126 | return decls 127 | } 128 | 129 | static func generateCases( 130 | from elements: MemberBlockItemListSyntax, 131 | enumName: TokenSyntax, 132 | body: (EnumCaseElementSyntax) -> DeclSyntax 133 | ) -> [DeclSyntax] { 134 | elements.flatMap { 135 | if let decl = $0.decl.as(EnumCaseDeclSyntax.self) { 136 | return decl.elements.map(body) 137 | } 138 | if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) { 139 | let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [DeclSyntax] in 140 | guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) else { 141 | return [] 142 | } 143 | let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")" 144 | return ["\(raw: title)"] 145 | + generateCases(from: elements, enumName: enumName, body: body) 146 | } 147 | return ifClauses + ["#endif"] 148 | } 149 | return [] 150 | } 151 | } 152 | 153 | static func generateDeclSyntax( 154 | from elements: MemberBlockItemListSyntax, 155 | enumName: TokenSyntax 156 | ) -> [DeclSyntax] { 157 | elements.flatMap { 158 | if let decl = $0.decl.as(EnumCaseDeclSyntax.self) { 159 | return generateDeclSyntax(from: decl, enumName: enumName) 160 | } 161 | if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) { 162 | let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [DeclSyntax] in 163 | guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) else { 164 | return [] 165 | } 166 | let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")" 167 | return ["\(raw: title)"] 168 | + generateDeclSyntax(from: elements, enumName: enumName) 169 | } 170 | return ifClauses + ["#endif"] 171 | } 172 | return [] 173 | } 174 | } 175 | 176 | static func generateDeclSyntax( 177 | from decl: EnumCaseDeclSyntax, 178 | enumName: TokenSyntax 179 | ) -> [DeclSyntax] { 180 | decl.elements.map { 181 | let caseName = $0.name.trimmed 182 | let associatedValueName = $0.trimmedTypeDescription 183 | let hasPayload = $0.parameterClause.map { !$0.parameters.isEmpty } ?? false 184 | let embed: DeclSyntax = hasPayload ? "\(enumName).\(caseName)" : "{ \(enumName).\(caseName) }" 185 | let bindingNames: String 186 | let returnName: String 187 | if hasPayload, let associatedValue = $0.parameterClause { 188 | let parameterNames = (0.. Bool in lhs < rhs }) 204 | ?? 0 205 | let leadingTrivia = 206 | leadingTriviaLines 207 | .map { String($0.dropFirst(indent)) } 208 | .joined(separator: "\n") 209 | .trimmingSuffix(while: { $0.isWhitespace && !$0.isNewline }) 210 | return """ 211 | \(raw: leadingTrivia)public var \(caseName): \ 212 | \(raw: casePathTypeName.qualified)<\(enumName), \(raw: associatedValueName)> { 213 | ._$embed(\(embed)) { 214 | guard case\(raw: hasPayload ? " let" : "").\(caseName)\(raw: bindingNames) = $0 else { \ 215 | return nil \ 216 | } 217 | return \(raw: returnName) 218 | } 219 | } 220 | """ 221 | } 222 | } 223 | } 224 | 225 | enum CasePathableMacroDiagnostic { 226 | case notAnEnum(DeclGroupSyntax) 227 | case overloadedCaseName(String) 228 | } 229 | 230 | extension CasePathableMacroDiagnostic: DiagnosticMessage { 231 | var message: String { 232 | switch self { 233 | case let .notAnEnum(decl): 234 | return """ 235 | '@CasePathable' cannot be applied to\ 236 | \(decl.keywordDescription.map { " \($0)" } ?? "") type\ 237 | \(decl.nameDescription.map { " '\($0)'" } ?? "") 238 | """ 239 | case let .overloadedCaseName(name): 240 | return """ 241 | '@CasePathable' cannot be applied to overloaded case name '\(name)' 242 | """ 243 | } 244 | } 245 | 246 | var diagnosticID: MessageID { 247 | switch self { 248 | case .notAnEnum: 249 | return MessageID(domain: "MetaEnumDiagnostic", id: "notAnEnum") 250 | case .overloadedCaseName: 251 | return MessageID(domain: "MetaEnumDiagnostic", id: "overloadedCaseName") 252 | } 253 | } 254 | 255 | var severity: DiagnosticSeverity { 256 | switch self { 257 | case .notAnEnum: 258 | return .error 259 | case .overloadedCaseName: 260 | return .error 261 | } 262 | } 263 | 264 | func diagnose(at node: Syntax) -> Diagnostic { 265 | Diagnostic(node: node, message: self) 266 | } 267 | } 268 | 269 | extension AttributeListSyntax { 270 | var availability: AttributeListSyntax? { 271 | var elements = [AttributeListSyntax.Element]() 272 | for element in self { 273 | if let availability = element.availability { 274 | elements.append(availability) 275 | } 276 | } 277 | if elements.isEmpty { 278 | return nil 279 | } 280 | return AttributeListSyntax(elements) 281 | } 282 | } 283 | 284 | extension AttributeListSyntax.Element { 285 | var availability: AttributeListSyntax.Element? { 286 | switch self { 287 | case .attribute(let attribute): 288 | if let availability = attribute.availability { 289 | return .attribute(availability) 290 | } 291 | case .ifConfigDecl(let ifConfig): 292 | if let availability = ifConfig.availability { 293 | return .ifConfigDecl(availability) 294 | } 295 | @unknown default: return nil 296 | } 297 | return nil 298 | } 299 | } 300 | 301 | extension AttributeSyntax { 302 | var availability: AttributeSyntax? { 303 | if attributeName.identifier == "available" { 304 | return self 305 | } else { 306 | return nil 307 | } 308 | } 309 | } 310 | 311 | extension IfConfigClauseSyntax { 312 | var availability: IfConfigClauseSyntax? { 313 | if let availability = elements?.availability { 314 | return with(\.elements, availability) 315 | } else { 316 | return nil 317 | } 318 | } 319 | 320 | var clonedAsIf: IfConfigClauseSyntax { 321 | detached.with(\.poundKeyword, .poundIfToken()) 322 | } 323 | } 324 | 325 | extension IfConfigClauseSyntax.Elements { 326 | var availability: IfConfigClauseSyntax.Elements? { 327 | switch self { 328 | case .attributes(let attributes): 329 | if let availability = attributes.availability { 330 | return .attributes(availability) 331 | } else { 332 | return nil 333 | } 334 | default: 335 | return nil 336 | } 337 | } 338 | } 339 | 340 | extension IfConfigDeclSyntax { 341 | var availability: IfConfigDeclSyntax? { 342 | var elements = [IfConfigClauseListSyntax.Element]() 343 | for clause in clauses { 344 | if let availability = clause.availability { 345 | if elements.isEmpty { 346 | elements.append(availability.clonedAsIf) 347 | } else { 348 | elements.append(availability) 349 | } 350 | } 351 | } 352 | if elements.isEmpty { 353 | return nil 354 | } else { 355 | return with(\.clauses, IfConfigClauseListSyntax(elements)) 356 | } 357 | } 358 | } 359 | 360 | extension DeclGroupSyntax { 361 | var keyword: Syntax { 362 | switch self { 363 | case let syntax as ActorDeclSyntax: 364 | return Syntax(syntax.actorKeyword) 365 | case let syntax as ClassDeclSyntax: 366 | return Syntax(syntax.classKeyword) 367 | case let syntax as ExtensionDeclSyntax: 368 | return Syntax(syntax.extensionKeyword) 369 | case let syntax as ProtocolDeclSyntax: 370 | return Syntax(syntax.protocolKeyword) 371 | case let syntax as StructDeclSyntax: 372 | return Syntax(syntax.structKeyword) 373 | case let syntax as EnumDeclSyntax: 374 | return Syntax(syntax.enumKeyword) 375 | default: 376 | return Syntax(self) 377 | } 378 | } 379 | 380 | var keywordDescription: String? { 381 | switch self { 382 | case let syntax as ActorDeclSyntax: 383 | return syntax.actorKeyword.trimmedDescription 384 | case let syntax as ClassDeclSyntax: 385 | return syntax.classKeyword.trimmedDescription 386 | case let syntax as ExtensionDeclSyntax: 387 | return syntax.extensionKeyword.trimmedDescription 388 | case let syntax as ProtocolDeclSyntax: 389 | return syntax.protocolKeyword.trimmedDescription 390 | case let syntax as StructDeclSyntax: 391 | return syntax.structKeyword.trimmedDescription 392 | case let syntax as EnumDeclSyntax: 393 | return syntax.enumKeyword.trimmedDescription 394 | default: 395 | return nil 396 | } 397 | } 398 | 399 | var nameDescription: String? { 400 | switch self { 401 | case let syntax as ActorDeclSyntax: 402 | return syntax.name.trimmedDescription 403 | case let syntax as ClassDeclSyntax: 404 | return syntax.name.trimmedDescription 405 | case let syntax as ExtensionDeclSyntax: 406 | return syntax.extendedType.trimmedDescription 407 | case let syntax as ProtocolDeclSyntax: 408 | return syntax.name.trimmedDescription 409 | case let syntax as StructDeclSyntax: 410 | return syntax.name.trimmedDescription 411 | case let syntax as EnumDeclSyntax: 412 | return syntax.name.trimmedDescription 413 | default: 414 | return nil 415 | } 416 | } 417 | } 418 | 419 | extension EnumCaseElementListSyntax.Element { 420 | var trimmedTypeDescription: String { 421 | if var associatedValue = self.parameterClause, !associatedValue.parameters.isEmpty { 422 | if associatedValue.parameters.count == 1, 423 | let type = associatedValue.parameters.first?.type.trimmed 424 | { 425 | return type.is(SomeOrAnyTypeSyntax.self) 426 | ? "(\(type))" 427 | : "\(type)" 428 | } else { 429 | for index in associatedValue.parameters.indices { 430 | associatedValue.parameters[index].type.trailingTrivia = "" 431 | associatedValue.parameters[index].defaultValue = nil 432 | if associatedValue.parameters[index].firstName?.tokenKind == .wildcard { 433 | associatedValue.parameters[index].colon = nil 434 | associatedValue.parameters[index].firstName = nil 435 | associatedValue.parameters[index].secondName = nil 436 | } 437 | } 438 | return "(\(associatedValue.parameters.trimmed))" 439 | } 440 | } else { 441 | return "Void" 442 | } 443 | } 444 | } 445 | 446 | extension SyntaxStringInterpolation { 447 | mutating func appendInterpolation(_ node: Node?) { 448 | if let node { 449 | self.appendInterpolation(node) 450 | } 451 | } 452 | } 453 | 454 | extension TypeSyntax { 455 | var identifier: String? { 456 | for token in tokens(viewMode: .all) { 457 | switch token.tokenKind { 458 | case .identifier(let identifier): 459 | return identifier 460 | default: 461 | break 462 | } 463 | } 464 | return nil 465 | } 466 | } 467 | 468 | final class SelfRewriter: SyntaxRewriter { 469 | let selfEquivalent: TokenSyntax 470 | 471 | init(selfEquivalent: TokenSyntax) { 472 | self.selfEquivalent = selfEquivalent 473 | } 474 | 475 | override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax { 476 | guard node.name.text == "Self" 477 | else { return super.visit(node) } 478 | return super.visit(node.with(\.name, self.selfEquivalent)) 479 | } 480 | } 481 | 482 | final class ElementRewriter: SyntaxRewriter { 483 | var didRewriteElement = false 484 | 485 | override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax { 486 | guard node.name.text == "Element" 487 | else { return super.visit(node) } 488 | didRewriteElement = true 489 | return super.visit(node.with(\.name, "_$Element")) 490 | } 491 | } 492 | 493 | extension [String] { 494 | fileprivate var qualified: [String] { 495 | map(\.qualified) 496 | } 497 | } 498 | 499 | extension String { 500 | fileprivate var qualified: String { 501 | "\(CasePathableMacro.moduleName).\(self)" 502 | } 503 | } 504 | 505 | extension StringProtocol { 506 | @inline(__always) 507 | func trimmingSuffix(while condition: (Element) throws -> Bool) rethrows -> Self.SubSequence { 508 | var view = self[...] 509 | 510 | while let character = view.last, try condition(character) { 511 | view = view.dropLast() 512 | } 513 | 514 | return view 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /Sources/CasePathsMacros/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct CasePathsPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | CasePathableMacro.self 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/CasePathableTests.swift: -------------------------------------------------------------------------------- 1 | import CasePaths 2 | import XCTest 3 | 4 | final class CasePathableTests: XCTestCase { 5 | func testModify() { 6 | struct MyError: Equatable, Error {} 7 | var result = Result.success(1) 8 | result.modify(\.success) { $0 += 1 } 9 | XCTAssertEqual(result, .success(2)) 10 | } 11 | 12 | #if DEBUG && !os(Linux) && !os(Windows) && !os(WASI) && !os(Android) 13 | func testModifyWrongCase() { 14 | guard ProcessInfo.processInfo.environment["CI"] == nil else { return } 15 | var response = Result.failure(MyError()) 16 | XCTExpectFailure { 17 | response.modify(\.success) { $0 += 1 } 18 | } 19 | XCTAssertEqual(response, .failure(MyError())) 20 | } 21 | #endif 22 | 23 | struct MyError: Equatable, Error {} 24 | } 25 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/CasePathsTests.swift: -------------------------------------------------------------------------------- 1 | import CasePaths 2 | import XCTest 3 | 4 | final class CasePathsTests: XCTestCase { 5 | func testOptional() { 6 | XCTAssertEqual(Int?.some(42)[case: \.some], 42) 7 | XCTAssertNil(Int?.none[case: \.some]) 8 | XCTAssertNil(Int?.some(42)[case: \.none]) 9 | XCTAssertNotNil(Int?.none[case: \.none]) 10 | XCTAssertEqual((\Int?.Cases.some)(42), 42) 11 | XCTAssertEqual((\Int?.Cases.none)(), nil) 12 | XCTAssertEqual(Fizz.buzz(.fizzBuzz(.int(42)))[case: \.buzz.fizzBuzz.int], 42) 13 | let buzzPath1: CaseKeyPath = \Fizz.Cases.buzz 14 | let buzzPath2: CaseKeyPath = \Fizz.Cases.buzz 15 | XCTAssertEqual(buzzPath1, \.buzz) 16 | XCTAssertEqual(buzzPath2, \.buzz) 17 | let buzzPath3 = \Fizz.Cases.buzz 18 | XCTAssertEqual(buzzPath1, buzzPath3) 19 | XCTAssertNotEqual(buzzPath2, buzzPath3) 20 | XCTAssertEqual(ifLet(state: \Fizz.buzz, action: \Fizz.Cases.buzz), 42) 21 | XCTAssertEqual(ifLet(state: \Fizz.buzz, action: \Foo.Cases.bar), nil) 22 | let fizzBuzzPath1: CaseKeyPath = \Fizz.Cases.buzz.fizzBuzz.int 23 | let fizzBuzzPath2: CaseKeyPath = \Fizz.Cases.buzz.fizzBuzz.int 24 | let fizzBuzzPath3 = \Fizz.Cases.buzz.fizzBuzz.int 25 | XCTAssertNotEqual(fizzBuzzPath1, fizzBuzzPath3) 26 | XCTAssertEqual(fizzBuzzPath2, fizzBuzzPath3) 27 | XCTAssertEqual(Optional.allCasePaths[Int?.some(42)], \.some) 28 | XCTAssertNotEqual(Optional.allCasePaths[Int?.some(42)], \.none) 29 | XCTAssertEqual(Optional.allCasePaths[Int?.none], \.none) 30 | XCTAssertNotEqual(Optional.allCasePaths[Int?.none], \.some) 31 | } 32 | 33 | func testResult() { 34 | struct SomeError: Error, Equatable {} 35 | XCTAssertEqual(Result.success(42)[case: \.success], 42) 36 | XCTAssertNil(Result.failure(SomeError())[case: \.success]) 37 | XCTAssertNil(Result.success(42)[case: \.failure]) 38 | XCTAssertNotNil(Result.failure(SomeError())[case: \.failure]) 39 | XCTAssertEqual((\Result.Cases.success)(42), .success(42)) 40 | XCTAssertEqual((\Result.Cases.failure)(SomeError()), .failure(SomeError())) 41 | XCTAssertEqual(Result.allCasePaths[Result.success(42)], \.success) 42 | XCTAssertNotEqual(Result.allCasePaths[Result.success(42)], \.failure) 43 | XCTAssertEqual(Result.allCasePaths[Result.failure(SomeError())], \.failure) 44 | XCTAssertNotEqual(Result.allCasePaths[Result.failure(SomeError())], \.success) 45 | } 46 | 47 | func testSelfCaseKeyPathCallAsFunction() { 48 | enum Loadable: Equatable { 49 | case isLoading(progress: Float) 50 | case isLoaded 51 | } 52 | 53 | var loadable = Loadable.isLoading(progress: 0) 54 | loadable = (\.self as CaseKeyPath)(.isLoading(progress: 0.5)) 55 | XCTAssertEqual(loadable, .isLoading(progress: 0.5)) 56 | loadable = (\.self as CaseKeyPath)(.isLoaded) 57 | XCTAssertEqual(loadable, .isLoaded) 58 | } 59 | 60 | func testCaseKeyPaths() { 61 | var foo: Foo = .bar(.int(1)) 62 | 63 | XCTAssertEqual(foo.bar, .int(1)) 64 | // NB: Due to a Swift bug, this is only possible to do outside the library: 65 | // XCTAssertEqual(foo.bar?.int, 1) 66 | 67 | XCTAssertEqual(foo[keyPath: \.bar], .int(1)) 68 | XCTAssertEqual(foo[keyPath: \.bar?.int], 1) 69 | 70 | XCTAssertEqual(foo[case: \.bar], .int(1)) 71 | XCTAssertEqual(foo[case: \.bar.int], 1) 72 | 73 | foo[case: \.bar] = .int(42) 74 | 75 | XCTAssertEqual(foo, .bar(.int(42))) 76 | 77 | foo[case: \.baz] = .string("Forty-two") 78 | 79 | XCTAssertEqual(foo, .bar(.int(42))) 80 | 81 | foo[case: \.bar.int] = 1792 82 | 83 | XCTAssertEqual(foo, .bar(.int(1792))) 84 | 85 | foo[case: \.baz.string] = "Seventeen hundred and ninety-two" 86 | 87 | XCTAssertEqual(foo, .bar(.int(1792))) 88 | 89 | foo[case: \.bar] = .int(42) 90 | 91 | XCTAssertEqual((\Foo.Cases.self)(.bar(.int(1))), .bar(.int(1))) 92 | XCTAssertEqual((\Foo.Cases.bar)(.int(1)), .bar(.int(1))) 93 | XCTAssertEqual((\Foo.Cases.bar.int)(1), .bar(.int(1))) 94 | XCTAssertEqual((\Foo.Cases.fizzBuzz)(), .fizzBuzz) 95 | 96 | XCTAssertEqual(Foo.allCasePaths[.bar(.int(1))], \.bar) 97 | XCTAssertEqual(Foo.allCasePaths[.baz(.string(""))], \.baz) 98 | XCTAssertEqual(Foo.allCasePaths[.fizzBuzz], \.fizzBuzz) 99 | XCTAssertEqual(Foo.allCasePaths[.foo(nil)], \.foo) 100 | 101 | XCTAssertEqual( 102 | Array(Foo.allCasePaths), 103 | [ 104 | \.bar, 105 | \.baz, 106 | \.fizzBuzz, 107 | \.blob, 108 | \.foo, 109 | ] 110 | ) 111 | } 112 | 113 | func testCasePathableModify() { 114 | var foo = Foo.bar(.int(21)) 115 | foo.modify(\.bar.int) { $0 *= 2 } 116 | XCTAssertEqual(foo, .bar(.int(42))) 117 | } 118 | 119 | #if DEBUG && !os(Linux) && !os(Windows) && !os(WASI) && !os(Android) 120 | func testCasePathableModify_Failure() { 121 | guard ProcessInfo.processInfo.environment["CI"] == nil else { return } 122 | var foo = Foo.bar(.int(21)) 123 | XCTExpectFailure { 124 | foo.modify(\.baz.string) { $0.append("!") } 125 | } 126 | XCTAssertEqual(foo, .bar(.int(21))) 127 | } 128 | #endif 129 | 130 | func testAppend() { 131 | let fooToBar = \Foo.Cases.bar 132 | let barToInt = \Bar.Cases.int 133 | let fooToInt = fooToBar.appending(path: barToInt) 134 | 135 | XCTAssertEqual(Foo.bar(.int(42))[case: fooToInt], 42) 136 | XCTAssertEqual(Foo.baz(.string("Hello"))[case: fooToInt], nil) 137 | XCTAssertEqual(Foo.bar(.int(123)), fooToInt(123)) 138 | } 139 | 140 | func testMatch() { 141 | switch Foo.bar(.int(42)) { 142 | case \.bar.int: 143 | break 144 | default: 145 | XCTFail() 146 | } 147 | 148 | switch Foo.bar(.int(42)) { 149 | case \.bar: 150 | break 151 | default: 152 | XCTFail() 153 | } 154 | 155 | XCTAssertTrue(Foo.bar(.int(42)).is(\.bar)) 156 | XCTAssertTrue(Foo.bar(.int(42)).is(\.bar.int)) 157 | XCTAssertFalse(Foo.bar(.int(42)).is(\.baz)) 158 | XCTAssertFalse(Foo.bar(.int(42)).is(\.baz.string)) 159 | XCTAssertFalse(Foo.bar(.int(42)).is(\.blob)) 160 | XCTAssertFalse(Foo.bar(.int(42)).is(\.fizzBuzz)) 161 | XCTAssertTrue(Foo.foo(nil).is(\.foo)) 162 | XCTAssertTrue(Foo.foo(nil).is(\.foo.none)) 163 | XCTAssertTrue(Foo.foo("").is(\.foo)) 164 | XCTAssertFalse(Foo.foo(nil).is(\.bar)) 165 | } 166 | 167 | func testPartialCaseKeyPath() { 168 | let partialPath = \Foo.Cases.bar as PartialCaseKeyPath 169 | XCTAssertEqual(.bar(.int(42)), partialPath(Bar.int(42))) 170 | XCTAssertNil(partialPath(42)) 171 | 172 | XCTAssertEqual(.int(42), Foo.bar(.int(42))[case: partialPath] as? Bar) 173 | XCTAssertNil(Foo.baz(.string("Hello"))[case: partialPath]) 174 | } 175 | 176 | func testExistentials() { 177 | let caseA: PartialCaseKeyPath = \.a 178 | let caseB: PartialCaseKeyPath = \.b 179 | 180 | let a = A.a("Hello") 181 | guard let valueA = a[case: caseA] else { return XCTFail() } 182 | guard let b = caseB(valueA) else { return XCTFail() } 183 | XCTAssertEqual(b, .b("Hello")) 184 | } 185 | 186 | func testExistentials_Optional() { 187 | let foo: PartialCaseKeyPath = \.foo 188 | XCTAssertNotNil(foo(String?.none as Any)) 189 | XCTAssertNotNil(foo(String?.some("Blob") as Any)) 190 | XCTAssertNotNil(foo("Blob")) 191 | } 192 | 193 | func testIs_Optional() { 194 | XCTAssertTrue(Optional(Foo.fizzBuzz).is(\.fizzBuzz)) 195 | XCTAssertFalse(Optional(Foo.fizzBuzz).is(\.bar)) 196 | XCTAssertFalse(Optional(Foo.fizzBuzz).is(\.baz)) 197 | XCTAssertFalse(Optional(Foo.fizzBuzz).is(\.blob)) 198 | XCTAssertFalse(Optional(Foo.fizzBuzz).is(\.foo)) 199 | } 200 | } 201 | 202 | @CasePathable 203 | enum A: Equatable { 204 | case a(String) 205 | } 206 | 207 | @CasePathable 208 | enum B: Equatable { 209 | case b(String) 210 | } 211 | 212 | @CasePathable @dynamicMemberLookup enum Foo: Equatable { 213 | case bar(Bar) 214 | case baz(Baz) 215 | case fizzBuzz 216 | case blob(Blob) 217 | case foo(String?) 218 | } 219 | @CasePathable @dynamicMemberLookup enum Bar: Equatable { 220 | case int(Int) 221 | } 222 | @CasePathable @dynamicMemberLookup enum Baz: Equatable { 223 | case string(String) 224 | } 225 | @CasePathable enum Blob: Equatable { 226 | } 227 | @CasePathable @dynamicMemberLookup enum Fizz: Equatable { 228 | case buzz(Buzz?) 229 | } 230 | @CasePathable @dynamicMemberLookup enum Buzz: Equatable { 231 | case fizzBuzz(FizzBuzz?) 232 | } 233 | @CasePathable @dynamicMemberLookup enum FizzBuzz: Equatable { 234 | case int(Int) 235 | } 236 | 237 | func ifLet(state: KeyPath, action: CaseKeyPath) -> Int? { 42 } 238 | @_disfavoredOverload 239 | func ifLet(state: KeyPath, action: CaseKeyPath) -> Int? { nil } 240 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/CaseSetTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Testing) && swift(>=6) 2 | import CasePaths 3 | import Testing 4 | 5 | @dynamicMemberLookup 6 | public struct CaseSet { 7 | private var storage: [PartialCaseKeyPath: Element] = [:] 8 | 9 | public init() {} 10 | 11 | public init(_ elements: some Collection) 12 | where Element.AllCasePaths: CasePathReflectable { 13 | self.storage = [PartialCaseKeyPath & Sendable: Element]( 14 | uniqueKeysWithValues: elements.map { (Element.allCasePaths[$0], $0) } 15 | ) 16 | } 17 | 18 | public subscript( 19 | dynamicMember keyPath: CaseKeyPath // & Sendable 20 | ) -> Member? { 21 | get { storage[keyPath].flatMap { $0[case: keyPath] } } 22 | set { storage[keyPath] = newValue.map(keyPath.callAsFunction) } 23 | } 24 | 25 | public subscript( 26 | dynamicMember keyPath: CaseKeyPath // & Sendable 27 | ) -> Bool { 28 | get { storage[keyPath].flatMap { $0[case: keyPath] } != nil } 29 | set { storage[keyPath] = newValue ? keyPath() : nil } 30 | } 31 | } 32 | 33 | extension CaseSet: Collection { 34 | public struct Index: Comparable { 35 | fileprivate let rawValue: [PartialCaseKeyPath: Element].Index 36 | 37 | public static func < (lhs: Self, rhs: Self) -> Bool { 38 | lhs.rawValue < rhs.rawValue 39 | } 40 | } 41 | 42 | public var startIndex: Index { Index(rawValue: storage.startIndex) } 43 | public var endIndex: Index { Index(rawValue: storage.endIndex) } 44 | public func index(after i: Index) -> Index { Index(rawValue: storage.index(after: i.rawValue)) } 45 | public subscript(position: Index) -> Element { storage[position.rawValue].value } 46 | } 47 | 48 | extension CaseSet: SetAlgebra 49 | where Element: Equatable, Element.AllCasePaths: CasePathReflectable { 50 | public var isEmpty: Bool { storage.isEmpty } 51 | 52 | public func union(_ other: CaseSet) -> CaseSet { 53 | var copy = self 54 | copy.formUnion(other) 55 | return copy 56 | } 57 | 58 | public func intersection(_ other: CaseSet) -> CaseSet { 59 | var copy = self 60 | copy.formIntersection(other) 61 | return copy 62 | } 63 | 64 | public func symmetricDifference(_ other: CaseSet) -> CaseSet { 65 | var copy = self 66 | copy.formSymmetricDifference(other) 67 | return copy 68 | } 69 | 70 | @discardableResult 71 | public mutating func insert( 72 | _ newMember: Element 73 | ) -> (inserted: Bool, memberAfterInsert: Element) { 74 | let inserted = storage.updateValue(newMember, forKey: Element.allCasePaths[newMember]) == nil 75 | return (inserted, newMember) 76 | } 77 | 78 | @discardableResult 79 | public mutating func remove(_ member: Element) -> Element? { 80 | storage.removeValue(forKey: Element.allCasePaths[member]) 81 | } 82 | 83 | @discardableResult 84 | public mutating func update(with newMember: Element) -> Element? { 85 | let inserted = storage.updateValue(newMember, forKey: Element.allCasePaths[newMember]) == nil 86 | return inserted ? nil : newMember 87 | } 88 | 89 | public mutating func formUnion(_ other: CaseSet) { 90 | storage.merge(other.storage, uniquingKeysWith: { $1 }) 91 | } 92 | 93 | public mutating func formIntersection(_ other: CaseSet) { 94 | for keyPath in other.storage.keys { 95 | if !storage.keys.contains(keyPath) { 96 | storage.removeValue(forKey: keyPath) 97 | } 98 | } 99 | } 100 | 101 | public mutating func formSymmetricDifference(_ other: CaseSet) { 102 | for (keyPath, value) in other.storage { 103 | if storage.keys.contains(keyPath) { 104 | storage.removeValue(forKey: keyPath) 105 | } else { 106 | storage[keyPath] = value 107 | } 108 | } 109 | } 110 | } 111 | 112 | extension CaseSet: Equatable where Element: Equatable {} 113 | extension CaseSet: Hashable where Element: Hashable {} 114 | extension CaseSet: @unchecked Sendable where Element: Sendable {} 115 | 116 | extension CaseSet: Decodable 117 | where Element: Decodable, Element.AllCasePaths: CasePathReflectable { 118 | public init(from decoder: any Decoder) throws { 119 | var container = try decoder.unkeyedContainer() 120 | if let count = container.count { 121 | storage.reserveCapacity(count) 122 | } 123 | while !container.isAtEnd { 124 | let element = try container.decode(Element.self) 125 | if let original = storage.updateValue(element, forKey: Element.allCasePaths[element]) { 126 | throw DecodingError.dataCorrupted( 127 | DecodingError.Context( 128 | codingPath: container.codingPath, 129 | debugDescription: "Duplicate elements for case: '\(original)', '\(element)'" 130 | ) 131 | ) 132 | } 133 | } 134 | } 135 | } 136 | 137 | extension CaseSet: Encodable where Element: Encodable { 138 | public func encode(to encoder: any Encoder) throws { 139 | var container = encoder.unkeyedContainer() 140 | for element in storage.values { 141 | try container.encode(element) 142 | } 143 | } 144 | } 145 | 146 | extension CaseSet: ExpressibleByArrayLiteral 147 | where Element.AllCasePaths: CasePathReflectable { 148 | public init(arrayLiteral elements: Element...) { 149 | self.init(elements) 150 | } 151 | } 152 | 153 | extension CaseSet { 154 | @_disfavoredOverload 155 | public subscript( 156 | dynamicMember keyPath: CaseKeyPath // & Sendable 157 | ) -> CaseSetBuilder { 158 | CaseSetBuilder(set: self, keyPath: keyPath.unsafeSendable()) 159 | } 160 | } 161 | 162 | public struct CaseSetBuilder { 163 | let set: CaseSet 164 | let keyPath: CaseKeyPath & Sendable 165 | 166 | public func callAsFunction(_ value: Value?) -> CaseSet { 167 | var set = set 168 | set[dynamicMember: keyPath] = value 169 | return set 170 | } 171 | 172 | public func callAsFunction(_ value: Bool = true) -> CaseSet where Value == Void { 173 | var set = set 174 | set[dynamicMember: keyPath] = value ? () : nil 175 | return set 176 | } 177 | } 178 | 179 | extension CaseSet { 180 | public func require( 181 | _ keyPath: repeat CaseKeyPath & Sendable 182 | ) -> (repeat each Value)? { 183 | func unwrap(_ wrapped: Wrapped?) throws -> Wrapped { 184 | guard let wrapped else { throw Nil() } 185 | return wrapped 186 | } 187 | return try? (repeat unwrap(self[dynamicMember: each keyPath])) 188 | } 189 | 190 | private struct Nil: Error {} 191 | } 192 | 193 | @CasePathable private enum Post: Equatable { 194 | case title(String) 195 | case body(String) 196 | case isHidden 197 | } 198 | 199 | @Test private func caseSet() throws { 200 | var set: CaseSet = [.title("Hello")] 201 | set.body = "World" 202 | set.isHidden = true 203 | 204 | #expect(set.title == "Hello") 205 | #expect(set.body == "World") 206 | #expect(set.isHidden) 207 | 208 | let newSet = set.title("Goodnight") 209 | .body("Moon") 210 | .isHidden(false) 211 | 212 | #expect(newSet == [.title("Goodnight"), .body("Moon")]) 213 | 214 | #expect(newSet.title(nil).body(nil).isEmpty) 215 | 216 | let required = try #require(newSet.require(\.title, \.body)) 217 | #expect(required == ("Goodnight", "Moon")) 218 | 219 | #expect(newSet.require(\.title, \.isHidden) == nil) 220 | } 221 | #endif 222 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/CompileTimeTests.swift: -------------------------------------------------------------------------------- 1 | import CasePaths 2 | 3 | @CasePathable 4 | private enum EnumWithExtractAndEmbedCase { 5 | case embed 6 | case extract 7 | } 8 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/DeprecatedTests.swift: -------------------------------------------------------------------------------- 1 | import CasePaths 2 | import XCTest 3 | 4 | protocol TestProtocol: Sendable {} 5 | extension Int: TestProtocol {} 6 | protocol TestClassProtocol: AnyObject {} 7 | 8 | @available(*, deprecated) 9 | final class DeprecatedTests: XCTestCase { 10 | func testSimplePayload() { 11 | enum Enum { case payload(Int) } 12 | let path = /Enum.payload 13 | for _ in 1...2 { 14 | XCTAssertEqual(path.extract(from: .payload(42)), 42) 15 | XCTAssertEqual(path.extract(from: .payload(42)), 42) 16 | } 17 | XCTAssertEqual(AnyCasePath(Enum.payload).extract(from: .payload(42)), 42) 18 | } 19 | 20 | func testSimpleLabeledPayload() { 21 | enum Enum { case payload(label: Int) } 22 | let path = /Enum.payload(label:) 23 | for _ in 1...2 { 24 | XCTAssertEqual(path.extract(from: .payload(label: 42)), 42) 25 | } 26 | XCTAssertEqual(AnyCasePath(Enum.payload(label:)).extract(from: .payload(label: 42)), 42) 27 | } 28 | 29 | func testSimpleOverloadedPayload() { 30 | enum Enum { 31 | case payload(a: Int) 32 | case payload(b: Int) 33 | } 34 | let pathA = /Enum.payload(a:) 35 | let pathB = /Enum.payload(b:) 36 | for _ in 1...2 { 37 | XCTAssertEqual(pathA.extract(from: .payload(a: 42)), 42) 38 | XCTAssertEqual(pathA.extract(from: .payload(b: 42)), nil) 39 | XCTAssertEqual(pathB.extract(from: .payload(a: 42)), nil) 40 | XCTAssertEqual(pathB.extract(from: .payload(b: 42)), 42) 41 | } 42 | XCTAssertEqual(AnyCasePath(Enum.payload(a:)).extract(from: .payload(a: 42)), 42) 43 | XCTAssertEqual(AnyCasePath(Enum.payload(a:)).extract(from: .payload(b: 42)), nil) 44 | XCTAssertEqual(AnyCasePath(Enum.payload(b:)).extract(from: .payload(a: 42)), nil) 45 | XCTAssertEqual(AnyCasePath(Enum.payload(b:)).extract(from: .payload(b: 42)), 42) 46 | } 47 | 48 | func testMultiPayload() { 49 | enum Enum { case payload(Int, String) } 50 | let path: AnyCasePath = /Enum.payload 51 | for _ in 1...2 { 52 | XCTAssert(try unwrap(path.extract(from: .payload(42, "Blob"))) == (42, "Blob")) 53 | } 54 | XCTAssert( 55 | try unwrap(AnyCasePath(Enum.payload).extract(from: .payload(42, "Blob"))) == (42, "Blob") 56 | ) 57 | } 58 | 59 | func testMultiLabeledPayload() { 60 | enum Enum { case payload(a: Int, b: String) } 61 | let path: AnyCasePath = /Enum.payload 62 | for _ in 1...2 { 63 | XCTAssert( 64 | try unwrap(path.extract(from: .payload(a: 42, b: "Blob"))) == (42, "Blob") 65 | ) 66 | XCTAssert( 67 | try unwrap(path.extract(from: .payload(a: 42, b: "Blob"))) == (a: 42, b: "Blob") 68 | ) 69 | } 70 | XCTAssert( 71 | try unwrap(AnyCasePath(Enum.payload).extract(from: .payload(a: 42, b: "Blob"))) == ( 72 | 42, "Blob" 73 | ) 74 | ) 75 | XCTAssert( 76 | try unwrap(AnyCasePath(Enum.payload).extract(from: .payload(a: 42, b: "Blob"))) 77 | == (a: 42, b: "Blob") 78 | ) 79 | } 80 | 81 | func testNoPayload() { 82 | enum Enum { case a, b } 83 | let pathA = /Enum.a 84 | let pathB = /Enum.b 85 | for _ in 1...2 { 86 | XCTAssertNotNil(pathA.extract(from: .a)) 87 | XCTAssertNotNil(pathB.extract(from: .b)) 88 | XCTAssertNil(pathA.extract(from: .b)) 89 | XCTAssertNil(pathB.extract(from: .a)) 90 | } 91 | XCTAssertNotNil(AnyCasePath(Enum.a).extract(from: .a)) 92 | XCTAssertNotNil(AnyCasePath(Enum.b).extract(from: .b)) 93 | XCTAssertNil(AnyCasePath(Enum.a).extract(from: .b)) 94 | XCTAssertNil(AnyCasePath(Enum.b).extract(from: .a)) 95 | } 96 | 97 | func testZeroMemoryLayoutPayload() { 98 | struct Unit1 {} 99 | enum Unit2 { case unit } 100 | enum Enum { 101 | case void(Void) 102 | case unit1(Unit1) 103 | case unit2(Unit2) 104 | } 105 | let path1 = /Enum.void 106 | let path2 = /Enum.unit1 107 | let path3 = /Enum.unit2 108 | for _ in 1...2 { 109 | XCTAssertNotNil(path1.extract(from: .void(()))) 110 | XCTAssertNotNil(path2.extract(from: .unit1(.init()))) 111 | XCTAssertNotNil(path3.extract(from: .unit2(.unit))) 112 | XCTAssertNil(path1.extract(from: .unit1(.init()))) 113 | XCTAssertNil(path1.extract(from: .unit2(.unit))) 114 | XCTAssertNil(path2.extract(from: .void(()))) 115 | XCTAssertNil(path2.extract(from: .unit2(.unit))) 116 | XCTAssertNil(path3.extract(from: .void(()))) 117 | XCTAssertNil(path3.extract(from: .unit1(.init()))) 118 | } 119 | XCTAssertNotNil(AnyCasePath(Enum.void).extract(from: .void(()))) 120 | XCTAssertNotNil(AnyCasePath(Enum.unit1).extract(from: .unit1(.init()))) 121 | XCTAssertNotNil(AnyCasePath(Enum.unit2).extract(from: .unit2(.unit))) 122 | XCTAssertNil(AnyCasePath(Enum.void).extract(from: .unit1(.init()))) 123 | XCTAssertNil(AnyCasePath(Enum.void).extract(from: .unit2(.unit))) 124 | XCTAssertNil(AnyCasePath(Enum.unit1).extract(from: .void(()))) 125 | XCTAssertNil(AnyCasePath(Enum.unit1).extract(from: .unit2(.unit))) 126 | XCTAssertNil(AnyCasePath(Enum.unit2).extract(from: .void(()))) 127 | XCTAssertNil(AnyCasePath(Enum.unit2).extract(from: .unit1(.init()))) 128 | } 129 | 130 | func testUninhabitedPayload() { 131 | enum Uninhabited {} 132 | enum Enum { 133 | case never(Never) 134 | case uninhabited(Uninhabited) 135 | case value 136 | } 137 | let path1 = /Enum.never 138 | let path2 = /Enum.uninhabited 139 | for _ in 1...2 { 140 | XCTAssertNil(path1.extract(from: .value)) 141 | XCTAssertNil(path2.extract(from: .value)) 142 | } 143 | XCTAssertNil(AnyCasePath(Enum.never).extract(from: .value)) 144 | XCTAssertNil(AnyCasePath(Enum.uninhabited).extract(from: .value)) 145 | } 146 | 147 | // Dynamic closure payload extraction is not supported on WebAssembly 148 | // 149 | // Discussion: 150 | // Closure payloads `() -> Void` are extracted via generic abstraction 151 | // `Value` in `extractHelp`, and the extracted closure is called as 152 | // `() -> @out τ_0_0 where τ_0_0 == Void`. 153 | // However, the closure value is stored without generic abstraction 154 | // in the enum payload storage, and not wrapped by abstraction thunk. 155 | // This abstraction mismatch causes signature mismatch between the 156 | // call-site and the actual callee. On most platforms, this mismatch 157 | // is luckily not a problem, but WebAssembly enforces the signature 158 | // match and causes a runtime crash. 159 | // Ideally, `extractHelp` should insert an abstraction thunk when 160 | // extracting a closure payload, but it cannot be done without 161 | // JIT code generation (and WebAssembly does not support JIT...) 162 | #if !arch(wasm32) 163 | func testClosurePayload() throws { 164 | enum Enum { case closure(() -> Void) } 165 | let path = /Enum.closure 166 | for _ in 1...2 { 167 | var invoked = false 168 | let closure = try unwrap(path.extract(from: .closure { invoked = true })) 169 | closure() 170 | XCTAssertTrue(invoked) 171 | } 172 | var invoked = false 173 | let closure = try unwrap(AnyCasePath(Enum.closure).extract(from: .closure { invoked = true })) 174 | closure() 175 | XCTAssertTrue(invoked) 176 | } 177 | #endif 178 | 179 | func testRecursivePayload() { 180 | indirect enum Enum: Equatable { 181 | case indirect(Enum) 182 | case direct 183 | } 184 | let shallowPath = /Enum.indirect 185 | let deepPath = /Enum.indirect 186 | for _ in 1...2 { 187 | XCTAssertEqual(shallowPath.extract(from: .indirect(.direct)), .direct) 188 | XCTAssertEqual( 189 | deepPath.extract(from: .indirect(.indirect(.direct))), .indirect(.direct) 190 | ) 191 | } 192 | XCTAssertEqual(AnyCasePath(Enum.indirect).extract(from: .indirect(.direct)), .direct) 193 | XCTAssertEqual( 194 | AnyCasePath(Enum.indirect).extract(from: .indirect(.indirect(.direct))), .indirect(.direct) 195 | ) 196 | } 197 | 198 | func testIndirectSimplePayload() { 199 | enum Enum: Equatable { 200 | indirect case indirect(Int) 201 | case direct(Int) 202 | } 203 | 204 | let indirectPath = /Enum.indirect 205 | let directPath = /Enum.direct 206 | 207 | for _ in 1...2 { 208 | XCTAssertEqual(indirectPath.extract(from: .indirect(42)), 42) 209 | XCTAssertEqual(indirectPath.extract(from: .direct(42)), nil) 210 | XCTAssertEqual(directPath.extract(from: .indirect(42)), nil) 211 | XCTAssertEqual(directPath.extract(from: .direct(42)), 42) 212 | } 213 | XCTAssertEqual(AnyCasePath(Enum.indirect).extract(from: .indirect(42)), 42) 214 | XCTAssertEqual(AnyCasePath(Enum.indirect).extract(from: .direct(42)), nil) 215 | XCTAssertEqual(AnyCasePath(Enum.direct).extract(from: .indirect(42)), nil) 216 | XCTAssertEqual(AnyCasePath(Enum.direct).extract(from: .direct(42)), 42) 217 | } 218 | 219 | fileprivate class Object: Equatable { 220 | static func == (lhs: Object, rhs: Object) -> Bool { 221 | return lhs === rhs 222 | } 223 | } 224 | 225 | func testIndirectCompoundPayload() throws { 226 | let object = Object() 227 | 228 | enum Enum: Equatable { 229 | indirect case indirect(Int, Object?, Int, Object?) 230 | case direct(Int, Object?, Int, Object?) 231 | } 232 | 233 | let indirectPath: AnyCasePath = /Enum.indirect 234 | let directPath: AnyCasePath = /Enum.direct 235 | 236 | for _ in 1...2 { 237 | XCTAssert( 238 | try unwrap(indirectPath.extract(from: .indirect(42, nil, 43, object))) 239 | == (42, nil, 43, object) 240 | ) 241 | XCTAssertNil(indirectPath.extract(from: .direct(42, nil, 43, object))) 242 | XCTAssertNil(directPath.extract(from: .indirect(42, nil, 43, object))) 243 | XCTAssert( 244 | try unwrap(directPath.extract(from: .direct(42, nil, 43, object))) == (42, nil, 43, object) 245 | ) 246 | } 247 | XCTAssert( 248 | try unwrap(AnyCasePath(Enum.indirect).extract(from: .indirect(42, nil, 43, object))) 249 | == (42, nil, 43, object) 250 | ) 251 | XCTAssertNil(AnyCasePath(Enum.indirect).extract(from: .direct(42, nil, 43, object))) 252 | XCTAssertNil(AnyCasePath(Enum.direct).extract(from: .indirect(42, nil, 43, object))) 253 | XCTAssert( 254 | try unwrap(AnyCasePath(Enum.direct).extract(from: .direct(42, nil, 43, object))) == ( 255 | 42, nil, 43, object 256 | ) 257 | ) 258 | } 259 | 260 | #if RELEASE 261 | func testNonEnumExtract() { 262 | // This is a bogus CasePath, intended to verify that it just returns nil. 263 | let path: CasePath = /{ $0 } 264 | 265 | for _ in 1...2 { 266 | XCTAssertNil(path.extract(from: 42)) 267 | } 268 | XCTAssertNil(CasePath { $0 }.extract(from: 42)) 269 | } 270 | #endif 271 | 272 | func testOptionalPayload() { 273 | enum Enum { case int(Int?) } 274 | let path = /Enum.int 275 | for _ in 1...2 { 276 | XCTAssertEqual(path.extract(from: .int(.some(42))), .some(.some(42))) 277 | XCTAssertEqual(path.extract(from: .int(.none)), .some(.none)) 278 | } 279 | XCTAssertEqual(AnyCasePath(Enum.int).extract(from: .int(.some(42))), .some(.some(42))) 280 | XCTAssertEqual(AnyCasePath(Enum.int).extract(from: .int(.none)), .some(.none)) 281 | } 282 | 283 | func testAnyPayload() { 284 | enum Enum { case any(Any) } 285 | let path = /Enum.any 286 | for _ in 1...2 { 287 | XCTAssertEqual(path.extract(from: .any(42)) as? Int, 42) 288 | } 289 | XCTAssertEqual(AnyCasePath(Enum.any).extract(from: .any(42)) as? Int, 42) 290 | } 291 | 292 | func testAnyObjectPayload() { 293 | class Class {} 294 | enum Enum { case anyObject(AnyObject) } 295 | let object = Class() 296 | let nsObject = NSObject() 297 | let path = /Enum.anyObject 298 | for _ in 1...2 { 299 | XCTAssert(try unwrap(path.extract(from: .anyObject(object))) === object) 300 | XCTAssert(try unwrap(path.extract(from: .anyObject(nsObject))) === nsObject) 301 | } 302 | XCTAssert(try unwrap(AnyCasePath(Enum.anyObject).extract(from: .anyObject(object))) === object) 303 | XCTAssert( 304 | try unwrap(AnyCasePath(Enum.anyObject).extract(from: .anyObject(nsObject))) === nsObject) 305 | } 306 | 307 | func testProtocolPayload() { 308 | struct Error: Swift.Error, Equatable {} 309 | enum Enum { case error(Swift.Error) } 310 | let path = /Enum.error 311 | for _ in 1...2 { 312 | XCTAssertEqual(path.extract(from: .error(Error())) as? Error, Error()) 313 | } 314 | XCTAssertEqual(AnyCasePath(Enum.error).extract(from: .error(Error())) as? Error, Error()) 315 | } 316 | 317 | func testSubclassPayload() { 318 | class Superclass {} 319 | class Subclass: Superclass {} 320 | enum Enum { 321 | case superclass(Superclass) 322 | case subclass(Subclass) 323 | } 324 | let superclass = Superclass() 325 | let subclass = Subclass() 326 | let superclassPath = /Enum.superclass 327 | let subclassPath = /Enum.subclass 328 | for _ in 1...2 { 329 | XCTAssert( 330 | try unwrap(superclassPath.extract(from: .superclass(superclass))) === superclass 331 | ) 332 | XCTAssert( 333 | try unwrap(superclassPath.extract(from: .superclass(subclass))) === subclass 334 | ) 335 | XCTAssert( 336 | try unwrap(subclassPath.extract(from: .subclass(subclass))) === subclass 337 | ) 338 | } 339 | XCTAssert( 340 | try unwrap(AnyCasePath(Enum.superclass).extract(from: .superclass(superclass))) === superclass 341 | ) 342 | XCTAssert( 343 | try unwrap(AnyCasePath(Enum.superclass).extract(from: .superclass(subclass))) === subclass 344 | ) 345 | XCTAssert( 346 | try unwrap(AnyCasePath(Enum.subclass).extract(from: .subclass(subclass))) === subclass 347 | ) 348 | } 349 | 350 | func testDefaults() { 351 | enum Enum { case n(Int, m: Int? = nil, file: String = #file, line: UInt = #line) } 352 | let path: AnyCasePath = /Enum.n 353 | for _ in 1...2 { 354 | XCTAssert( 355 | try unwrap(path.extract(from: .n(42))) == (42, nil, #file, #line) 356 | ) 357 | } 358 | XCTAssert( 359 | try unwrap(AnyCasePath(Enum.n).extract(from: .n(42))) == (42, nil, #file, #line) 360 | ) 361 | } 362 | 363 | func testDifferentMemoryLayouts() { 364 | struct Struct { var array: [Int] = [1, 2, 3], string: String = "Blob" } 365 | enum Enum { 366 | case bool(Bool) 367 | case int(Int) 368 | case void(Void) 369 | case structure(Struct) 370 | case any(Any) 371 | } 372 | 373 | let boolPath = /Enum.bool 374 | let intPath = /Enum.int 375 | let voidPath = /Enum.void 376 | let structPath = /Enum.structure 377 | let anyPath = /Enum.any 378 | for _ in 1...2 { 379 | XCTAssertNil(boolPath.extract(from: .int(42))) 380 | XCTAssertNil(boolPath.extract(from: .void(()))) 381 | XCTAssertNil(boolPath.extract(from: .structure(.init()))) 382 | XCTAssertNil(boolPath.extract(from: .any("Blob"))) 383 | XCTAssertNil(intPath.extract(from: .bool(true))) 384 | XCTAssertNil(intPath.extract(from: .void(()))) 385 | XCTAssertNil(intPath.extract(from: .structure(.init()))) 386 | XCTAssertNil(intPath.extract(from: .any("Blob"))) 387 | XCTAssertNil(voidPath.extract(from: .bool(true))) 388 | XCTAssertNil(voidPath.extract(from: .int(42))) 389 | XCTAssertNil(voidPath.extract(from: .structure(.init()))) 390 | XCTAssertNil(voidPath.extract(from: .any("Blob"))) 391 | XCTAssertNil(structPath.extract(from: .bool(true))) 392 | XCTAssertNil(structPath.extract(from: .int(42))) 393 | XCTAssertNil(structPath.extract(from: .void(()))) 394 | XCTAssertNil(structPath.extract(from: .any("Blob"))) 395 | XCTAssertNil(anyPath.extract(from: .bool(true))) 396 | XCTAssertNil(anyPath.extract(from: .int(42))) 397 | XCTAssertNil(anyPath.extract(from: .void(()))) 398 | XCTAssertNil(anyPath.extract(from: .structure(.init()))) 399 | 400 | XCTAssertNotNil(boolPath.extract(from: .bool(true))) 401 | XCTAssertNotNil(intPath.extract(from: .int(42))) 402 | XCTAssertNotNil(voidPath.extract(from: .void(()))) 403 | XCTAssertNotNil(anyPath.extract(from: .any("Blob"))) 404 | } 405 | XCTAssertNil(AnyCasePath(Enum.bool).extract(from: .int(42))) 406 | XCTAssertNil(AnyCasePath(Enum.bool).extract(from: .void(()))) 407 | XCTAssertNil(AnyCasePath(Enum.bool).extract(from: .structure(.init()))) 408 | XCTAssertNil(AnyCasePath(Enum.bool).extract(from: .any("Blob"))) 409 | XCTAssertNil(AnyCasePath(Enum.int).extract(from: .bool(true))) 410 | XCTAssertNil(AnyCasePath(Enum.int).extract(from: .void(()))) 411 | XCTAssertNil(AnyCasePath(Enum.int).extract(from: .structure(.init()))) 412 | XCTAssertNil(AnyCasePath(Enum.int).extract(from: .any("Blob"))) 413 | XCTAssertNil(AnyCasePath(Enum.void).extract(from: .bool(true))) 414 | XCTAssertNil(AnyCasePath(Enum.void).extract(from: .int(42))) 415 | XCTAssertNil(AnyCasePath(Enum.void).extract(from: .structure(.init()))) 416 | XCTAssertNil(AnyCasePath(Enum.void).extract(from: .any("Blob"))) 417 | XCTAssertNil(AnyCasePath(Enum.structure).extract(from: .bool(true))) 418 | XCTAssertNil(AnyCasePath(Enum.structure).extract(from: .int(42))) 419 | XCTAssertNil(AnyCasePath(Enum.structure).extract(from: .void(()))) 420 | XCTAssertNil(AnyCasePath(Enum.structure).extract(from: .any("Blob"))) 421 | XCTAssertNil(AnyCasePath(Enum.any).extract(from: .bool(true))) 422 | XCTAssertNil(AnyCasePath(Enum.any).extract(from: .int(42))) 423 | XCTAssertNil(AnyCasePath(Enum.any).extract(from: .void(()))) 424 | XCTAssertNil(AnyCasePath(Enum.any).extract(from: .structure(.init()))) 425 | 426 | XCTAssertNotNil(AnyCasePath(Enum.bool).extract(from: .bool(true))) 427 | XCTAssertNotNil(AnyCasePath(Enum.int).extract(from: .int(42))) 428 | XCTAssertNotNil(AnyCasePath(Enum.void).extract(from: .void(()))) 429 | XCTAssertNotNil(AnyCasePath(Enum.structure).extract(from: .structure(.init()))) 430 | XCTAssertNotNil(AnyCasePath(Enum.any).extract(from: .any("Blob"))) 431 | } 432 | 433 | func testAssociatedValueIsExistential() { 434 | enum Enum { 435 | case proto(TestProtocol) 436 | case int(Int) 437 | } 438 | 439 | let protoPath = /Enum.proto 440 | let intPath = /Enum.int 441 | 442 | for _ in 1...2 { 443 | XCTAssertNil(protoPath.extract(from: .int(100))) 444 | XCTAssertEqual(protoPath.extract(from: .proto(100)) as? Int, 100) 445 | 446 | XCTAssertNil(intPath.extract(from: .proto(100))) 447 | XCTAssertEqual(intPath.extract(from: .int(100)), 100) 448 | } 449 | XCTAssertNil(AnyCasePath(Enum.proto).extract(from: .int(100))) 450 | XCTAssertEqual(AnyCasePath(Enum.proto).extract(from: .proto(100)) as? Int, 100) 451 | 452 | XCTAssertNil(AnyCasePath(Enum.int).extract(from: .proto(100))) 453 | XCTAssertEqual(AnyCasePath(Enum.int).extract(from: .int(100)), 100) 454 | } 455 | 456 | func testClassConstrainedExistential() { 457 | class Class: TestClassProtocol {} 458 | enum Enum { 459 | case proto(TestClassProtocol) 460 | case int(Int) 461 | } 462 | let protoPath = /Enum.proto 463 | let intPath = /Enum.int 464 | 465 | let object = Class() 466 | 467 | for _ in 1...2 { 468 | XCTAssertNil(protoPath.extract(from: .int(100))) 469 | XCTAssertTrue(protoPath.extract(from: .proto(object)) === object) 470 | 471 | XCTAssertNil(intPath.extract(from: .proto(object))) 472 | XCTAssertEqual(intPath.extract(from: .int(100)), 100) 473 | } 474 | XCTAssertNil(AnyCasePath(Enum.proto).extract(from: .int(100))) 475 | XCTAssertTrue(AnyCasePath(Enum.proto).extract(from: .proto(object)) === object) 476 | 477 | XCTAssertNil(AnyCasePath(Enum.int).extract(from: .proto(object))) 478 | XCTAssertEqual(AnyCasePath(Enum.int).extract(from: .int(100)), 100) 479 | } 480 | 481 | func testContravariantEmbed() { 482 | enum Enum: Sendable { 483 | // associated value type is TestProtocol existential 484 | case directExistential(TestProtocol) 485 | 486 | // associated value type is single-element tuple (TestProtocol existential) 487 | case directTuple(label: TestProtocol) 488 | 489 | indirect case indirectExistential(TestProtocol) 490 | 491 | indirect case indirectTuple(label: TestProtocol) 492 | 493 | static let cdeCase = Enum.directExistential(Conformer()) 494 | static let cdtCase = Enum.directTuple(label: Conformer()) 495 | static let cieCase = Enum.indirectExistential(Conformer()) 496 | static let citCase = Enum.indirectTuple(label: Conformer()) 497 | 498 | static let ideCase = Enum.directExistential(100) 499 | static let idtCase = Enum.directTuple(label: 100) 500 | static let iieCase = Enum.indirectExistential(100) 501 | static let iitCase = Enum.indirectTuple(label: 100) 502 | } 503 | 504 | // This is intentionally too big to fit in the three-word buffer of a protocol existential, so 505 | // that it is stored indirectly. 506 | struct Conformer: TestProtocol, Equatable { 507 | var a, b, c, d: Int 508 | init() { 509 | (a, b, c, d) = (100, 300, 200, 400) 510 | } 511 | } 512 | 513 | var dePath: AnyCasePath = /Enum.directExistential 514 | var dtPath: AnyCasePath = /Enum.directTuple 515 | var iePath: AnyCasePath = /Enum.indirectExistential 516 | var itPath: AnyCasePath = /Enum.indirectTuple 517 | 518 | for _ in 1...2 { 519 | XCTAssertNil(dePath.extract(from: .cdtCase)) 520 | XCTAssertNil(dePath.extract(from: .cieCase)) 521 | XCTAssertNil(dePath.extract(from: .citCase)) 522 | XCTAssertNil(dePath.extract(from: .ideCase)) 523 | XCTAssertNil(dePath.extract(from: .idtCase)) 524 | XCTAssertNil(dePath.extract(from: .iieCase)) 525 | XCTAssertNil(dePath.extract(from: .iitCase)) 526 | XCTAssertEqual(dePath.extract(from: .cdeCase), .some(Conformer())) 527 | 528 | XCTAssertNil(dtPath.extract(from: .cdeCase)) 529 | XCTAssertNil(dtPath.extract(from: .cieCase)) 530 | XCTAssertNil(dtPath.extract(from: .citCase)) 531 | XCTAssertNil(dtPath.extract(from: .ideCase)) 532 | XCTAssertNil(dtPath.extract(from: .idtCase)) 533 | XCTAssertNil(dtPath.extract(from: .iieCase)) 534 | XCTAssertNil(dtPath.extract(from: .iitCase)) 535 | XCTAssertEqual(dtPath.extract(from: .cdtCase), .some(Conformer())) 536 | 537 | XCTAssertNil(iePath.extract(from: .cdeCase)) 538 | XCTAssertNil(iePath.extract(from: .cdtCase)) 539 | XCTAssertNil(iePath.extract(from: .citCase)) 540 | XCTAssertNil(iePath.extract(from: .ideCase)) 541 | XCTAssertNil(iePath.extract(from: .idtCase)) 542 | XCTAssertNil(iePath.extract(from: .iieCase)) 543 | XCTAssertNil(iePath.extract(from: .iitCase)) 544 | XCTAssertEqual(iePath.extract(from: .cieCase), .some(Conformer())) 545 | 546 | XCTAssertNil(itPath.extract(from: .cdeCase)) 547 | XCTAssertNil(itPath.extract(from: .cdtCase)) 548 | XCTAssertNil(itPath.extract(from: .cieCase)) 549 | XCTAssertNil(itPath.extract(from: .ideCase)) 550 | XCTAssertNil(itPath.extract(from: .idtCase)) 551 | XCTAssertNil(itPath.extract(from: .iieCase)) 552 | XCTAssertNil(itPath.extract(from: .iitCase)) 553 | XCTAssertEqual(itPath.extract(from: .citCase), .some(Conformer())) 554 | } 555 | 556 | dePath = AnyCasePath(Enum.directExistential) 557 | dtPath = AnyCasePath(Enum.directTuple) 558 | iePath = AnyCasePath(Enum.indirectExistential) 559 | itPath = AnyCasePath(Enum.indirectTuple) 560 | 561 | XCTAssertNil(dePath.extract(from: .cdtCase)) 562 | XCTAssertNil(dePath.extract(from: .cieCase)) 563 | XCTAssertNil(dePath.extract(from: .citCase)) 564 | XCTAssertNil(dePath.extract(from: .ideCase)) 565 | XCTAssertNil(dePath.extract(from: .idtCase)) 566 | XCTAssertNil(dePath.extract(from: .iieCase)) 567 | XCTAssertNil(dePath.extract(from: .iitCase)) 568 | XCTAssertEqual(dePath.extract(from: .cdeCase), .some(Conformer())) 569 | 570 | XCTAssertNil(dtPath.extract(from: .cdeCase)) 571 | XCTAssertNil(dtPath.extract(from: .cieCase)) 572 | XCTAssertNil(dtPath.extract(from: .citCase)) 573 | XCTAssertNil(dtPath.extract(from: .ideCase)) 574 | XCTAssertNil(dtPath.extract(from: .idtCase)) 575 | XCTAssertNil(dtPath.extract(from: .iieCase)) 576 | XCTAssertNil(dtPath.extract(from: .iitCase)) 577 | XCTAssertEqual(dtPath.extract(from: .cdtCase), .some(Conformer())) 578 | 579 | XCTAssertNil(iePath.extract(from: .cdeCase)) 580 | XCTAssertNil(iePath.extract(from: .cdtCase)) 581 | XCTAssertNil(iePath.extract(from: .citCase)) 582 | XCTAssertNil(iePath.extract(from: .ideCase)) 583 | XCTAssertNil(iePath.extract(from: .idtCase)) 584 | XCTAssertNil(iePath.extract(from: .iieCase)) 585 | XCTAssertNil(iePath.extract(from: .iitCase)) 586 | XCTAssertEqual(iePath.extract(from: .cieCase), .some(Conformer())) 587 | 588 | XCTAssertNil(itPath.extract(from: .cdeCase)) 589 | XCTAssertNil(itPath.extract(from: .cdtCase)) 590 | XCTAssertNil(itPath.extract(from: .cieCase)) 591 | XCTAssertNil(itPath.extract(from: .ideCase)) 592 | XCTAssertNil(itPath.extract(from: .idtCase)) 593 | XCTAssertNil(itPath.extract(from: .iieCase)) 594 | XCTAssertNil(itPath.extract(from: .iitCase)) 595 | XCTAssertEqual(itPath.extract(from: .citCase), .some(Conformer())) 596 | } 597 | 598 | func testCompoundContravariantEmbed() { 599 | enum Enum { 600 | case c(TestProtocol, Int) 601 | } 602 | 603 | let path: AnyCasePath = /Enum.c 604 | 605 | for _ in 1...2 { 606 | XCTAssert(try XCTUnwrap(path.extract(from: .c(34, 12))) == (34, 12)) 607 | } 608 | } 609 | 610 | func testPathExtractFromOptionalRoot() { 611 | enum Authentication { 612 | case authenticated(token: String) 613 | case unauthenticated 614 | } 615 | 616 | let root: Authentication? = .authenticated(token: "deadbeef") 617 | let path: AnyCasePath = /Authentication.authenticated 618 | for _ in 1...2 { 619 | let actual = path.extract(from: root) 620 | XCTAssertEqual(actual, "deadbeef") 621 | } 622 | XCTAssertEqual(AnyCasePath(Authentication.authenticated).extract(from: root), "deadbeef") 623 | } 624 | 625 | func testPathExtractFromOptionalRoot_AnyHashable() { 626 | enum Authentication { 627 | case authenticated(token: AnyHashable) 628 | case unauthenticated 629 | } 630 | 631 | let root: Authentication? = .authenticated(token: "deadbeef") 632 | let path: AnyCasePath = /Authentication.authenticated 633 | for _ in 1...2 { 634 | let actual = path.extract(from: root) 635 | XCTAssertEqual(actual, "deadbeef") 636 | } 637 | } 638 | 639 | func testEmbed() { 640 | enum Foo: Equatable { case bar(Int) } 641 | 642 | let fooBar = /Foo.bar 643 | XCTAssertEqual(.bar(42), fooBar.embed(42)) 644 | XCTAssertEqual(.bar(42), (/Foo.self).embed(Foo.bar(42))) 645 | XCTAssertEqual(.bar(42), AnyCasePath(Foo.bar).embed(42)) 646 | XCTAssertEqual(.bar(42), AnyCasePath(Foo.self).embed(Foo.bar(42))) 647 | } 648 | 649 | func testNestedEmbed() { 650 | enum Foo: Equatable { case bar(Bar) } 651 | enum Bar: Equatable { case baz(Int) } 652 | 653 | let fooBaz = /Foo.bar .. Bar.baz 654 | XCTAssertEqual(.bar(.baz(42)), fooBaz.embed(42)) 655 | XCTAssertEqual(.bar(.baz(42)), AnyCasePath(Foo.bar).appending(path: .init(Bar.baz)).embed(42)) 656 | } 657 | 658 | func testVoidAnyCasePath() { 659 | enum Foo: Equatable { case bar } 660 | 661 | let fooBar = /Foo.bar 662 | XCTAssertEqual(.bar, fooBar.embed(())) 663 | XCTAssertEqual(.bar, AnyCasePath(Foo.bar).embed(())) 664 | } 665 | 666 | func testCasePaths() { 667 | let some = /String?.some 668 | XCTAssertEqual( 669 | .some("Hello"), 670 | some.extract(from: "Hello") 671 | ) 672 | XCTAssertNil( 673 | some.extract(from: .none) 674 | ) 675 | XCTAssertEqual( 676 | .some("Hello"), 677 | AnyCasePath(String?.some).extract(from: "Hello") 678 | ) 679 | XCTAssertNil( 680 | AnyCasePath(String?.some).extract(from: .none) 681 | ) 682 | 683 | struct MyError: Equatable, Error {} 684 | var success = /Result.success 685 | var failure = /Result.failure 686 | var mySuccess = /Result.success 687 | var myFailure = /Result.failure 688 | 689 | for _ in 1...2 { 690 | XCTAssertEqual( 691 | .some("Hello"), 692 | success.extract(from: .success("Hello")) 693 | ) 694 | XCTAssertNil( 695 | failure.extract(from: .success("Hello")) 696 | ) 697 | XCTAssertEqual( 698 | .some(MyError()), 699 | failure.extract(from: .failure(MyError())) as? MyError 700 | ) 701 | XCTAssertNil( 702 | success.extract(from: .failure(MyError())) 703 | ) 704 | XCTAssertEqual( 705 | .some(MyError()), 706 | myFailure.extract(from: .failure(MyError())) 707 | ) 708 | XCTAssertNil( 709 | mySuccess.extract(from: .failure(MyError())) 710 | ) 711 | } 712 | 713 | success = AnyCasePath(Result.success) 714 | failure = AnyCasePath(Result.failure) 715 | mySuccess = AnyCasePath(Result.success) 716 | myFailure = AnyCasePath(Result.failure) 717 | 718 | XCTAssertEqual( 719 | .some("Hello"), 720 | success.extract(from: .success("Hello")) 721 | ) 722 | XCTAssertNil( 723 | failure.extract(from: .success("Hello")) 724 | ) 725 | XCTAssertEqual( 726 | .some(MyError()), 727 | failure.extract(from: .failure(MyError())) as? MyError 728 | ) 729 | XCTAssertNil( 730 | success.extract(from: .failure(MyError())) 731 | ) 732 | XCTAssertEqual( 733 | .some(MyError()), 734 | myFailure.extract(from: .failure(MyError())) 735 | ) 736 | XCTAssertNil( 737 | mySuccess.extract(from: .failure(MyError())) 738 | ) 739 | } 740 | 741 | func testIdentity() { 742 | let id = /Int.self 743 | XCTAssertEqual( 744 | .some(42), 745 | id.extract(from: 42) 746 | ) 747 | XCTAssertEqual( 748 | .some(42), 749 | (/.self) 750 | .extract(from: 42) 751 | ) 752 | 753 | XCTAssertEqual( 754 | .some(42), 755 | AnyCasePath(Int.self).extract(from: 42) 756 | ) 757 | XCTAssertEqual( 758 | .some(42), 759 | AnyCasePath(Int.self) 760 | .extract(from: 42) 761 | ) 762 | } 763 | 764 | func testSome() { 765 | XCTAssertEqual( 766 | (/.some).extract(from: Optional(42)), 767 | .some(42) 768 | ) 769 | } 770 | 771 | func testLabeledCases() { 772 | enum Foo: Equatable { 773 | case bar(some: Int) 774 | case bar(none: Int) 775 | } 776 | 777 | let fooBarSome = /Foo.bar(some:) 778 | XCTAssertEqual( 779 | .some(42), 780 | fooBarSome.extract(from: .bar(some: 42)) 781 | ) 782 | XCTAssertNil( 783 | fooBarSome.extract(from: .bar(none: 42)) 784 | ) 785 | 786 | XCTAssertEqual( 787 | .some(42), 788 | AnyCasePath(Foo.bar(some:)).extract(from: .bar(some: 42)) 789 | ) 790 | XCTAssertNil( 791 | AnyCasePath(Foo.bar(some:)).extract(from: .bar(none: 42)) 792 | ) 793 | } 794 | 795 | func testMultiCases() { 796 | enum Foo { 797 | case bar(Int, String) 798 | } 799 | 800 | guard let fizzBuzz = (/Foo.bar).extract(from: .bar(42, "Blob")) 801 | else { 802 | XCTFail() 803 | return 804 | } 805 | XCTAssertEqual(42, fizzBuzz.0) 806 | XCTAssertEqual("Blob", fizzBuzz.1) 807 | } 808 | 809 | func testMultiLabeledCases() { 810 | enum Foo { 811 | case bar(fizz: Int, buzz: String) 812 | } 813 | 814 | let fooBar: AnyCasePath = /Foo.bar(fizz:buzz:) 815 | guard let fizzBuzz = fooBar.extract(from: .bar(fizz: 42, buzz: "Blob")) 816 | else { 817 | XCTFail() 818 | return 819 | } 820 | XCTAssertEqual(42, fizzBuzz.fizz) 821 | XCTAssertEqual("Blob", fizzBuzz.buzz) 822 | } 823 | 824 | func testMultiMixedCases() { 825 | enum Foo { 826 | case bar(Int, buzz: String) 827 | } 828 | 829 | guard let fizzBuzz = (/Foo.bar).extract(from: .bar(42, buzz: "Blob")) 830 | else { 831 | XCTFail() 832 | return 833 | } 834 | XCTAssertEqual(42, fizzBuzz.0) 835 | XCTAssertEqual("Blob", fizzBuzz.1) 836 | } 837 | 838 | func testNestedZeroMemoryLayout() { 839 | enum Foo { 840 | case bar(Bar) 841 | } 842 | enum Bar: Equatable { 843 | case baz 844 | } 845 | 846 | let fooBar = /Foo.bar 847 | XCTAssertEqual( 848 | .baz, 849 | fooBar.extract(from: .bar(.baz)) 850 | ) 851 | } 852 | 853 | func testCompoundUninhabitedType() { 854 | // Under Swift 5.1 (Xcode 11.3), this test creates a bogus instance of the tuple 855 | // `(Never, Never)`, but remarkably, doesn't cause a crash and extracts the correct answer 856 | // (`nil`). 857 | 858 | enum Enum { 859 | case nevers(Never, Never) 860 | case something(Void) 861 | } 862 | 863 | let path: AnyCasePath = /Enum.nevers(_:_:) 864 | 865 | for _ in 1...2 { 866 | XCTAssertNil(path.extract(from: Enum.something(()))) 867 | } 868 | } 869 | 870 | func testNestedUninhabitedTypes() { 871 | enum Uninhabited {} 872 | 873 | enum Foo { 874 | case foo 875 | case bar(Uninhabited) 876 | case baz(Never) 877 | } 878 | 879 | let fooBar = /Foo.bar 880 | XCTAssertNil(fooBar.extract(from: Foo.foo)) 881 | 882 | let fooBaz = /Foo.baz 883 | XCTAssertNil(fooBaz.extract(from: Foo.foo)) 884 | } 885 | 886 | func testEnumsWithoutAssociatedValues() { 887 | enum Foo: Equatable { 888 | case bar 889 | case baz 890 | } 891 | 892 | XCTAssertNotNil( 893 | (/Foo.bar) 894 | .extract(from: .bar) 895 | ) 896 | XCTAssertNil( 897 | (/Foo.bar) 898 | .extract(from: .baz) 899 | ) 900 | 901 | XCTAssertNotNil( 902 | (/Foo.baz) 903 | .extract(from: .baz) 904 | ) 905 | XCTAssertNil( 906 | (/Foo.baz) 907 | .extract(from: .bar) 908 | ) 909 | } 910 | 911 | // Dynamic closure payload extraction is not supported on WebAssembly 912 | // See testClosurePayload for more details 913 | #if !arch(wasm32) 914 | func testEnumsWithClosures() { 915 | enum Foo { 916 | case bar(() -> Void) 917 | } 918 | 919 | var didRun = false 920 | let fooBar = /Foo.bar 921 | guard let bar = fooBar.extract(from: .bar { didRun = true }) 922 | else { 923 | XCTFail() 924 | return 925 | } 926 | bar() 927 | XCTAssertTrue(didRun) 928 | } 929 | #endif 930 | 931 | func testRecursive() { 932 | indirect enum Foo { 933 | case foo(Foo) 934 | case bar(Int) 935 | } 936 | 937 | XCTAssertEqual( 938 | .some(42), 939 | (/Foo.foo .. /Foo.foo .. /Foo.foo .. /Foo.bar).extract(from: .foo(.foo(.foo(.bar(42))))) 940 | ) 941 | XCTAssertNil( 942 | (/Foo.foo .. /Foo.foo .. /Foo.foo .. /Foo.bar).extract(from: .foo(.foo(.bar(42)))) 943 | ) 944 | } 945 | 946 | func testExtract() { 947 | struct MyError: Error {} 948 | 949 | XCTAssertEqual( 950 | [1], 951 | [Result.success(1), .success(nil), .failure(MyError())] 952 | .compactMap(/Result.success .. Optional.some) 953 | ) 954 | 955 | enum Authentication { 956 | case authenticated(token: String) 957 | case unauthenticated 958 | } 959 | 960 | XCTAssertEqual( 961 | ["deadbeef"], 962 | [Authentication.authenticated(token: "deadbeef"), .unauthenticated] 963 | .compactMap(/Authentication.authenticated) 964 | ) 965 | 966 | XCTAssertEqual( 967 | 1, 968 | [Authentication.authenticated(token: "deadbeef"), .unauthenticated] 969 | .compactMap(/Authentication.unauthenticated) 970 | .count 971 | ) 972 | 973 | enum Foo { case bar(Int, Int) } 974 | XCTAssertEqual( 975 | [3], 976 | [Foo.bar(1, 2)].compactMap(/Foo.bar).map(+) 977 | ) 978 | 979 | enum Case { 980 | case one(One) 981 | case none 982 | } 983 | enum One { 984 | case two(Two) 985 | } 986 | enum Two { 987 | case value(Int) 988 | } 989 | 990 | XCTAssertEqual( 991 | [1], 992 | [Case.one(.two(.value(1))), .none].compactMap(/Case.one .. One.two .. Two.value) 993 | ) 994 | } 995 | 996 | func testAppending() { 997 | let success = /Result.success 998 | let int = /Int?.some 999 | let success2int = success .. int 1000 | XCTAssertEqual( 1001 | .some(42), 1002 | success2int.extract(from: .success(.some(42))) 1003 | ) 1004 | } 1005 | 1006 | func testExample() { 1007 | XCTAssertEqual("Blob", (/Result.success).extract(from: .success("Blob"))) 1008 | XCTAssertNil((/Result.failure).extract(from: .success("Blob"))) 1009 | 1010 | XCTAssertEqual(42, (/Int??.some .. Int?.some).extract(from: Optional(Optional(42)))) 1011 | } 1012 | 1013 | func testConstantAnyCasePath() { 1014 | XCTAssertEqual(.some(42), AnyCasePath.constant(42).extract(from: ())) 1015 | XCTAssertNotNil(AnyCasePath.constant(42).embed(42)) 1016 | } 1017 | 1018 | func testNeverAnyCasePath() { 1019 | XCTAssertNil(AnyCasePath.never.extract(from: 42)) 1020 | } 1021 | 1022 | func testRawValuePath() { 1023 | enum Foo: String { case bar, baz } 1024 | 1025 | XCTAssertEqual(.some(.bar), AnyCasePath.rawValue.extract(from: "bar")) 1026 | XCTAssertEqual("baz", AnyCasePath.rawValue.embed(Foo.baz)) 1027 | } 1028 | 1029 | func testDescriptionPath() { 1030 | XCTAssertEqual(.some(42), AnyCasePath.description.extract(from: "42")) 1031 | XCTAssertEqual("42", AnyCasePath.description.embed(42)) 1032 | } 1033 | 1034 | func testA() { 1035 | enum EnumWithLabeledCase { 1036 | case labeled(label: Int, otherLabel: Int) 1037 | case labeled(Int, Int) 1038 | } 1039 | XCTAssertNil((/EnumWithLabeledCase.labeled(label:otherLabel:)).extract(from: .labeled(2, 2))) 1040 | XCTAssertNotNil( 1041 | (/EnumWithLabeledCase.labeled(label:otherLabel:)).extract( 1042 | from: .labeled(label: 2, otherLabel: 2))) 1043 | } 1044 | 1045 | func testPatternMatching() { 1046 | let results = [ 1047 | Result.success(1), 1048 | .success(2), 1049 | .failure(NSError(domain: "co.pointfree", code: -1)), 1050 | .success(3), 1051 | ] 1052 | XCTAssertEqual( 1053 | Array(results.lazy.prefix(while: { /Result.success ~= $0 }).compactMap(/Result.success)), 1054 | [1, 2] 1055 | ) 1056 | 1057 | switch results[0] { 1058 | case /Result.success: 1059 | break 1060 | default: 1061 | XCTFail() 1062 | } 1063 | } 1064 | 1065 | func testCustomStringConvertible() { 1066 | XCTAssertEqual( 1067 | "\(/Result.success)", 1068 | "AnyCasePath, String>" 1069 | ) 1070 | } 1071 | 1072 | func testOptionalInsideResult() { 1073 | let result: Result = .success("hello, world") 1074 | let path: AnyCasePath, String> = /Result.success 1075 | let actual = path.extract(from: result) 1076 | XCTAssertEqual( 1077 | actual, 1078 | "hello, world") 1079 | 1080 | XCTAssertNil((/Result.failure).extract(from: result)) 1081 | 1082 | let success: (Result) -> String? = /Result.success 1083 | XCTAssertEqual(success(result), "hello, world") 1084 | let failure: (Result) -> Error? = /Result.failure 1085 | XCTAssertNil(failure(result)) 1086 | } 1087 | 1088 | func testExtractFromOptionalRoot() { 1089 | enum Foo { 1090 | case foo(String) 1091 | case bar(String) 1092 | case baz 1093 | } 1094 | 1095 | var opt: Foo? = .foo("blob1") 1096 | XCTAssertEqual("blob1", (/Foo.foo).extract(from: opt)) 1097 | XCTAssertNil((/Foo.bar).extract(from: opt)) 1098 | XCTAssertNil((/Foo.baz).extract(from: opt)) 1099 | 1100 | opt = .bar("blob2") 1101 | XCTAssertNil((/Foo.foo).extract(from: opt)) 1102 | XCTAssertEqual("blob2", (/Foo.bar).extract(from: opt)) 1103 | XCTAssertNil((/Foo.baz).extract(from: opt)) 1104 | 1105 | opt = .baz 1106 | XCTAssertNil((/Foo.foo).extract(from: opt)) 1107 | XCTAssertNil((/Foo.bar).extract(from: opt)) 1108 | XCTAssertNotNil((/Foo.baz).extract(from: opt)) 1109 | 1110 | opt = nil 1111 | XCTAssertNil((/Foo.foo).extract(from: opt)) 1112 | XCTAssertNil((/Foo.bar).extract(from: opt)) 1113 | XCTAssertNil((/Foo.baz).extract(from: opt)) 1114 | 1115 | let extractExpression: (Foo?) -> String? = /Foo.foo 1116 | XCTAssertNotNil(extractExpression(.some(.foo("blob1")))) 1117 | XCTAssertNil(extractExpression(.some(.bar("blob2")))) 1118 | XCTAssertNil(extractExpression(.some(.baz))) 1119 | XCTAssertNil(extractExpression(nil)) 1120 | 1121 | let voidExtractExpression: (Foo?) -> Void? = /Foo.baz 1122 | XCTAssertNil(voidExtractExpression(.some(.foo("blob1")))) 1123 | XCTAssertNil(voidExtractExpression(.some(.bar("blob2")))) 1124 | XCTAssertNotNil(voidExtractExpression(.some(.baz))) 1125 | XCTAssertNil(voidExtractExpression(nil)) 1126 | } 1127 | 1128 | func testExtractFromOptionalRootWithEmbeddedTagBits() { 1129 | enum E { 1130 | case c1(TestObject) 1131 | case c2(TestObject) 1132 | } 1133 | 1134 | let o = TestObject() 1135 | let c1Path: AnyCasePath = /E.c1 1136 | let c2Path: AnyCasePath = /E.c2 1137 | 1138 | func check(_ path: AnyCasePath, _ input: E?, _ expected: TestObject?) { 1139 | let actual = path.extract(from: input) 1140 | XCTAssertEqual(actual, expected) 1141 | } 1142 | 1143 | for _ in 1...2 { 1144 | check(c1Path, nil, nil) 1145 | check(c1Path, .c1(o), o) 1146 | check(c1Path, .c2(o), nil) 1147 | check(c2Path, nil, nil) 1148 | check(c2Path, .c1(o), nil) 1149 | check(c2Path, .c2(o), o) 1150 | } 1151 | } 1152 | 1153 | func testExtractSuccessFromFailedResultWithErrorProtocolError() { 1154 | let path = /Result.success 1155 | 1156 | func check(_ error: Error) { 1157 | let result = Result.failure(error) 1158 | XCTAssertNil(path.extract(from: result)) 1159 | } 1160 | 1161 | struct EmptyError: Error { 1162 | } 1163 | 1164 | struct LittleError: Error { 1165 | var a = 42 1166 | } 1167 | 1168 | struct BigError: Error { 1169 | var a = "" 1170 | var b = "" 1171 | var c = "" 1172 | var d = "" 1173 | var e = "" 1174 | var f = "" 1175 | } 1176 | 1177 | for _ in 1...2 { 1178 | check(EmptyError()) 1179 | check(LittleError()) 1180 | check(BigError()) 1181 | XCTAssertEqual(path.extract(from: .success("hello")), "hello") 1182 | } 1183 | } 1184 | 1185 | func testExtractionFailureOfOptional() { 1186 | enum Action { 1187 | case child1(Child1) 1188 | case child2(Child2?) 1189 | } 1190 | enum Child1 { 1191 | case a 1192 | } 1193 | enum Child2 { 1194 | case b 1195 | } 1196 | 1197 | XCTAssertNil((/Action.child1).extract(from: .child2(.b))) 1198 | } 1199 | 1200 | func testModify() throws { 1201 | enum Foo: Equatable { case bar(Int) } 1202 | var foo = Foo.bar(42) 1203 | try (/Foo.bar).modify(&foo) { $0 *= 2 } 1204 | XCTAssertEqual(foo, .bar(84)) 1205 | } 1206 | 1207 | func testRegression_gh72() throws { 1208 | enum E1 { 1209 | case c1(E2) 1210 | } 1211 | 1212 | enum E2 { 1213 | case c1(Bool) 1214 | case c2(Bool) 1215 | case c3(Any) 1216 | } 1217 | 1218 | XCTAssertNotNil( 1219 | (/E1.c1).extract(from: .c1(.c1(true))) 1220 | ) 1221 | XCTAssertNotNil( 1222 | (/E1.c1).extract(from: .c1(.c2(true))) 1223 | ) 1224 | } 1225 | 1226 | #if os(Windows) || arch(wasm32) 1227 | // There seems to be some strangeness with the current 1228 | // concurrency implmentation on Windows that breaks if 1229 | // you have more than 100 tasks here. 1230 | // WebAssembly doesn't support tail-call for now, so we 1231 | // have smaller limit as well. 1232 | let maxIterations = 100 1233 | #else 1234 | let maxIterations = 100_000 1235 | #endif 1236 | 1237 | func testConcurrency_SharedCasePath() async throws { 1238 | enum Enum { case payload(Int) } 1239 | let casePath = /Enum.payload 1240 | 1241 | await withTaskGroup(of: Void.self) { group in 1242 | for index in 1...maxIterations { 1243 | group.addTask { 1244 | XCTAssertEqual(casePath.extract(from: Enum.payload(index)), index) 1245 | } 1246 | } 1247 | } 1248 | } 1249 | } 1250 | 1251 | private class TestObject: Equatable { 1252 | static func == (lhs: TestObject, rhs: TestObject) -> Bool { lhs === rhs } 1253 | } 1254 | 1255 | // Replace this with XCTUnwrap when we drop support for Xcode 11.3. 1256 | private func unwrap(_ optional: Wrapped?) throws -> Wrapped { 1257 | guard let wrapped = optional else { throw UnexpectedNil() } 1258 | return wrapped 1259 | } 1260 | private struct UnexpectedNil: Error {} 1261 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/DeprecatedXCTModifyTests.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) 2 | @_spi(Internals) import CasePaths 3 | import XCTest 4 | 5 | @available(*, deprecated) 6 | final class DeprecatedXCTModifyTests: XCTestCase { 7 | struct SomeError: Error, Equatable {} 8 | 9 | struct Sheet { 10 | struct State { 11 | var count = 0 12 | } 13 | } 14 | struct Destination { 15 | enum State { 16 | case alert 17 | case sheet(Sheet.State) 18 | } 19 | } 20 | 21 | func testXCTModifyFailure() throws { 22 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 23 | 24 | XCTExpectFailure { 25 | $0.compactDescription == """ 26 | failed - XCTModify: Expected to extract value of type "Int" from "Result" … 27 | 28 | Actual: 29 | failure(CasePathsTests.DeprecatedXCTModifyTests.SomeError()) 30 | """ 31 | } 32 | 33 | var result = Result.failure(SomeError()) 34 | XCTModify(&result, case: /Result.success) { 35 | $0 += 1 36 | } 37 | } 38 | 39 | func testXCTModifyFailure_OptionalPromotion() throws { 40 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 41 | 42 | XCTExpectFailure { 43 | $0.compactDescription == """ 44 | failed - XCTModify: Expected to extract value of type \ 45 | "DeprecatedXCTModifyTests.Sheet.State" from \ 46 | "DeprecatedXCTModifyTests.Destination.State?" … 47 | 48 | Actual: 49 | Optional(CasePathsTests.DeprecatedXCTModifyTests.Destination.State.alert) 50 | """ 51 | } 52 | 53 | var result = Optional(Destination.State.alert) 54 | XCTModify(&result, case: /Destination.State.sheet) { 55 | $0.count += 1 56 | } 57 | } 58 | 59 | func testXCTModifyFailure_Nil_OptionalPromotion() throws { 60 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 61 | 62 | XCTExpectFailure { 63 | $0.compactDescription == """ 64 | failed - XCTModify: Expected to extract value of type "Int" from \ 65 | "Optional>" … 66 | 67 | Actual: 68 | nil 69 | """ 70 | } 71 | 72 | var result = Optional>.none 73 | XCTModify(&result, case: /Result.success) { 74 | $0 += 1 75 | } 76 | } 77 | 78 | func testXCTModifyFailure_WithMessage() throws { 79 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 80 | 81 | XCTExpectFailure { 82 | $0.compactDescription == """ 83 | failed - XCTModify: Expected to extract value of type "Int" from "Result" - \ 84 | Should be success … 85 | 86 | Actual: 87 | failure(CasePathsTests.DeprecatedXCTModifyTests.SomeError()) 88 | """ 89 | } 90 | 91 | var result = Result.failure(SomeError()) 92 | XCTModify(&result, case: /Result.success, "Should be success") { 93 | $0 += 1 94 | } 95 | } 96 | 97 | func testXCTModifyPass() throws { 98 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 99 | 100 | var result = Result.success(2) 101 | XCTModify(&result, case: /Result.success) { 102 | $0 += 1 103 | } 104 | XCTAssertEqual(result, .success(3)) 105 | } 106 | 107 | func testXCTModifyPass_OptionalPromotion() throws { 108 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 109 | 110 | var result = Optional(Result.success(2)) 111 | XCTModify(&result, case: /Result.success) { 112 | $0 += 1 113 | } 114 | XCTAssertEqual(result, .success(3)) 115 | } 116 | 117 | func testXCTModifyFailUnchangedEquatable() throws { 118 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 119 | 120 | XCTExpectFailure { 121 | $0.compactDescription == """ 122 | failed - XCTModify: Expected "Int" value to be modified but it was unchanged. 123 | """ 124 | } 125 | 126 | var result = Result.success(2) 127 | XCTModify(&result, case: /Result.success) { 128 | _ = $0 129 | } 130 | XCTAssertEqual(result, .success(2)) 131 | } 132 | 133 | func testXCTModify_BodyThrowsError() throws { 134 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 135 | 136 | XCTExpectFailure { 137 | $0.compactDescription == """ 138 | failed - Threw error: SomeError() 139 | """ 140 | } 141 | 142 | var result = Result.success(2) 143 | XCTModify(&result, case: /Result.success) { _ in 144 | throw SomeError() 145 | } 146 | XCTAssertEqual(result, .success(2)) 147 | } 148 | 149 | func testXCTModifyFailUnchangedEquatable_NonExhaustive() throws { 150 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 151 | 152 | var result = Result.success(2) 153 | XCTModifyLocals.$isExhaustive.withValue(false) { 154 | XCTModify(&result, case: /Result.success) { 155 | _ = $0 156 | } 157 | } 158 | XCTAssertEqual(result, .success(2)) 159 | } 160 | } 161 | #endif 162 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/MacroTests.swift: -------------------------------------------------------------------------------- 1 | import CasePaths 2 | 3 | @CasePathable 4 | private enum Comments { 5 | // Comment above case 6 | case bar 7 | /*Comment before case*/ case baz(Int) 8 | case fizz(buzz: String) // Comment on case 9 | case fizzier /*Comment in case*/(Int, buzzier: String) 10 | case fizziest 11 | } 12 | 13 | @CasePathable 14 | enum Action { 15 | case alert(Alert) 16 | } 17 | 18 | @CasePathable 19 | enum Alert { 20 | case alert(Never) 21 | } 22 | 23 | @CasePathable enum EnumWithElementGeneric { 24 | case element(Element) 25 | } 26 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/ReflectionTests.swift: -------------------------------------------------------------------------------- 1 | @_spi(Reflection) import CasePaths 2 | import XCTest 3 | 4 | final class ReflectionTests: XCTestCase { 5 | func testProject() throws { 6 | struct MyIdentifiable: Identifiable { 7 | let id = 42 8 | } 9 | let success = Result.success(MyIdentifiable()) 10 | let anyIdentifiable = try XCTUnwrap(EnumMetadata.project(success) as? any Identifiable) 11 | func id(of identifiable: some Identifiable) -> AnyHashable { 12 | identifiable.id 13 | } 14 | XCTAssertEqual(42, id(of: anyIdentifiable)) 15 | } 16 | 17 | func testProject_Existential() throws { 18 | struct MyIdentifiable: Identifiable { 19 | let id = 42 20 | } 21 | let success = Result.success(MyIdentifiable()) 22 | let anyIdentifiable = try XCTUnwrap(EnumMetadata.project(success) as? any Identifiable) 23 | func id(of identifiable: some Identifiable) -> AnyHashable { 24 | identifiable.id 25 | } 26 | XCTAssertEqual(42, id(of: anyIdentifiable)) 27 | } 28 | 29 | func testProject_Indirect() throws { 30 | struct MyIdentifiable: Identifiable { 31 | let id = 42 32 | } 33 | indirect enum Enum { 34 | case indirectCase(MyIdentifiable) 35 | } 36 | let indirect = Enum.indirectCase(MyIdentifiable()) 37 | let anyIdentifiable = try XCTUnwrap(EnumMetadata.project(indirect) as? any Identifiable) 38 | func id(of identifiable: some Identifiable) -> AnyHashable { 39 | identifiable.id 40 | } 41 | XCTAssertEqual(42, id(of: anyIdentifiable)) 42 | } 43 | 44 | func testProject_NoPayload() throws { 45 | enum Enum { 46 | case noPayload 47 | } 48 | let value = EnumMetadata.project(Enum.noPayload) 49 | try XCTUnwrap(EnumMetadata.project(value) as? Void) 50 | } 51 | 52 | func testLabel() throws { 53 | enum Enum: Equatable { 54 | case label(id: Int) 55 | case multiLabel(id: Int, name: String) 56 | } 57 | 58 | XCTAssertEqual(EnumMetadata.project(Enum.label(id: 42)) as? Int, 42) 59 | let pair = try XCTUnwrap( 60 | EnumMetadata.project(Enum.multiLabel(id: 42, name: "Blob")) as? (Int, String) 61 | ) 62 | XCTAssert(pair == (42, "Blob")) 63 | } 64 | 65 | func testCompound() throws { 66 | let object = Object() 67 | enum Enum: Equatable { 68 | indirect case indirect(Int, Object?, Int, Object?) 69 | case direct(Int, Object?, Int, Object?) 70 | } 71 | 72 | let indirect = try XCTUnwrap( 73 | EnumMetadata.project(Enum.indirect(42, nil, 43, object)) 74 | as? (Int, Object?, Int, Object?) 75 | ) 76 | XCTAssert(indirect == (42, nil, 43, object)) 77 | } 78 | } 79 | 80 | private class Object: Equatable { 81 | static func == (lhs: Object, rhs: Object) -> Bool { 82 | return lhs === rhs 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/XCTModifyTests.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) 2 | @_spi(Internals) import CasePaths 3 | import XCTest 4 | 5 | final class XCTModifyTests: XCTestCase { 6 | struct SomeError: Error, Equatable {} 7 | 8 | struct Sheet { 9 | struct State { 10 | var count = 0 11 | } 12 | } 13 | struct Destination { 14 | @CasePathable 15 | enum State { 16 | case alert 17 | case sheet(Sheet.State) 18 | } 19 | } 20 | 21 | func testXCTModifyFailure() throws { 22 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 23 | 24 | XCTExpectFailure { 25 | $0.compactDescription == """ 26 | failed - XCTModify: Expected to extract value of type "Int" from "Result" … 27 | 28 | Actual: 29 | failure(CasePathsTests.XCTModifyTests.SomeError()) 30 | """ 31 | } 32 | 33 | var result = Result.failure(SomeError()) 34 | XCTModify(&result, case: \.success) { 35 | $0 += 1 36 | } 37 | } 38 | 39 | func testXCTModifyFailure_OptionalPromotion() throws { 40 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 41 | 42 | XCTExpectFailure { 43 | $0.compactDescription == """ 44 | failed - XCTModify: Expected to extract value of type "XCTModifyTests.Sheet.State" from \ 45 | "XCTModifyTests.Destination.State?" … 46 | 47 | Actual: 48 | Optional(CasePathsTests.XCTModifyTests.Destination.State.alert) 49 | """ 50 | } 51 | 52 | var result = Optional(Destination.State.alert) 53 | XCTModify(&result, case: \.some.sheet) { 54 | $0.count += 1 55 | } 56 | } 57 | 58 | func testXCTModifyFailure_Nil_OptionalPromotion() throws { 59 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 60 | 61 | XCTExpectFailure { 62 | $0.compactDescription == """ 63 | failed - XCTModify: Expected to extract value of type "Int" from \ 64 | "Optional>" … 65 | 66 | Actual: 67 | nil 68 | """ 69 | } 70 | 71 | var result = Optional>.none 72 | XCTModify(&result, case: \.some.success) { 73 | $0 += 1 74 | } 75 | } 76 | 77 | func testXCTModifyFailure_WithMessage() throws { 78 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 79 | 80 | XCTExpectFailure { 81 | $0.compactDescription == """ 82 | failed - XCTModify: Expected to extract value of type "Int" from "Result" - \ 83 | Should be success … 84 | 85 | Actual: 86 | failure(CasePathsTests.XCTModifyTests.SomeError()) 87 | """ 88 | } 89 | 90 | var result = Result.failure(SomeError()) 91 | XCTModify(&result, case: \.success, "Should be success") { 92 | $0 += 1 93 | } 94 | } 95 | 96 | func testXCTModifyPass() throws { 97 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 98 | 99 | var result = Result.success(2) 100 | XCTModify(&result, case: \.success) { 101 | $0 += 1 102 | } 103 | XCTAssertEqual(result, .success(3)) 104 | } 105 | 106 | func testXCTModifyPass_OptionalPromotion() throws { 107 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 108 | 109 | var result = Optional(Result.success(2)) 110 | XCTModify(&result, case: \.some.success) { 111 | $0 += 1 112 | } 113 | XCTAssertEqual(result, .success(3)) 114 | } 115 | 116 | func testXCTModifyFailUnchangedEquatable() throws { 117 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 118 | 119 | XCTExpectFailure { 120 | $0.compactDescription == """ 121 | failed - XCTModify: Expected "Int" value to be modified but it was unchanged. 122 | """ 123 | } 124 | 125 | var result = Result.success(2) 126 | XCTModify(&result, case: \.success) { 127 | _ = $0 128 | } 129 | XCTAssertEqual(result, .success(2)) 130 | } 131 | 132 | func testXCTModify_BodyThrowsError() throws { 133 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 134 | 135 | XCTExpectFailure { 136 | $0.compactDescription == """ 137 | failed - Threw error: SomeError() 138 | """ 139 | } 140 | 141 | var result = Result.success(2) 142 | XCTModify(&result, case: \.success) { _ in 143 | throw SomeError() 144 | } 145 | XCTAssertEqual(result, .success(2)) 146 | } 147 | 148 | func testXCTModifyFailUnchangedEquatable_NonExhaustive() throws { 149 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 150 | 151 | var result = Result.success(2) 152 | XCTModifyLocals.$isExhaustive.withValue(false) { 153 | XCTModify(&result, case: \.success) { 154 | _ = $0 155 | } 156 | } 157 | XCTAssertEqual(result, .success(2)) 158 | } 159 | } 160 | #endif 161 | -------------------------------------------------------------------------------- /Tests/CasePathsTests/XCTUnwrapTests.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) 2 | import CasePaths 3 | import XCTest 4 | 5 | @available(*, deprecated) 6 | final class XCTUnwrapTests: XCTestCase { 7 | func testXCTUnwrapFailure() throws { 8 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 9 | 10 | XCTExpectFailure { 11 | $0.compactDescription == """ 12 | failed - XCTUnwrap: Expected to extract value of type "Error" from "Result" … 13 | 14 | Actual: 15 | success(2) 16 | """ 17 | } 18 | _ = try XCTUnwrap(Result.success(2), case: /Result.failure) 19 | } 20 | 21 | func testXCTUnwrapFailure_WithMessage() throws { 22 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 23 | 24 | XCTExpectFailure { 25 | $0.compactDescription == """ 26 | failed - XCTUnwrap: Expected to extract value of type "Error" from "Result" - \ 27 | Should be 'failure' … 28 | 29 | Actual: 30 | success(2) 31 | """ 32 | } 33 | _ = try XCTUnwrap(Result.success(2), case: /Result.failure, "Should be 'failure'") 34 | } 35 | 36 | func testXCTUnwrapPass() throws { 37 | try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) 38 | 39 | XCTAssertEqual( 40 | try XCTUnwrap(Result.success(2), case: /Result.success), 41 | 2 42 | ) 43 | } 44 | } 45 | #endif 46 | --------------------------------------------------------------------------------