├── .github └── workflows │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .mergify.yml ├── .scalafmt.conf ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── core └── src │ ├── main │ └── scala │ │ └── org │ │ └── typelevel │ │ └── vault │ │ ├── InvariantMapping.scala │ │ ├── Key.scala │ │ ├── Locker.scala │ │ └── Vault.scala │ └── test │ └── scala │ └── org │ └── typelevel │ └── vault │ ├── KeySuite.scala │ └── VaultSuite.scala ├── docs └── index.md ├── licenses └── LICENSE_vault └── project ├── build.properties └── plugins.sbt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**', '!update/**', '!pr/**'] 13 | push: 14 | branches: ['**', '!update/**', '!pr/**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | 21 | concurrency: 22 | group: ${{ github.workflow }} @ ${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | build: 27 | name: Test 28 | strategy: 29 | matrix: 30 | os: [ubuntu-22.04] 31 | scala: [2.12, 3, 2.13] 32 | java: [temurin@8, temurin@17] 33 | project: [rootJS, rootJVM, rootNative] 34 | exclude: 35 | - scala: 2.12 36 | java: temurin@17 37 | - scala: 3 38 | java: temurin@17 39 | - project: rootJS 40 | java: temurin@17 41 | - project: rootNative 42 | java: temurin@17 43 | runs-on: ${{ matrix.os }} 44 | timeout-minutes: 60 45 | steps: 46 | - name: Checkout current branch (full) 47 | uses: actions/checkout@v4 48 | with: 49 | fetch-depth: 0 50 | 51 | - name: Setup sbt 52 | uses: sbt/setup-sbt@v1 53 | 54 | - name: Setup Java (temurin@8) 55 | id: setup-java-temurin-8 56 | if: matrix.java == 'temurin@8' 57 | uses: actions/setup-java@v4 58 | with: 59 | distribution: temurin 60 | java-version: 8 61 | cache: sbt 62 | 63 | - name: sbt update 64 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 65 | run: sbt +update 66 | 67 | - name: Setup Java (temurin@17) 68 | id: setup-java-temurin-17 69 | if: matrix.java == 'temurin@17' 70 | uses: actions/setup-java@v4 71 | with: 72 | distribution: temurin 73 | java-version: 17 74 | cache: sbt 75 | 76 | - name: sbt update 77 | if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' 78 | run: sbt +update 79 | 80 | - name: Check that workflows are up to date 81 | run: sbt githubWorkflowCheck 82 | 83 | - name: Check headers and formatting 84 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 85 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck 86 | 87 | - name: scalaJSLink 88 | if: matrix.project == 'rootJS' 89 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult 90 | 91 | - name: nativeLink 92 | if: matrix.project == 'rootNative' 93 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/nativeLink 94 | 95 | - name: Test 96 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test 97 | 98 | - name: Check binary compatibility 99 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 100 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues 101 | 102 | - name: Generate API documentation 103 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 104 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc 105 | 106 | - name: Make target directories 107 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 108 | run: mkdir -p core/.native/target core/.js/target core/.jvm/target project/target 109 | 110 | - name: Compress target directories 111 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 112 | run: tar cf targets.tar core/.native/target core/.js/target core/.jvm/target project/target 113 | 114 | - name: Upload target directories 115 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }} 119 | path: targets.tar 120 | 121 | publish: 122 | name: Publish Artifacts 123 | needs: [build] 124 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 125 | strategy: 126 | matrix: 127 | os: [ubuntu-22.04] 128 | java: [temurin@8] 129 | runs-on: ${{ matrix.os }} 130 | steps: 131 | - name: Checkout current branch (full) 132 | uses: actions/checkout@v4 133 | with: 134 | fetch-depth: 0 135 | 136 | - name: Setup sbt 137 | uses: sbt/setup-sbt@v1 138 | 139 | - name: Setup Java (temurin@8) 140 | id: setup-java-temurin-8 141 | if: matrix.java == 'temurin@8' 142 | uses: actions/setup-java@v4 143 | with: 144 | distribution: temurin 145 | java-version: 8 146 | cache: sbt 147 | 148 | - name: sbt update 149 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 150 | run: sbt +update 151 | 152 | - name: Setup Java (temurin@17) 153 | id: setup-java-temurin-17 154 | if: matrix.java == 'temurin@17' 155 | uses: actions/setup-java@v4 156 | with: 157 | distribution: temurin 158 | java-version: 17 159 | cache: sbt 160 | 161 | - name: sbt update 162 | if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' 163 | run: sbt +update 164 | 165 | - name: Download target directories (2.12, rootJS) 166 | uses: actions/download-artifact@v4 167 | with: 168 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJS 169 | 170 | - name: Inflate target directories (2.12, rootJS) 171 | run: | 172 | tar xf targets.tar 173 | rm targets.tar 174 | 175 | - name: Download target directories (2.12, rootJVM) 176 | uses: actions/download-artifact@v4 177 | with: 178 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJVM 179 | 180 | - name: Inflate target directories (2.12, rootJVM) 181 | run: | 182 | tar xf targets.tar 183 | rm targets.tar 184 | 185 | - name: Download target directories (2.12, rootNative) 186 | uses: actions/download-artifact@v4 187 | with: 188 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootNative 189 | 190 | - name: Inflate target directories (2.12, rootNative) 191 | run: | 192 | tar xf targets.tar 193 | rm targets.tar 194 | 195 | - name: Download target directories (3, rootJS) 196 | uses: actions/download-artifact@v4 197 | with: 198 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS 199 | 200 | - name: Inflate target directories (3, rootJS) 201 | run: | 202 | tar xf targets.tar 203 | rm targets.tar 204 | 205 | - name: Download target directories (3, rootJVM) 206 | uses: actions/download-artifact@v4 207 | with: 208 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM 209 | 210 | - name: Inflate target directories (3, rootJVM) 211 | run: | 212 | tar xf targets.tar 213 | rm targets.tar 214 | 215 | - name: Download target directories (3, rootNative) 216 | uses: actions/download-artifact@v4 217 | with: 218 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative 219 | 220 | - name: Inflate target directories (3, rootNative) 221 | run: | 222 | tar xf targets.tar 223 | rm targets.tar 224 | 225 | - name: Download target directories (2.13, rootJS) 226 | uses: actions/download-artifact@v4 227 | with: 228 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS 229 | 230 | - name: Inflate target directories (2.13, rootJS) 231 | run: | 232 | tar xf targets.tar 233 | rm targets.tar 234 | 235 | - name: Download target directories (2.13, rootJVM) 236 | uses: actions/download-artifact@v4 237 | with: 238 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJVM 239 | 240 | - name: Inflate target directories (2.13, rootJVM) 241 | run: | 242 | tar xf targets.tar 243 | rm targets.tar 244 | 245 | - name: Download target directories (2.13, rootNative) 246 | uses: actions/download-artifact@v4 247 | with: 248 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootNative 249 | 250 | - name: Inflate target directories (2.13, rootNative) 251 | run: | 252 | tar xf targets.tar 253 | rm targets.tar 254 | 255 | - name: Import signing key 256 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' 257 | env: 258 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 259 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 260 | run: echo $PGP_SECRET | base64 -d -i - | gpg --import 261 | 262 | - name: Import signing key and strip passphrase 263 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' 264 | env: 265 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 266 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 267 | run: | 268 | echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg 269 | echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg 270 | (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) 271 | 272 | - name: Publish 273 | env: 274 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 275 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 276 | SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} 277 | run: sbt tlCiRelease 278 | 279 | dependency-submission: 280 | name: Submit Dependencies 281 | if: github.event.repository.fork == false && github.event_name != 'pull_request' 282 | strategy: 283 | matrix: 284 | os: [ubuntu-22.04] 285 | java: [temurin@8] 286 | runs-on: ${{ matrix.os }} 287 | steps: 288 | - name: Checkout current branch (full) 289 | uses: actions/checkout@v4 290 | with: 291 | fetch-depth: 0 292 | 293 | - name: Setup sbt 294 | uses: sbt/setup-sbt@v1 295 | 296 | - name: Setup Java (temurin@8) 297 | id: setup-java-temurin-8 298 | if: matrix.java == 'temurin@8' 299 | uses: actions/setup-java@v4 300 | with: 301 | distribution: temurin 302 | java-version: 8 303 | cache: sbt 304 | 305 | - name: sbt update 306 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 307 | run: sbt +update 308 | 309 | - name: Setup Java (temurin@17) 310 | id: setup-java-temurin-17 311 | if: matrix.java == 'temurin@17' 312 | uses: actions/setup-java@v4 313 | with: 314 | distribution: temurin 315 | java-version: 17 316 | cache: sbt 317 | 318 | - name: sbt update 319 | if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' 320 | run: sbt +update 321 | 322 | - name: Submit Dependencies 323 | uses: scalacenter/sbt-dependency-submission@v2 324 | with: 325 | modules-ignore: rootjs_2.12 rootjs_3 rootjs_2.13 docs_2.12 docs_3 docs_2.13 rootjvm_2.12 rootjvm_3 rootjvm_2.13 rootnative_2.12 rootnative_3 rootnative_2.13 326 | configs-ignore: test scala-tool scala-doc-tool test-internal 327 | 328 | site: 329 | name: Generate Site 330 | strategy: 331 | matrix: 332 | os: [ubuntu-22.04] 333 | java: [temurin@17] 334 | runs-on: ${{ matrix.os }} 335 | steps: 336 | - name: Checkout current branch (full) 337 | uses: actions/checkout@v4 338 | with: 339 | fetch-depth: 0 340 | 341 | - name: Setup sbt 342 | uses: sbt/setup-sbt@v1 343 | 344 | - name: Setup Java (temurin@8) 345 | id: setup-java-temurin-8 346 | if: matrix.java == 'temurin@8' 347 | uses: actions/setup-java@v4 348 | with: 349 | distribution: temurin 350 | java-version: 8 351 | cache: sbt 352 | 353 | - name: sbt update 354 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 355 | run: sbt +update 356 | 357 | - name: Setup Java (temurin@17) 358 | id: setup-java-temurin-17 359 | if: matrix.java == 'temurin@17' 360 | uses: actions/setup-java@v4 361 | with: 362 | distribution: temurin 363 | java-version: 17 364 | cache: sbt 365 | 366 | - name: sbt update 367 | if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' 368 | run: sbt +update 369 | 370 | - name: Generate site 371 | run: sbt docs/tlSite 372 | 373 | - name: Publish site 374 | if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' 375 | uses: peaceiris/actions-gh-pages@v4.0.0 376 | with: 377 | github_token: ${{ secrets.GITHUB_TOKEN }} 378 | publish_dir: site/target/docs/site 379 | keep_files: true 380 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | # vim 4 | *.sw? 5 | 6 | # Ignore [ce]tags files 7 | tags 8 | 9 | .bloop 10 | .metals 11 | .vscode 12 | .bsp 13 | .jekyll-cache/ 14 | 15 | # metals 16 | metals.sbt -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatically merge scala-steward's PRs 3 | conditions: 4 | - author=scala-steward 5 | - status-success=Travis CI - Pull Request 6 | - body~=labels:.*semver-patch.* 7 | actions: 8 | merge: 9 | method: merge 10 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=3.9.4 2 | runner.dialect = scala213source3 3 | align.openParenCallSite = true 4 | align.openParenDefnSite = true 5 | maxColumn = 120 6 | continuationIndent.defnSite = 2 7 | assumeStandardLibraryStripMargin = true 8 | danglingParentheses.preset = true 9 | rewrite.rules = [AvoidInfix, SortImports, RedundantParens, SortModifiers] 10 | newlines.afterCurlyLambda = preserve 11 | docstrings.style = Asterisk 12 | docstrings.oneline = unfold 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | This file summarizes **notable** changes for each release, but does not describe internal changes unless they are particularly exciting. This change log is ordered chronologically, so each release contains all changes described below it. 4 | 5 | ---- 6 | 7 | ## Unreleased Changes 8 | 9 | ## New and Noteworthy for Version 3.0.0-RC1 10 | 11 | * Built for cats-effect-3.0.0-RC1 12 | * Uses Cats-Effect-3's `Unique` instead of our internal one 13 | 14 | ## New and Noteworthy for Version 3.0.0-M1 15 | 16 | * Built for cats-effect-3.0.0-M5 17 | * Internal, private copy of Unique 18 | 19 | ## New and Noteworthy for Version 2.1.7 20 | 21 | * Add support for Dotty 3.0.0-RC1 22 | * Drop support for Dotty 3.0.0-M2 23 | 24 | ## New and Noteworthy for Version 2.1.0 25 | 26 | * cats-2.4.2 27 | * cats-effect-2.3.3 28 | * unique-2.1.1 29 | 30 | ## New and Noteworthy for Version 2.1.0-M2 31 | 32 | * Now publishes under `org.typelevel` 33 | * Package renamed to `org.typelevel.vault` 34 | * Support for Dotty 3.0.0-M2 and 3.0.0-M3 35 | 36 | ## New and Noteworthy for Version 1.0.0 37 | 38 | Stable Release of `vault`. This library will maintain binary compatibility moving forward for the forseeable future. 39 | 40 | - [#39](https://github.com/ChristopherDavenport/vault/pull/39) Scala 2.13 Integration 41 | 42 | Upgrades: 43 | 44 | - cats 1.6.0 45 | - cats-effect 1.2.0 46 | - unique 1.0.0 47 | 48 | ## New and Noteworthy for Version 0.1.0 49 | 50 | Baseline Vault implementation for an implementation of a type-safe, persistent storage of values of arbitrary types. Dependencies on `cats` 1.x, `cats-effect` 1.x, and `unique` 0.1.x. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Christopher Davenport 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | vault 2 | Copyright 2018 Christopher Davenport 3 | Licensed under the MIT License (see LICENSE) 4 | 5 | This software contains portions of code derived from HeinrichApfelmus/vault 6 | https://github.com/HeinrichApfelmus/vault 7 | Copyright (c)2011, Heinrich Apfelmus 8 | Licensed under BSD3 (see licenses/LICENSE_vault) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vault [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.typelevel/vault_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.typelevel/vault_2.12) ![Continuous Integration](https://github.com/typelevel/vault/workflows/Continuous%20Integration/badge.svg) 2 | 3 | Vault is a tiny library that provides a single data structure called vault. 4 | 5 | Inspiration was drawn from [HeinrichApfelmus/vault](https://github.com/HeinrichApfelmus/vault) and the original [blog post](https://apfelmus.nfshost.com/blog/2011/09/04-vault.html) 6 | 7 | A vault is a type-safe, persistent storage for values of arbitrary types. Like `Ref`, it should be capable of storing values of any type in it, but unlike `Ref`, behave like a persistent, first-class data structure. 8 | 9 | It is analogous to a bank vault, where you can access different bank boxes with different keys; hence the name. 10 | 11 | ## Microsite 12 | 13 | Head on over [to the microsite](https://typelevel.org/vault/) 14 | 15 | ## Quick Start 16 | 17 | To use vault in an existing SBT project with Scala 2.12 or a later version, add the following dependencies to your 18 | `build.sbt` depending on your needs: 19 | 20 | ```scala 21 | libraryDependencies ++= Seq( 22 | "org.typelevel" %% "vault" % "" 23 | ) 24 | ``` 25 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val Scala212 = "2.12.20" 2 | val Scala213 = "2.13.16" 3 | val Scala3 = "3.3.6" 4 | 5 | ThisBuild / tlBaseVersion := "3.6" 6 | ThisBuild / crossScalaVersions := Seq(Scala212, Scala3, Scala213) 7 | ThisBuild / tlVersionIntroduced := Map("3" -> "3.0.3") 8 | ThisBuild / tlMimaPreviousVersions ~= (_.filterNot(_ == "3.2.0")) 9 | ThisBuild / licenses := List("MIT" -> url("http://opensource.org/licenses/MIT")) 10 | ThisBuild / startYear := Some(2021) 11 | ThisBuild / tlSiteApiUrl := Some(url("https://www.javadoc.io/doc/org.typelevel/vault_2.13/latest/org/typelevel/vault/")) 12 | 13 | ThisBuild / developers := List( 14 | tlGitHubDev("christopherdavenport", "Christopher Davenport") 15 | ) 16 | 17 | val JDK8 = JavaSpec.temurin("8") 18 | val JDK17 = JavaSpec.temurin("17") 19 | 20 | ThisBuild / githubWorkflowJavaVersions := Seq(JDK8, JDK17) 21 | 22 | lazy val root = tlCrossRootProject.aggregate(core) 23 | 24 | lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) 25 | .crossType(CrossType.Pure) 26 | .in(file("core")) 27 | .settings( 28 | name := "vault", 29 | libraryDependencies ++= Seq( 30 | "org.typelevel" %%% "cats-core" % catsV, 31 | "org.typelevel" %%% "cats-effect" % catsEffectV, 32 | "org.typelevel" %%% "cats-laws" % catsV % Test, 33 | "org.typelevel" %%% "discipline-munit" % disciplineMunitV % Test, 34 | "org.typelevel" %%% "scalacheck-effect-munit" % scalacheckEffectV % Test, 35 | "org.typelevel" %%% "munit-cats-effect" % munitCatsEffectV % Test 36 | ) 37 | ) 38 | .nativeSettings( 39 | tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "3.2.2").toMap 40 | ) 41 | 42 | lazy val docs = project 43 | .in(file("site")) 44 | .settings(tlFatalWarnings := false) 45 | .dependsOn(core.jvm) 46 | .enablePlugins(TypelevelSitePlugin) 47 | 48 | val catsV = "2.11.0" 49 | val catsEffectV = "3.6.1" 50 | val disciplineMunitV = "2.0.0-M3" 51 | val scalacheckEffectV = "2.0.0-M2" 52 | val munitCatsEffectV = "2.1.0" 53 | val kindProjectorV = "0.13.3" 54 | 55 | // Scalafmt 56 | addCommandAlias("fmt", "; Compile / scalafmt; Test / scalafmt; scalafmtSbt") 57 | addCommandAlias("fmtCheck", "; Compile / scalafmtCheck; Test / scalafmtCheck; scalafmtSbtCheck") 58 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/vault/InvariantMapping.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package org.typelevel.vault 23 | 24 | import cats.data.AndThen 25 | 26 | private[vault] trait InvariantMapping[A] { outer => 27 | type I 28 | def in: A => I 29 | def out: I => A 30 | def imap[B](f: A => B)(g: B => A): InvariantMapping[B] = 31 | new InvariantMapping[B] { 32 | type I = outer.I 33 | val in = AndThen(g).andThen(outer.in) 34 | val out = AndThen(outer.out).andThen(f) 35 | } 36 | } 37 | 38 | private[vault] object InvariantMapping { 39 | def id[A]: InvariantMapping[A] = 40 | new InvariantMapping[A] { 41 | type I = A 42 | val in = identity 43 | val out = identity 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/vault/Key.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package org.typelevel.vault 23 | 24 | import cats.Contravariant 25 | import cats.Functor 26 | import cats.effect.kernel.Unique 27 | import cats.Hash 28 | import cats.implicits._ 29 | import cats.Invariant 30 | import cats.data.AndThen 31 | 32 | /** 33 | * A unique value tagged with a specific type to that unique. Since it can only be created as a result of that, it links 34 | * a Unique identifier to a type known by the compiler. 35 | */ 36 | final class Key[A] private ( 37 | private[vault] val unique: Unique.Token, 38 | private[vault] val imapping: InvariantMapping[A] 39 | ) extends InsertKey[A] 40 | with LookupKey[A] { 41 | 42 | // Delegates, for convenience. 43 | private[vault] type I = imapping.I 44 | private[vault] val in = imapping.in 45 | private[vault] val out = imapping.out 46 | 47 | // Overloaded ctor to preserve bincompat 48 | def this(unique: Unique.Token) = 49 | this(unique, InvariantMapping.id[A]) 50 | 51 | /** 52 | * Create a copy of this key that references the same underlying vault element, transformed from type `B` before 53 | * insert, and to `B` after lookup. 54 | */ 55 | def imap[B](f: A => B)(g: B => A): Key[B] = 56 | new Key(unique, imapping.imap(f)(g)) 57 | 58 | override def hashCode(): Int = unique.hashCode() 59 | 60 | } 61 | 62 | sealed trait DeleteKey { 63 | private[vault] def unique: Unique.Token 64 | } 65 | 66 | sealed trait InsertKey[-A] extends DeleteKey { outer => 67 | private[vault] type I 68 | private[vault] def in: A => I 69 | 70 | def contramap[B](f: B => A): InsertKey[B] = 71 | new InsertKey[B] { 72 | val unique = outer.unique 73 | type I = outer.I 74 | val in = AndThen(f).andThen(outer.in) 75 | } 76 | } 77 | 78 | object InsertKey { 79 | implicit val ContravariantInsertKey: Contravariant[InsertKey] = 80 | new Contravariant[InsertKey] { 81 | def contramap[A, B](fa: InsertKey[A])(f: B => A): InsertKey[B] = 82 | fa.contramap(f) 83 | } 84 | } 85 | 86 | sealed trait LookupKey[+A] extends DeleteKey { outer => 87 | private[vault] def unique: Unique.Token 88 | private[vault] type I 89 | private[vault] def out: I => A 90 | 91 | def map[B](f: A => B): LookupKey[B] = 92 | new LookupKey[B] { 93 | val unique = outer.unique 94 | type I = outer.I 95 | val out = AndThen(outer.out).andThen(f) 96 | } 97 | } 98 | 99 | object LookupKey { 100 | implicit val FunctorLookupKey: Functor[LookupKey] = 101 | new Functor[LookupKey] { 102 | def map[A, B](fa: LookupKey[A])(f: A => B): LookupKey[B] = 103 | fa.map(f) 104 | } 105 | } 106 | 107 | object Key { 108 | 109 | /** 110 | * Create A Typed Key 111 | */ 112 | def newKey[F[_]: Functor: Unique, A]: F[Key[A]] = Unique[F].unique.map(new Key[A](_)) 113 | 114 | implicit def keyInstances[A]: Hash[Key[A]] = new Hash[Key[A]] { 115 | // Members declared in cats.kernel.Eq 116 | def eqv(x: Key[A], y: Key[A]): Boolean = 117 | x.unique === y.unique 118 | 119 | // Members declared in cats.kernel.Hash 120 | def hash(x: Key[A]): Int = Hash[Unique.Token].hash(x.unique) 121 | } 122 | 123 | implicit val InvariantKey: Invariant[Key] = 124 | new Invariant[Key] { 125 | def imap[A, B](fa: Key[A])(f: A => B)(g: B => A): Key[B] = 126 | fa.imap(f)(g) 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/vault/Locker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package org.typelevel.vault 23 | 24 | import cats.implicits._ 25 | import cats.effect.kernel.Unique 26 | 27 | /** 28 | * Locker - A persistent store for a single value. This utilizes the fact that a unique is linked to a type. Since the 29 | * key is linked to a type, then we can cast the value to Any, and join it to the Unique. Then if we are then asked to 30 | * unlock this locker with the same unique, we know that the type MUST be the type of the Key, so we can bring it back 31 | * as that type safely. 32 | */ 33 | final class Locker private (private val unique: Unique.Token, private val a: Any) { 34 | 35 | /** 36 | * Retrieve the value from the Locker. If the reference equality instance backed by a `Unique` value is the same then 37 | * allows conversion to that type, otherwise as it does not match then this will be `None` 38 | * 39 | * @param k 40 | * The key to check, if the internal Unique value matches then this Locker can be unlocked as the specifed value 41 | */ 42 | def unlock[A](k: LookupKey[A]): Option[A] = 43 | if (k.unique === unique) Some(k.out(a.asInstanceOf[k.I])) 44 | else None 45 | 46 | /** 47 | * Retrieve the value from the Locker. If the reference equality instance backed by a `Unique` value is the same then 48 | * allows conversion to that type, otherwise as it does not match then this will be `None` 49 | * 50 | * @param k 51 | * The key to check, if the internal Unique value matches then this Locker can be unlocked as the specifed value 52 | */ 53 | private[vault] def unlock[A](k: Key[A]): Option[A] = unlock(k: LookupKey[A]) 54 | } 55 | 56 | object Locker { 57 | 58 | /** 59 | * Put a single value into a Locker 60 | */ 61 | def apply[A](k: InsertKey[A], a: A): Locker = new Locker(k.unique, k.in(a)) 62 | 63 | /** 64 | * Put a single value into a Locker 65 | */ 66 | @deprecated("Use Locker(k, a)", since = "3.1.0") 67 | def lock[A](k: Key[A], a: A): Locker = Locker(k, a) 68 | 69 | /** 70 | * Retrieve the value from the Locker. If the reference equality instance backed by a `Unique` value is the same then 71 | * allows conversion to that type, otherwise as it does not match then this will be `None` 72 | * 73 | * @param k 74 | * The key to check, if the internal Unique value matches then this Locker can be unlocked as the specifed value 75 | * @param l 76 | * The locked to check against 77 | */ 78 | @deprecated("Use l.unlock(k)", since = "3.1.0") 79 | def unlock[A](k: Key[A], l: Locker): Option[A] = l.unlock(k) 80 | } 81 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/vault/Vault.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package org.typelevel.vault 23 | 24 | import cats.effect.kernel.Unique 25 | 26 | /** 27 | * Vault - A persistent store for values of arbitrary types. This extends the behavior of the locker, into a Map that 28 | * maps Keys to Lockers, creating a heterogenous store of values, accessible by keys. Such that the Vault has no type 29 | * information, all the type information is contained in the keys. 30 | */ 31 | final class Vault private (private val m: Map[Unique.Token, Locker]) { 32 | 33 | /** 34 | * Empty this Vault 35 | */ 36 | def empty: Vault = Vault.empty 37 | 38 | /** 39 | * Lookup the value of a key in this vault 40 | */ 41 | def lookup[A](k: LookupKey[A]): Option[A] = m.get(k.unique).flatMap(_.unlock(k)) 42 | 43 | /** 44 | * Lookup the value of a key in this vault 45 | */ 46 | private[vault] def lookup[A](k: Key[A]): Option[A] = lookup(k: LookupKey[A]) 47 | 48 | /** 49 | * Checks if the value of a key is in this vault. 50 | * 51 | * @param k 52 | * the key to lookup can be any LookupKey, InsertKey, or DeleteKey 53 | */ 54 | def contains(k: DeleteKey): Boolean = m.contains(k.unique) 55 | 56 | /** 57 | * Insert a value for a given key. Overwrites any previous value. 58 | */ 59 | def insert[A](k: InsertKey[A], a: A): Vault = new Vault(m + (k.unique -> Locker(k, a))) 60 | 61 | /** 62 | * Insert a value for a given key. Overwrites any previous value. 63 | */ 64 | private[vault] def insert[A](k: Key[A], a: A): Vault = insert(k: InsertKey[A], a) 65 | 66 | /** 67 | * Checks whether this Vault is empty 68 | */ 69 | def isEmpty: Boolean = m.isEmpty 70 | 71 | /** 72 | * Delete a key from the vault 73 | */ 74 | // Keeping unused type parameter for source compat 75 | def delete[A](k: DeleteKey): Vault = new Vault(m - k.unique) 76 | 77 | /** 78 | * Delete a key from the vault 79 | */ 80 | private[vault] def delete[A](k: Key[A]): Vault = delete(k: DeleteKey) 81 | 82 | /** 83 | * Adjust the value for a given key if it's present in the vault. 84 | */ 85 | def adjust[A](k: Key[A], f: A => A): Vault = lookup(k).fold(this)(a => insert(k, f(a))) 86 | 87 | /** 88 | * Merge Two Vaults. `that` is prioritized. 89 | */ 90 | def ++(that: Vault): Vault = new Vault(this.m ++ that.m) 91 | 92 | /** 93 | * The size of the vault 94 | */ 95 | def size: Int = m.size 96 | } 97 | object Vault { 98 | 99 | /** 100 | * The Empty Vault 101 | */ 102 | def empty = new Vault(Map.empty) 103 | 104 | /** 105 | * Lookup the value of a key in the vault 106 | */ 107 | @deprecated("Use v.lookup(k)", "3.1.0") 108 | def lookup[A](k: Key[A], v: Vault): Option[A] = 109 | v.lookup(k) 110 | 111 | /** 112 | * Insert a value for a given key. Overwrites any previous value. 113 | */ 114 | @deprecated("Use v.insert(k, a)", "3.1.0") 115 | def insert[A](k: Key[A], a: A, v: Vault): Vault = 116 | v.insert(k, a) 117 | 118 | /** 119 | * Checks whether the given Vault is empty 120 | */ 121 | @deprecated("Use v.isEmpty", "3.1.0") 122 | def isEmpty(v: Vault): Boolean = v.isEmpty 123 | 124 | /** 125 | * Delete a key from the vault 126 | */ 127 | @deprecated("Use v.delete(k)", "3.1.0") 128 | def delete[A](k: Key[A], v: Vault): Vault = v.delete(k) 129 | 130 | /** 131 | * Adjust the value for a given key if it's present in the vault. 132 | */ 133 | @deprecated("Use v.adjust(k, f)", "3.1.0") 134 | def adjust[A](k: Key[A], f: A => A, v: Vault): Vault = 135 | v.adjust(k, f) 136 | 137 | /** 138 | * Merge Two Vaults. v2 is prioritized. 139 | */ 140 | @deprecated("Use v2 ++ v2", "3.1.0") 141 | def union(v1: Vault, v2: Vault): Vault = v1 ++ v2 142 | 143 | } 144 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/vault/KeySuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package org.typelevel.vault 23 | 24 | import org.scalacheck._ 25 | import cats.effect.SyncIO 26 | import cats.kernel.laws.discipline.{EqTests, HashTests} 27 | import munit.DisciplineSuite 28 | import cats.laws.discipline.InvariantTests 29 | 30 | class KeySuite extends DisciplineSuite { 31 | implicit def functionArbitrary[B, A: Arbitrary]: Arbitrary[B => A] = Arbitrary { 32 | for { 33 | a <- Arbitrary.arbitrary[A] 34 | } yield { (_: B) => a } 35 | } 36 | 37 | implicit def uniqueKey[A]: Arbitrary[Key[A]] = Arbitrary { 38 | Arbitrary.arbitrary[Unit].map(_ => Key.newKey[SyncIO, A].unsafeRunSync()) 39 | } 40 | 41 | checkAll("Key", HashTests[Key[Int]].hash) 42 | checkAll("Key", EqTests[Key[Int]].eqv) 43 | checkAll("Key", InvariantTests[Key].invariant[Int, String, Boolean]) 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/vault/VaultSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package org.typelevel.vault 23 | 24 | import cats.effect._ 25 | import munit.{CatsEffectSuite, ScalaCheckEffectSuite} 26 | import org.scalacheck.effect.PropF 27 | 28 | class VaultSuite extends CatsEffectSuite with ScalaCheckEffectSuite { 29 | test("Vault should contain a single value correctly") { 30 | PropF.forAllF { (i: Int) => 31 | val emptyVault: Vault = Vault.empty 32 | val test = Key.newKey[IO, Int].map(k => emptyVault.insert(k, i).lookup(k)) 33 | 34 | assertIO(test, Some(i)) 35 | } 36 | } 37 | 38 | test("Vault should contain only the last value after inserts") { 39 | PropF.forAllF { (l: List[String]) => 40 | val emptyVault: Vault = Vault.empty 41 | val test = Key.newKey[IO, String].map { k => 42 | l.reverse.foldLeft(emptyVault)((v, a) => v.insert(k, a)).lookup(k) 43 | } 44 | 45 | assertIO(test, l.headOption) 46 | } 47 | } 48 | 49 | test("Vault should contain no value after being emptied") { 50 | PropF.forAllF { (l: List[String]) => 51 | val emptyVault: Vault = Vault.empty 52 | val test: IO[Option[String]] = Key.newKey[IO, String].map { k => 53 | l.reverse.foldLeft(emptyVault)((v, a) => v.insert(k, a)).empty.lookup(k) 54 | } 55 | 56 | assertIO(test, None) 57 | } 58 | } 59 | 60 | test("Vault should not be accessible via a different key") { 61 | PropF.forAllF { (i: Int) => 62 | val test = for { 63 | key1 <- Key.newKey[IO, Int] 64 | key2 <- Key.newKey[IO, Int] 65 | } yield Vault.empty.insert(key1, i).lookup(key2) 66 | 67 | assertIO(test, None) 68 | } 69 | } 70 | 71 | test("Vault should contain mapped value inserted with unmapped key") { 72 | PropF.forAllF { (i: Int) => 73 | val emptyVault: Vault = Vault.empty 74 | val test = 75 | for { 76 | k <- Key.newKey[IO, Int] 77 | kʹ = k.imap(_.toString)(_.toInt) // create a mapped key 78 | vʹ = emptyVault.insert(k, i) // insert using unmapped key 79 | s = vʹ.lookup(kʹ) // read using mapped key 80 | } yield s 81 | assertIO(test, Some(i.toString)) 82 | } 83 | } 84 | 85 | test("Vault should contain unmapped value inserted with mapped key") { 86 | PropF.forAllF { (i: Int) => 87 | val emptyVault: Vault = Vault.empty 88 | val test = 89 | for { 90 | k <- Key.newKey[IO, Int] 91 | kʹ = k.imap(_.toString)(_.toInt) // create a mapped key 92 | vʹ = emptyVault.insert(kʹ, i.toString) // insert using mapped key 93 | n = vʹ.lookup(k) // read using unmapped key 94 | } yield n 95 | assertIO(test, Some(i)) 96 | } 97 | } 98 | 99 | test("Vault should contain mapped value inserted with mapped key") { 100 | PropF.forAllF { (i: Int) => 101 | val emptyVault: Vault = Vault.empty 102 | val test = 103 | for { 104 | k <- Key.newKey[IO, Int] 105 | kʹ = k.imap(_.toString)(_.toInt) // create a mapped key 106 | vʹ = emptyVault.insert(kʹ, i.toString) // insert using mapped key 107 | n = vʹ.lookup(kʹ) // read using mapped key 108 | } yield n 109 | assertIO(test, Some(i.toString)) 110 | } 111 | } 112 | 113 | test("Vault contains should be true for inserted values") { 114 | PropF.forAllF { (i: Int) => 115 | val emptyVault: Vault = Vault.empty 116 | val test = Key.newKey[IO, Int].map(k => emptyVault.insert(k, i).contains(k)) 117 | 118 | assertIO(test, true) 119 | } 120 | } 121 | 122 | test("Vault contains should be false for keys not inserted") { 123 | PropF.forAllF { (i: Int) => 124 | val test = for { 125 | key1 <- Key.newKey[IO, Int] 126 | key2 <- Key.newKey[IO, Int] 127 | } yield Vault.empty.insert(key1, i).contains(key2) 128 | 129 | assertIO(test, false) 130 | } 131 | } 132 | 133 | test("Vault contains should be false when empty") { 134 | val emptyVault: Vault = Vault.empty 135 | val test = Key.newKey[IO, Int].map(k => emptyVault.contains(k)) 136 | 137 | assertIO(test, false) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # vault 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.chrisdavenport/vault_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.chrisdavenport/vault-core_2.12) 4 | 5 | ## Project Goals 6 | 7 | Vault is a tiny library that provides a single data structure called vault. 8 | 9 | Inspiration was drawn from [HeinrichApfelmus/vault](https://github.com/HeinrichApfelmus/vault) and the original [blog post](https://apfelmus.nfshost.com/blog/2011/09/04-vault.html) 10 | 11 | A vault is a type-safe, persistent storage for values of arbitrary types. Like `Ref`, it should be capable of storing values of any type in it, but unlike `Ref`, behave like a persistent, first-class data structure. 12 | 13 | It is analogous to a bank vault, where you can access different bank boxes with different keys; hence the name. 14 | 15 | ## Quick Start 16 | 17 | To use vault in an existing SBT project with Scala 2.11 or a later version, add the following dependencies to your 18 | `build.sbt` depending on your needs: 19 | 20 | ```scala 21 | libraryDependencies ++= Seq( 22 | "org.typelevel" %% "vault" % "@VERSION@", 23 | ) 24 | ``` 25 | 26 | First the imports 27 | 28 | ```scala mdoc:silent 29 | import cats.effect._ 30 | import cats.implicits._ 31 | import org.typelevel.vault._ 32 | 33 | // Importing global cats-effect runtime to allow .unsafeRunSync(); 34 | // In real code you should follow cats-effect advice on obtaining a runtime 35 | import cats.effect.unsafe.implicits.global 36 | ``` 37 | 38 | Then some basic operations 39 | 40 | ```scala mdoc:silent 41 | case class Bar(a: String, b: Int, c: Long) 42 | 43 | // Creating keys are effects, but interacting with the vault 44 | // not, it acts like a simple persistent store. 45 | 46 | val basicLookup = for { 47 | key <- Key.newKey[IO, Bar] 48 | } yield { 49 | Vault.empty 50 | .insert(key, Bar("", 1, 2L)) 51 | .lookup(key) 52 | } 53 | ``` 54 | 55 | ```scala mdoc 56 | basicLookup.unsafeRunSync() 57 | ``` 58 | 59 | ```scala mdoc:silent 60 | val containsKey = for { 61 | key <- Key.newKey[IO, Bar] 62 | } yield { 63 | Vault.empty 64 | .insert(key, Bar("", 1, 2L)) 65 | .contains(key) 66 | } 67 | ``` 68 | 69 | ```scala mdoc 70 | containsKey.unsafeRunSync() 71 | ``` 72 | 73 | ```scala mdoc:silent 74 | val lookupValuesOfDifferentTypes = for { 75 | key1 <- Key.newKey[IO, Bar] 76 | key2 <- Key.newKey[IO, String] 77 | key3 <- Key.newKey[IO, String] 78 | } yield { 79 | val myvault = Vault.empty 80 | .insert(key1, Bar("", 1, 2L)) 81 | .insert(key2, "I'm at Key2") 82 | .insert(key3, "Key3 Reporting for Duty!") 83 | 84 | (myvault.lookup(key1), myvault.lookup(key2), myvault.lookup(key3)) 85 | .mapN((_,_,_)) 86 | } 87 | ``` 88 | 89 | ```scala mdoc 90 | lookupValuesOfDifferentTypes.unsafeRunSync() 91 | ``` 92 | 93 | ```scala mdoc:silent 94 | val emptyLookup = for { 95 | key <- Key.newKey[IO, Bar] 96 | } yield { 97 | Vault.empty 98 | .lookup(key) 99 | } 100 | ``` 101 | 102 | ```scala mdoc 103 | emptyLookup.unsafeRunSync() 104 | ``` 105 | 106 | ```scala mdoc:silent 107 | val doubleInsertTakesMostRecent = for { 108 | key <- Key.newKey[IO, Bar] 109 | } yield { 110 | Vault.empty 111 | .insert(key, Bar("", 1, 2L)) 112 | .insert(key, Bar("Monkey", 7, 5L)) 113 | .lookup(key) 114 | } 115 | ``` 116 | 117 | ```scala mdoc 118 | doubleInsertTakesMostRecent.unsafeRunSync() 119 | ``` 120 | 121 | ```scala mdoc:silent 122 | val mergedVaultsTakesLatter = for { 123 | key <- Key.newKey[IO, Bar] 124 | } yield { 125 | ( 126 | Vault.empty.insert(key, Bar("", 1, 2L)) ++ 127 | Vault.empty.insert(key, Bar("Monkey", 7, 5L)) 128 | ).lookup(key) 129 | } 130 | ``` 131 | 132 | ```scala mdoc 133 | mergedVaultsTakesLatter.unsafeRunSync() 134 | ``` 135 | 136 | ```scala mdoc:silent 137 | val deletedKeyIsMissing = for { 138 | key <- Key.newKey[IO, Bar] 139 | } yield { 140 | Vault.empty 141 | .insert(key, Bar("", 1, 2L)) 142 | .delete(key) 143 | .lookup(key) 144 | } 145 | ``` 146 | 147 | ```scala mdoc 148 | deletedKeyIsMissing.unsafeRunSync() 149 | ``` 150 | 151 | We can also interact with a single value `locker` instead of the 152 | larger datastructure that a `vault` enables. 153 | 154 | ```scala mdoc:silent 155 | val lockerExample = for { 156 | key <- Key.newKey[IO, Bar] 157 | } yield { 158 | Locker(key, Bar("", 1, 2L)) 159 | .unlock(key) 160 | } 161 | ``` 162 | 163 | ```scala mdoc 164 | lockerExample.unsafeRunSync() 165 | ``` 166 | 167 | ```scala mdoc:silent 168 | val wrongLockerExample = for { 169 | key <- Key.newKey[IO, Bar] 170 | key2 <- Key.newKey[IO, Bar] 171 | } yield { 172 | Locker(key, Bar("", 1, 2L)) 173 | .unlock(key2) 174 | } 175 | ``` 176 | 177 | ```scala mdoc 178 | wrongLockerExample.unsafeRunSync() 179 | ``` 180 | -------------------------------------------------------------------------------- /licenses/LICENSE_vault: -------------------------------------------------------------------------------- 1 | Copyright (c)2011, Heinrich Apfelmus 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Heinrich Apfelmus nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") 2 | val sbtTypelevelVersion = "0.8.0" 3 | addSbtPlugin("org.typelevel" % "sbt-typelevel" % sbtTypelevelVersion) 4 | addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % sbtTypelevelVersion) 5 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") 6 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 7 | --------------------------------------------------------------------------------