├── .github ├── ISSUE_TEMPLATE │ └── compilerrewriteunsuccessfulexception-bug-report.md ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── release-drafter.yml │ └── release.yml ├── .gitignore ├── .scalafmt.conf ├── .sdkmanrc ├── AUTHORS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── build.sbt ├── cats-unwrapped └── src │ ├── main │ └── scala │ │ └── CatsEffect.scala │ └── test │ └── scala │ └── CatsEffectTests.scala ├── docs ├── AUTHORS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.md ├── README.md └── SIP.md ├── http-unwrapped └── src │ ├── main │ ├── mediaTypes │ │ ├── application.csv │ │ ├── audio.csv │ │ ├── font.csv │ │ ├── image.csv │ │ ├── message.csv │ │ ├── model.csv │ │ ├── multipart.csv │ │ ├── text.csv │ │ └── video.csv │ └── scala │ │ └── unwrapped │ │ ├── Http.scala │ │ ├── HttpBodyMapper.scala │ │ ├── HttpClient.scala │ │ ├── HttpClientConfig.scala │ │ ├── HttpConnectionTimeout.scala │ │ ├── HttpExecutionException.scala │ │ ├── HttpFollowRedirects.scala │ │ ├── HttpHeader.scala │ │ ├── HttpResponseMapper.scala │ │ ├── HttpRetries.scala │ │ ├── HttpRetryPolicy.scala │ │ ├── Https.scala │ │ ├── HttpsClient.scala │ │ ├── MediaType.scala │ │ ├── RetryHttpException.scala │ │ ├── Serde.scala │ │ ├── StatusCode.scala │ │ └── StatusCodes.scala │ └── test │ └── scala │ └── unwrapped │ ├── FakeHttpResponse.scala │ ├── HttpConnectionTimeoutSuite.scala │ ├── HttpResponseMapperSuite.scala │ ├── HttpRetriesSuite.scala │ ├── HttpRetryPolicySuite.scala │ ├── HttpServerFixtures.scala │ └── HttpSuite.scala ├── java-net-mulitpart-body-publisher └── src │ └── main │ └── scala │ └── fx │ ├── Boundary.scala │ ├── MultipartBodyPublisher.scala │ ├── PartSpecification.scala │ ├── PartSpecificationContentType.scala │ ├── PartSpecificationFileName.scala │ ├── PartSpecificationInputStream.scala │ ├── PartSpecificationName.scala │ ├── PartSpecificationValue.scala │ └── ToPartSpec.scala ├── munit-unwrapped └── src │ ├── it │ └── scala │ │ └── example │ │ └── ScalaFXSuiteIntegrationTest.scala │ ├── main │ └── scala │ │ └── munit │ │ └── unwrapped │ │ ├── Asserts.scala │ │ ├── UnwrappedAssertions.scala │ │ └── UnwrappedSuite.scala │ └── test │ └── scala │ └── munit │ └── fx │ └── ScalaFxAssertionsSuite.scala ├── project ├── Dependencies.scala ├── build.properties ├── build.sbt ├── plugins.sbt ├── project │ └── Dependencies.scala └── src │ └── main │ └── scala │ └── fx │ └── HttpScalaFxPlugin.scala ├── scalike-jdbc-unwrapped └── src │ ├── main │ └── scala │ │ └── fx │ │ └── DatabaseScala.scala │ └── test │ ├── resources │ └── db │ │ └── migration │ │ └── V0__init.sql │ └── scala │ └── unwrapped │ ├── DatabaseSpec.scala │ └── DatabaseSuite.scala ├── sttp-unwrapped └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── fx │ │ ├── HttpExtensions.scala │ │ ├── HttpScalaFXBackend.scala │ │ ├── PatchPartiallyApplied.scala │ │ ├── PostPartiallyApplied.scala │ │ ├── PutPartiallyApplied.scala │ │ ├── ReceiveStreams.scala │ │ ├── StatusCodeToStatusCode.scala │ │ └── ToHttpBodyMapper.scala │ └── test │ ├── resources │ └── brand.svg │ └── scala │ └── sttp │ └── fx │ ├── FullBackendFixtures.scala │ ├── HttpExtensionsSuite.scala │ ├── HttpExtensionsSuiteFixtures.scala │ ├── HttpScalaFXBackendSuite.scala │ ├── PatchPartiallyAppliedSuite.scala │ ├── PostPartiallyAppliedSuite.scala │ ├── PutPartiallyAppliedSuite.scala │ ├── StatusCodeToStatusCodeSuite.scala │ ├── ToHttpBodyMapperFixtures.scala │ └── ToHttpBodyMapperSuite.scala └── unwrapped └── src ├── main └── scala │ └── unwrapped │ ├── Bind.scala │ ├── Console.scala │ ├── Continuation.scala │ ├── Control.scala │ ├── Errors.scala │ ├── Fiber.scala │ ├── Nullable.scala │ ├── Parallel.scala │ ├── Resource.scala │ ├── Runtime.scala │ ├── Show.scala │ ├── ShowImpl.scala │ ├── Streams.scala │ ├── Structured.scala │ ├── Throws.scala │ ├── Use.scala │ └── requires.scala └── test └── scala └── unwrapped ├── BindTests.scala ├── ControlTests.scala ├── NullableTests.scala ├── RequiresTests.scala ├── ResourcesTests.scala ├── RuntimeTests.scala ├── StreamsTests.scala ├── StructuredTests.scala ├── UseTests.scala └── sip ├── Bind.scala ├── Continuation.scala ├── Control.scala ├── Fiber.scala ├── Model.scala ├── Program.scala ├── Structured.scala ├── StyleDirect.scala └── StyleIndirect.scala /.github/ISSUE_TEMPLATE/compilerrewriteunsuccessfulexception-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: CompilerRewriteUnsuccessfulException Bug report 3 | about: Create a report to help us improve 4 | title: CompilerRewriteUnsuccessfulException Bug 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. ... 16 | 2. ... 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | exclude-labels: 4 | - 'auto-update' 5 | - 'auto-documentation' 6 | - 'auto-changelog' 7 | categories: 8 | - title: '⚠️ Breaking changes' 9 | label: 'breaking-change' 10 | - title: '🚀 Features' 11 | label: 'enhancement' 12 | - title: '📘 Documentation' 13 | label: 'documentation' 14 | - title: '🐛 Bug Fixes' 15 | label: 'bug' 16 | - title: '📈 Dependency updates' 17 | labels: 18 | - 'dependency-update' 19 | - 'scala-steward' 20 | template: | 21 | ## What's changed 22 | 23 | $CHANGES 24 | 25 | ## Contributors to this release 26 | 27 | $CONTRIBUTORS -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Formatters & Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | env: 12 | JAVA_OPTS: --enable-preview --add-modules jdk.incubator.concurrent 13 | steps: 14 | - name: Checkout project (pull-request) 15 | if: github.event_name == 'pull_request' 16 | uses: actions/checkout@v3 17 | with: 18 | repository: ${{ github.event.pull_request.head.repo.full_name }} 19 | ref: ${{ github.event.pull_request.head.ref }} 20 | - name: Checkout project (main) 21 | if: github.event_name == 'push' 22 | uses: actions/checkout@v3 23 | - name: Setup Scala 24 | uses: actions/setup-java@v3.6.0 25 | with: 26 | distribution: 'temurin' 27 | java-version: 19 28 | - name: Run checks 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | run: sbt ci-test 32 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | name: Update documentation 6 | 7 | on: 8 | release: 9 | types: [published] 10 | repository_dispatch: 11 | types: [docs] 12 | 13 | jobs: 14 | documentation: 15 | if: "!contains(github.event.head_commit.message, 'skip ci')" 16 | runs-on: ubuntu-latest 17 | env: 18 | JAVA_OPTS: --enable-preview --add-modules jdk.incubator.concurrent 19 | steps: 20 | - name: Checkout project 21 | uses: actions/checkout@v3 22 | with: 23 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 24 | ref: main 25 | - name: Fetch tags 26 | run: git fetch --tags 27 | - name: Setup Scala 28 | uses: actions/setup-java@v3.6.0 29 | with: 30 | distribution: 'temurin' 31 | java-version: 19 32 | - name: Generate documentation 33 | run: sbt ci-docs 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | DOWNLOAD_INFO_FROM_GITHUB: true 37 | - name: Push changes 38 | uses: stefanzweifel/git-auto-commit-action@v4.1.3 39 | with: 40 | commit_message: 'Update documentation, and other files [skip ci]' -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Drafts/updates the next repository release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | branches: main 8 | 9 | jobs: 10 | release: 11 | if: "!contains(github.event.head_commit.message, 'skip ci')" 12 | runs-on: ubuntu-latest 13 | env: 14 | JAVA_OPTS: --enable-preview --add-modules jdk.incubator.concurrent 15 | steps: 16 | - name: Checkout project 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Fetch tags 21 | run: git fetch --tags 22 | - name: Setup Scala 23 | uses: actions/setup-java@v3.6.0 24 | with: 25 | distribution: 'temurin' 26 | java-version: 19 27 | - name: Setup GPG 28 | uses: olafurpg/setup-gpg@v3 29 | - name: Release new version 30 | run: sbt ci-publish 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 34 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 35 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 36 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.tasty 3 | *.log 4 | .DS_Store 5 | 6 | # sbt specific 7 | .cache 8 | .history 9 | .lib/ 10 | dist/* 11 | target/ 12 | lib_managed/ 13 | src_managed/ 14 | project/boot/ 15 | project/plugins/project/ 16 | .bsp 17 | 18 | # Scala-IDE specific 19 | .scala_dependencies 20 | .worksheet 21 | .sc 22 | 23 | .idea 24 | *.iml 25 | 26 | out/ 27 | 28 | .bloop/ 29 | .metals/ 30 | .vscode/ 31 | project/metals.sbt 32 | project/project/ 33 | !project/project/Dependencies.scala -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.2.1 2 | 3 | runner.dialect = scala3 4 | 5 | project.excludeFilters = [ 6 | "scalafix/*" 7 | ] 8 | 9 | maxColumn = 96 10 | 11 | includeCurlyBraceInSelectChains = true 12 | includeNoParensInSelectChains = true 13 | 14 | optIn { 15 | breakChainOnFirstMethodDot = false 16 | forceBlankLineBeforeDocstring = true 17 | } 18 | 19 | binPack { 20 | literalArgumentLists = true 21 | parentConstructors = Never 22 | } 23 | 24 | danglingParentheses { 25 | defnSite = false 26 | callSite = false 27 | ctrlSite = false 28 | tupleSite = false 29 | 30 | exclude = [] 31 | } 32 | 33 | newlines { 34 | beforeCurlyLambdaParams = multilineWithCaseOnly 35 | afterCurlyLambda = squash 36 | implicitParamListModifierPrefer = before 37 | sometimesBeforeColonInMethodReturnType = true 38 | } 39 | 40 | align.preset = none 41 | align.stripMargin = true 42 | 43 | assumeStandardLibraryStripMargin = true 44 | 45 | docstrings { 46 | style = Asterisk 47 | oneline = unfold 48 | } 49 | 50 | project.git = true 51 | 52 | trailingCommas = never 53 | 54 | rewrite { 55 | // RedundantBraces honestly just doesn't work, otherwise I'd love to use it 56 | rules = [PreferCurlyFors, RedundantParens, SortImports] 57 | 58 | redundantBraces { 59 | maxLines = 1 60 | stringInterpolation = true 61 | } 62 | } 63 | 64 | rewriteTokens { 65 | "⇒": "=>" 66 | "→": "->" 67 | "←": "<-" 68 | } 69 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config 2 | # Add key=value pairs of SDKs to use below 3 | java=19.ea.35-open 4 | scala=3.1.3 -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | ## Maintainers 4 | 5 | The maintainers of the project are: 6 | 7 | - [![franciscodr](https://avatars.githubusercontent.com/u/1200151?v=4&s=20) **Francisco Diaz (franciscodr)**](https://github.com/franciscodr) 8 | - [![juanpedromoreno](https://avatars.githubusercontent.com/u/4879373?v=4&s=20) **Juan Pedro Moreno (juanpedromoreno)**](https://github.com/juanpedromoreno) 9 | - [![qohat](https://avatars.githubusercontent.com/u/15187322?v=4&s=20) **Qohat Pretel Polo (qohat)**](https://github.com/qohat) 10 | - [![raulraja](https://avatars.githubusercontent.com/u/456796?v=4&s=20) **Raúl Raja Martínez (raulraja)**](https://github.com/raulraja) 11 | - [![jackcviers](https://avatars.githubusercontent.com/u/660372?s=96&v=4) **Jack Viers (jackcviers)**](https://github.com/jackcviers) 12 | 13 | ## Contributors 14 | 15 | These are the people that have contributed to the _scala-fx_ project: 16 | 17 | - [![raulraja](https://avatars.githubusercontent.com/u/456796?v=4&s=20) **raulraja**](https://github.com/raulraja) 18 | - [![juanpedromoreno](https://avatars.githubusercontent.com/u/4879373?v=4&s=20) **juanpedromoreno**](https://github.com/juanpedromoreno) 19 | - [![qohat](https://avatars.githubusercontent.com/u/15187322?v=4&s=20) **qohat**](https://github.com/qohat) 20 | - [![franciscodr](https://avatars.githubusercontent.com/u/1200151?v=4&s=20) **franciscodr**](https://github.com/franciscodr) 21 | - [![mattmoore](https://avatars.githubusercontent.com/u/3020667?v=4&s=20) **mattmoore**](https://github.com/mattmoore) 22 | - [![serras](https://avatars.githubusercontent.com/u/309334?v=4&s=20) **serras**](https://github.com/serras) 23 | - [![jackcviers](https://avatars.githubusercontent.com/u/660372?s=96&v=4) **Jack Viers (jackcviers)**](https://github.com/jackcviers) 24 | 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming 4 | environment for all, regardless of level of experience, gender, gender 5 | identity and expression, sexual orientation, disability, personal 6 | appearance, body size, race, ethnicity, age, religion, nationality, or 7 | other such characteristics. 8 | 9 | Everyone is expected to follow the 10 | [Scala Code of Conduct](https://www.scala-lang.org/conduct/) when 11 | discussing the project on the available communication channels. If you 12 | are being harassed, please contact us immediately so that we can 13 | support you. 14 | 15 | ## Moderation 16 | 17 | For any questions, concerns, or moderation requests please contact a 18 | [member of the project](AUTHORS.md#maintainers). 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Discussion around _scala-fx_ happens in the [GitHub issues](https://github.com/47deg/scala-fx/issues) and [pull requests](https://github.com/47deg/scala-fx/pulls). 4 | 5 | Feel free to open an issue if you notice a bug, have an idea for a feature, or have a question about 6 | the code. Pull requests are also welcome. 7 | 8 | People are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md) when discussing _scala-fx_ on the Github page or other venues. 9 | 10 | If you are being harassed, please contact one of [us](AUTHORS.md#maintainers) immediately so that we can support you. In case you cannot get in touch with us please write an email to [47 Degrees](mailto:hello@47deg.com). 11 | 12 | ## How can I help? 13 | 14 | _scala-fx_ follows a standard [fork and pull](https://help.github.com/articles/using-pull-requests/) model for contributions via GitHub pull requests. 15 | 16 | The process is simple: 17 | 18 | 1. Find something you want to work on 19 | 2. Let us know you are working on it via GitHub issues/pull requests 20 | 3. Implement your contribution 21 | 4. Write tests 22 | 5. Update the documentation 23 | 6. Submit pull request 24 | 25 | You will be automatically included in the [AUTHORS.md](AUTHORS.md#contributors) file as contributor in the next release. 26 | 27 | If you encounter any confusion or frustration during the contribution process, please create a GitHub issue and we'll do our best to improve the process. -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | scala-fx 2 | 3 | Copyright (c) 2021-2022 47 Degrees. All rights reserved. 4 | 5 | Licensed under Apache-2.0. See [LICENSE](LICENSE.md) for terms. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unwrapped 2 | 3 | ## Getting started 4 | 5 | Unwrapped is an effects library for Scala 3 that introduces structured concurrency and an abilities system to describe pure functions and programs. 6 | 7 | The example below is a pure program that returns `Int` and requires the context capability `Bind`. Bind enables the `bind` syntax over values of Either and other types. 8 | 9 | ```scala 10 | import unwrapped.* 11 | 12 | val program: Int = 13 | Right(1).bind + Right(2).bind 14 | // program: Int = 3 15 | ``` 16 | 17 | Using Scala3 features such as context functions we can encode pure programs in terms of capabilities with minimal overhead. 18 | Capabilities can be introduced a la carte and will be carried as given contextual evidences through call sites until you proof you can get rid of them. 19 | 20 | ```scala 21 | import unwrapped.* 22 | 23 | def runProgram: Int | String = 24 | val program: Errors[String] ?=> Int = 25 | Right(1).bind + Right(2).bind + "oops".raise[Int] 26 | 27 | run(program) 28 | 29 | println(runProgram) 30 | // oops 31 | ``` 32 | 33 | Users and library authors may define their own Capabilities. Here is how `Bind` for `Either[E, A]` is declared 34 | 35 | ```scala 36 | /** Brings the capability to perform Monad bind in place. Types may 37 | * access [[Control]] to short-circuit as necessary 38 | */ 39 | extension [R, A](fa: Either[R, A]) 40 | def bind: Errors[R] ?=> A = fa.fold(_.shift, identity) 41 | ``` 42 | 43 | Unwrapped supports a structured concurrency model backed by the non-blocking [StructuredExecutorTask](https://openjdk.java.net/jeps/428) 44 | where you can `fork` and `join` cancellable fibers and scopes. 45 | 46 | Popular functions like `parallel` support arbitrary typed arity in arguments and return types. 47 | 48 | ```scala 49 | import unwrapped.* 50 | 51 | def runProgram: (String, Int, Double) = 52 | val results: Structured ?=> (String, Int, Double) = 53 | parallel( 54 | () => "1", 55 | () => 0, 56 | () => 47.03 57 | ) 58 | 59 | structured(results) 60 | 61 | println(runProgram) 62 | // (1,0,47.03) 63 | ``` 64 | 65 | Continuations based on Control Throwable or a non-blocking model like Loom are useful because they allow us to intermix async and sync programs in the same syntax without the need for boxing as is frequently the case in most scala effect libraries. 66 | 67 | ### Build and run in your local environment: 68 | 69 | Pre-requisites: 70 | 71 | - [Java 19](https://openjdk.org/projects/jdk/19/) 72 | - Scala 3 73 | 74 | 1. Download the latest Project Loom [Early-Access build](https://openjdk.org/projects/jdk/19/) for your system architecture. 75 | 76 | This is easy to do if you are using [SDKMAN](https://sdkman.io/). First list java 77 | versions with `sdk list java`, and `sdk install java 78 | 19-ea--open`, where XX is the latest Java.net version 19 79 | release number. Make it the default with `sdk default java 80 | 19-ea--open`. 81 | 82 | 2. Set your `JAVA_HOME` to the path you extracted above. 83 | 84 | Unnecessary if you have set the ea build to be your default in sdkman. 85 | 86 | You can now compile and run the tests: 87 | 88 | ```shell 89 | env JAVA_OPTS='--enable-preview --add-modules jdk.incubator.concurrent' sbt "clean; compile; test" 90 | ``` 91 | 92 | **NOTE**: The Loom project is defined as an incubator module that is a means to distribute APIs which are not final or completed to get feedback from the developers. 93 | You should include the `-add-module` Java option to add the module to the class path of the project. 94 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies.Compile._ 2 | import Dependencies.Test._ 3 | 4 | ThisBuild / scalaVersion := "3.1.2" 5 | ThisBuild / organization := "com.47deg" 6 | ThisBuild / versionScheme := Some("early-semver") 7 | 8 | addCommandAlias("ci-test", "scalafmtCheckAll; scalafmtSbtCheck; github; mdoc; root / test") 9 | addCommandAlias("ci-docs", "github; mdoc") 10 | addCommandAlias("ci-publish", "github; ci-release") 11 | 12 | lazy val root = 13 | (project in file("./")) 14 | .settings(publish / skip := true) 15 | .aggregate( 16 | benchmarks, 17 | documentation, 18 | `http-unwrapped`, 19 | `java-net-multipart-body-publisher`, 20 | `munit-unwrapped`, 21 | `unwrapped`, 22 | `scalike-jdbc-unwrapped`, 23 | `sttp-unwrapped` 24 | ) 25 | 26 | lazy val `unwrapped` = project.settings(unwrappedSettings: _*) 27 | 28 | lazy val benchmarks = 29 | project.dependsOn(`unwrapped`).settings(publish / skip := true).enablePlugins(JmhPlugin) 30 | 31 | lazy val documentation = project 32 | .dependsOn(`unwrapped`) 33 | .enablePlugins(MdocPlugin) 34 | .settings(mdocOut := file(".")) 35 | .settings(publish / skip := true) 36 | 37 | lazy val `munit-unwrapped` = (project in file("./munit-unwrapped")) 38 | .configs(IntegrationTest) 39 | .settings( 40 | munitUnwrappedSettings 41 | ) 42 | .dependsOn(`unwrapped`) 43 | 44 | lazy val `cats-unwrapped` = (project in file("./cats-unwrapped")) 45 | .configs(IntegrationTest) 46 | .settings( 47 | catsUnwrappedSettings 48 | ) 49 | .dependsOn(`unwrapped`) 50 | 51 | lazy val `scalike-jdbc-unwrapped` = project 52 | .dependsOn(`unwrapped`, `munit-unwrapped` % "test -> compile") 53 | .settings(scalalikeSettings) 54 | 55 | lazy val `java-net-multipart-body-publisher` = 56 | (project in file("./java-net-mulitpart-body-publisher")).settings(commonSettings) 57 | 58 | lazy val `http-unwrapped` = (project in file("./http-unwrapped")) 59 | .settings(httpUnwrappedSettings) 60 | .settings(generateMediaTypeSettings) 61 | .dependsOn( 62 | `java-net-multipart-body-publisher`, 63 | `unwrapped`, 64 | `munit-unwrapped` % "test -> compile") 65 | .enablePlugins(HttpUnwrappedPlugin) 66 | 67 | lazy val `sttp-unwrapped` = (project in file("./sttp-unwrapped")) 68 | .settings(sttpUnwrappedSettings) 69 | .dependsOn( 70 | `java-net-multipart-body-publisher`, 71 | `unwrapped`, 72 | `http-unwrapped`, 73 | `munit-unwrapped` % "test -> compile") 74 | 75 | lazy val commonSettings = Seq( 76 | javaOptions ++= javaOptionsSettings, 77 | autoAPIMappings := true, 78 | Test / fork := true 79 | ) 80 | 81 | lazy val unwrappedSettings: Seq[Def.Setting[_]] = 82 | Seq( 83 | classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat, 84 | javaOptions ++= javaOptionsSettings, 85 | autoAPIMappings := true, 86 | libraryDependencies ++= Seq( 87 | scalacheck % Test 88 | ) 89 | ) 90 | 91 | lazy val munitUnwrappedSettings = Defaults.itSettings ++ Seq( 92 | libraryDependencies ++= Seq( 93 | munitScalacheck, 94 | hedgehog, 95 | junit, 96 | munit, 97 | junitInterface 98 | ) 99 | ) ++ commonSettings 100 | 101 | lazy val catsUnwrappedSettings = Seq( 102 | classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat, 103 | javaOptions ++= javaOptionsSettings, 104 | autoAPIMappings := true, 105 | libraryDependencies ++= Seq( 106 | catsEffect, 107 | scalacheck % Test 108 | ) 109 | ) 110 | 111 | lazy val scalalikeSettings: Seq[Def.Setting[_]] = 112 | Seq( 113 | classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat, 114 | javaOptions ++= javaOptionsSettings, 115 | autoAPIMappings := true, 116 | libraryDependencies ++= Seq( 117 | scalikejdbc, 118 | h2Database, 119 | logback, 120 | postgres, 121 | scalacheck % Test, 122 | testContainers % Test, 123 | testContainersMunit % Test, 124 | testContainersPostgres % Test, 125 | flyway % Test 126 | ) 127 | ) 128 | 129 | lazy val httpUnwrappedSettings = commonSettings 130 | 131 | lazy val sttpUnwrappedSettings = commonSettings ++ Seq( 132 | libraryDependencies += sttp, 133 | libraryDependencies += httpCore5, 134 | libraryDependencies += hedgehog % Test 135 | ) 136 | 137 | lazy val javaOptionsSettings = Seq( 138 | "-XX:+IgnoreUnrecognizedVMOptions", 139 | "-XX:-DetectLocksInCompiledFrames", 140 | "-XX:+UnlockDiagnosticVMOptions", 141 | "-XX:+UnlockExperimentalVMOptions", 142 | "-XX:+UseNewCode", 143 | "--add-modules=java.base", 144 | "--add-modules=jdk.incubator.concurrent", 145 | "--add-opens java.base/jdk.internal.vm=ALL-UNNAMED", 146 | "--add-exports java.base/jdk.internal.vm=ALL-UNNAMED", 147 | "--enable-preview" 148 | ) 149 | -------------------------------------------------------------------------------- /cats-unwrapped/src/main/scala/CatsEffect.scala: -------------------------------------------------------------------------------- 1 | package fx 2 | package cats 3 | 4 | import _root_.cats.* 5 | import _root_.cats.implicits.* 6 | import _root_.cats.effect.* 7 | import _root_.cats.effect.unsafe.* 8 | import fx.run 9 | import fx.Fiber 10 | 11 | import java.util.concurrent.{CancellationException, CompletableFuture, Executors, Future} 12 | import scala.annotation.unchecked.uncheckedVariance 13 | import scala.concurrent.duration.Duration 14 | import scala.concurrent.{Await, ExecutionContext, ExecutionException} 15 | import scala.util.{Failure, Success} 16 | 17 | def toEffect[F[*]: [g[*]] =>> ApplicativeError[g, R], R: Manifest, A: Manifest]( 18 | program: Control[R] ?=> A): F[A] = 19 | val x = run(program) 20 | x match { 21 | case x: A => x.pure 22 | case err: R => ApplicativeError[F, R].raiseError[A](err) 23 | case _ => throw RuntimeException("impossible!") // exhaustivity checker is wrong 24 | } 25 | 26 | def fromIO[A](program: IO[A])(using runtime: IORuntime): Structured ?=> Fiber[A] = 27 | val fiber = CompletableFuture[A]() 28 | track(fiber) 29 | val (future, close) = program.unsafeToFutureCancelable() 30 | setupCancellation(fiber, close) 31 | given ExecutionContext = runtime.compute 32 | future.onComplete { trying => 33 | trying match 34 | case s: Success[a] => fiber.complete(s.get) 35 | case f: Failure[a] => fiber.completeExceptionally(f.exception) 36 | } 37 | fiber.asInstanceOf[Fiber[A]] 38 | 39 | def setupCancellation[A](fiber: CompletableFuture[A], close: () => scala.concurrent.Future[Unit]) = 40 | fiber.whenComplete { (_, exception) => 41 | if (exception != null && exception.isInstanceOf[CancellationException]) 42 | Await.result(close(), Duration.Inf) 43 | } 44 | /* 45 | private fun Job.setupCancellation(future: CompletableFuture<*>) { 46 | future.whenComplete { _, exception -> 47 | cancel(exception?.let { 48 | it as? CancellationException ?: CancellationException("CompletableFuture was completed exceptionally", it) 49 | }) 50 | } 51 | } 52 | */ 53 | 54 | /* 55 | suspend fun suspended(): A = suspendCoroutine { cont -> 56 | val connection = cont.context[SuspendConnection] ?: SuspendConnection.uncancellable 57 | 58 | IORunLoop.startCancellable(this, connection) { 59 | it.fold(cont::resumeWithException, cont::resume) 60 | } 61 | } 62 | */ 63 | -------------------------------------------------------------------------------- /cats-unwrapped/src/test/scala/CatsEffectTests.scala: -------------------------------------------------------------------------------- 1 | package fx 2 | package cats 3 | 4 | import _root_.{cats => c} 5 | import c.* 6 | import c.effect.* 7 | import c.implicits.* 8 | import c.effect.implicits.* 9 | import c.effect.unsafe.implicits.* 10 | import c.syntax.either._ 11 | import org.scalacheck.Prop.forAll 12 | import org.scalacheck.Properties 13 | 14 | import java.util.concurrent.{CompletableFuture, TimeUnit} 15 | import java.util.concurrent.atomic.AtomicInteger 16 | import java.util.concurrent.atomic.AtomicReference 17 | import scala.concurrent.CancellationException 18 | import scala.concurrent.duration.Duration 19 | import scala.concurrent.duration.FiniteDuration 20 | import scala.concurrent.duration._ 21 | 22 | object CatsEffectTests extends Properties("Cats Effect Tests"): 23 | given ApplicativeError[IO, Throwable] = summon 24 | given ApplicativeError[IO, String] = summon 25 | 26 | property("fx happy programs to IO") = forAll { (a: Int) => 27 | val effect: Control[Throwable] ?=> Int = a 28 | toEffect[IO, Throwable, Int](effect).unsafeRunSync() == a 29 | } 30 | 31 | property("fx failing programs to ApplicativeError effects") = forAll { (b: String) => 32 | val effect: Control[String] ?=> Int = b.shift 33 | implicit val ae: ApplicativeError[[a] =>> Either[String, a], String] = 34 | catsStdInstancesForEither 35 | toEffect[[a] =>> Either[String, a], String, Int](effect) == Left(b) 36 | 37 | } 38 | 39 | property("fx failing throwable programs to IO effects") = forAll { (b: String) => 40 | val expectedException = RuntimeException(b) 41 | val effect: Control[Throwable] ?=> Int = expectedException.shift 42 | 43 | toEffect[IO, Throwable, Int](effect).attempt.unsafeRunSync() == Left(expectedException) 44 | 45 | } 46 | 47 | property("IO cancellation is propagated through fx structure") = forAll { (a: Int) => 48 | var aa: Int | Null = null 49 | try 50 | structured { 51 | fromIO( 52 | IO.canceled 53 | .onCancel(IO { 54 | aa = a 55 | })) 56 | } 57 | false 58 | catch 59 | case e: CancellationException => aa == a 60 | case e: Throwable => 61 | println(e) 62 | false 63 | } 64 | 65 | property("fx happy programs to IO") = forAll { (a: Int) => 66 | val effect: Control[String] ?=> Int = a 67 | toEffect[IO, String, Int](effect).unsafeRunSync() == a 68 | } 69 | 70 | property("fx failing programs with Control[Throwable] to IO") = forAll { (b: String) => 71 | val effect: Control[Throwable] ?=> Int = new RuntimeException(b).shift 72 | toEffect[IO, Throwable, Int](effect).attempt.unsafeRunSync().leftMap { e => 73 | e.getMessage 74 | } == Left(b) 75 | } 76 | 77 | // we'll need to have a converter to lift R to throwable 78 | // property("fx failing programs to IO") = forAll { (b: String) => 79 | // val effect: Control[String] ?=> Int = b.shift 80 | // toEffect[IO, String, Int](effect).attempt.unsafeRunSync() == Left(b) 81 | 82 | // } 83 | 84 | property("fromIO can handle errors through IO") = forAll { (t: Throwable, expected: Int) => 85 | structured { 86 | val actual = fromIO(IO[Int] { 87 | throw t 88 | }.handleErrorWith { _ => IO.pure(expected) }) 89 | actual.join == expected 90 | } 91 | } 92 | 93 | property("structured cancellation should cancel IO") = forAll { (i: Int) => 94 | val promise = CompletableFuture[Int]() 95 | val latch = CompletableFuture[Unit]() 96 | structured { 97 | val fiber = fromIO(IO { 98 | latch.complete(()) 99 | }.flatMap(_ => IO.never[Int]).onCancel { 100 | IO(promise.complete(i)) 101 | }) 102 | latch.get() 103 | try fiber.cancel(true) 104 | catch case e: Throwable => () // ignore blow up 105 | promise.get() == i 106 | } 107 | } 108 | 109 | property("fromIO can cancel nested async IOs") = forAll { (i: Int) => 110 | IO.async_[Int] { cb => 111 | structured { 112 | val fiber = fromIO( 113 | IO.async_[Int] { _ => } 114 | .onCancel(IO { 115 | cb(Right(i)) 116 | })) 117 | try fiber.cancel(true) 118 | catch case e: Throwable => () // ignore blow up 119 | } 120 | }.unsafeRunSync() == i 121 | } 122 | -------------------------------------------------------------------------------- /docs/AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | ## Maintainers 4 | 5 | The maintainers of the project are: 6 | 7 | @COLLABORATORS@ 8 | 9 | ## Contributors 10 | 11 | These are the people that have contributed to the _@NAME@_ project: 12 | 13 | @CONTRIBUTORS@ -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming 4 | environment for all, regardless of level of experience, gender, gender 5 | identity and expression, sexual orientation, disability, personal 6 | appearance, body size, race, ethnicity, age, religion, nationality, or 7 | other such characteristics. 8 | 9 | Everyone is expected to follow the 10 | [Scala Code of Conduct](https://www.scala-lang.org/conduct/) when 11 | discussing the project on the available communication channels. If you 12 | are being harassed, please contact us immediately so that we can 13 | support you. 14 | 15 | ## Moderation 16 | 17 | For any questions, concerns, or moderation requests please contact a 18 | [member of the project](AUTHORS.md#maintainers). -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Discussion around _@NAME@_ happens in the [GitHub issues](https://github.com/@REPO@/issues) and [pull requests](https://github.com/@REPO@/pulls). 4 | 5 | Feel free to open an issue if you notice a bug, have an idea for a feature, or have a question about 6 | the code. Pull requests are also welcome. 7 | 8 | People are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md) when discussing _@NAME@_ on the Github page or other venues. 9 | 10 | If you are being harassed, please contact one of [us](AUTHORS.md#maintainers) immediately so that we can support you. In case you cannot get in touch with us please write an email to [@ORG_NAME@](mailto:@ORG_EMAIL@). 11 | 12 | ## How can I help? 13 | 14 | _@NAME@_ follows a standard [fork and pull](https://help.github.com/articles/using-pull-requests/) model for contributions via GitHub pull requests. 15 | 16 | The process is simple: 17 | 18 | 1. Find something you want to work on 19 | 2. Let us know you are working on it via GitHub issues/pull requests 20 | 3. Implement your contribution 21 | 4. Write tests 22 | 5. Update the documentation 23 | 6. Submit pull request 24 | 25 | You will be automatically included in the [AUTHORS.md](AUTHORS.md#contributors) file as contributor in the next release. 26 | 27 | If you encounter any confusion or frustration during the contribution process, please create a GitHub issue and we'll do our best to improve the process. -------------------------------------------------------------------------------- /docs/NOTICE.md: -------------------------------------------------------------------------------- 1 | @NAME@ 2 | 3 | Copyright (c) @YEAR_RANGE@ @ORG_NAME@. All rights reserved. 4 | 5 | Licensed under @LICENSE@. See [LICENSE](LICENSE.md) for terms. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # unwrapped 2 | 3 | ## Getting started 4 | 5 | Unwrapped is an effects library for Scala 3 that introduces structured concurrency and an abilities system to describe pure functions and programs. 6 | 7 | The example below is a pure program that returns `Int` and requires the context capability `Bind`. Bind enables the `bind` syntax over values of Either and other types. 8 | 9 | ```scala mdoc:reset 10 | import unwrapped.* 11 | 12 | val program: Int = 13 | Right(1).bind + Right(2).bind 14 | ``` 15 | 16 | Using Scala3 features such as context functions we can encode pure programs in terms of capabilities with minimal overhead. 17 | Capabilities can be introduced a la carte and will be carried as given contextual evidences through call sites until you proof you can get rid of them. 18 | 19 | ```scala mdoc:reset 20 | import unwrapped.* 21 | 22 | def runProgram: Int | String = 23 | val program: Errors[String] ?=> Int = 24 | Right(1).bind + Right(2).bind + "oops".raise[Int] 25 | 26 | run(program) 27 | 28 | println(runProgram) 29 | ``` 30 | 31 | Users and library authors may define their own Capabilities. Here is how `Bind` for `Either[E, A]` is declared 32 | 33 | ```scala 34 | /** Brings the capability to perform Monad bind in place. Types may 35 | * access [[Control]] to short-circuit as necessary 36 | */ 37 | extension [R, A](fa: Either[R, A]) 38 | def bind: Errors[R] ?=> A = fa.fold(_.shift, identity) 39 | ``` 40 | 41 | Unwrapped supports a structured concurrency model backed by the non-blocking [StructuredExecutorTask](https://openjdk.java.net/jeps/428) 42 | where you can `fork` and `join` cancellable fibers and scopes. 43 | 44 | Popular functions like `parallel` support arbitrary typed arity in arguments and return types. 45 | 46 | ```scala mdoc:reset 47 | import unwrapped.* 48 | 49 | def runProgram: (String, Int, Double) = 50 | val results: Structured ?=> (String, Int, Double) = 51 | parallel( 52 | () => "1", 53 | () => 0, 54 | () => 47.03 55 | ) 56 | 57 | structured(results) 58 | 59 | println(runProgram) 60 | ``` 61 | 62 | Continuations based on Control Throwable or a non-blocking model like Loom are useful because they allow us to intermix async and sync programs in the same syntax without the need for boxing as is frequently the case in most scala effect libraries. 63 | 64 | ### Build and run in your local environment: 65 | 66 | Pre-requisites: 67 | 68 | - [Project Loom](https://jdk.java.net/loom/) 69 | - Scala 3 70 | 71 | 1. Download the latest Project Loom [Early-Access build](https://jdk.java.net/loom/) for your system architecture. 72 | 2. Set your `JAVA_HOME` to the path you extracted above. 73 | 74 | You can now compile and run the tests: 75 | 76 | ```shell 77 | env JAVA_OPTS='--add-modules jdk.incubator.concurrent' sbt "clean; compile; test" 78 | ``` 79 | 80 | **NOTE**: The Loom project is defined as an incubator module that is a means to distribute APIs which are not final or completed to get feedback from the developers. 81 | You should include the `-add-module` Java option to add the module to the class path of the project. 82 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/audio.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | 1d-interleaved-parityfec,audio/1d-interleaved-parityfec,[RFC6015] 3 | 32kadpcm,audio/32kadpcm,[RFC3802][RFC2421] 4 | 3gpp,audio/3gpp,[RFC3839][RFC6381] 5 | 3gpp2,audio/3gpp2,[RFC4393][RFC6381] 6 | aac,audio/aac,[ISO-IEC_JTC1][Max_Neuendorf] 7 | ac3,audio/ac3,[RFC4184] 8 | AMR,audio/AMR,[RFC4867] 9 | AMR-WB,audio/AMR-WB,[RFC4867] 10 | amr-wb+,audio/amr-wb+,[RFC4352] 11 | aptx,audio/aptx,[RFC7310] 12 | asc,audio/asc,[RFC6295] 13 | ATRAC-ADVANCED-LOSSLESS,audio/ATRAC-ADVANCED-LOSSLESS,[RFC5584] 14 | ATRAC-X,audio/ATRAC-X,[RFC5584] 15 | ATRAC3,audio/ATRAC3,[RFC5584] 16 | basic,audio/basic,[RFC2045][RFC2046] 17 | BV16,audio/BV16,[RFC4298] 18 | BV32,audio/BV32,[RFC4298] 19 | clearmode,audio/clearmode,[RFC4040] 20 | CN,audio/CN,[RFC3389] 21 | DAT12,audio/DAT12,[RFC3190] 22 | dls,audio/dls,[RFC4613] 23 | dsr-es201108,audio/dsr-es201108,[RFC3557] 24 | dsr-es202050,audio/dsr-es202050,[RFC4060] 25 | dsr-es202211,audio/dsr-es202211,[RFC4060] 26 | dsr-es202212,audio/dsr-es202212,[RFC4060] 27 | DV,audio/DV,[RFC6469] 28 | DVI4,audio/DVI4,[RFC4856] 29 | eac3,audio/eac3,[RFC4598] 30 | encaprtp,audio/encaprtp,[RFC6849] 31 | EVRC,audio/EVRC,[RFC4788] 32 | EVRC-QCP,audio/EVRC-QCP,[RFC3625] 33 | EVRC0,audio/EVRC0,[RFC4788] 34 | EVRC1,audio/EVRC1,[RFC4788] 35 | EVRCB,audio/EVRCB,[RFC5188] 36 | EVRCB0,audio/EVRCB0,[RFC5188] 37 | EVRCB1,audio/EVRCB1,[RFC4788] 38 | EVRCNW,audio/EVRCNW,[RFC6884] 39 | EVRCNW0,audio/EVRCNW0,[RFC6884] 40 | EVRCNW1,audio/EVRCNW1,[RFC6884] 41 | EVRCWB,audio/EVRCWB,[RFC5188] 42 | EVRCWB0,audio/EVRCWB0,[RFC5188] 43 | EVRCWB1,audio/EVRCWB1,[RFC5188] 44 | EVS,audio/EVS,[_3GPP][Kyunghun_Jung] 45 | example,audio/example,[RFC4735] 46 | flexfec,audio/flexfec,[RFC8627] 47 | fwdred,audio/fwdred,[RFC6354] 48 | G711-0,audio/G711-0,[RFC7655] 49 | G719,audio/G719,[RFC5404][RFC Errata 3245] 50 | G7221,audio/G7221,[RFC5577] 51 | G722,audio/G722,[RFC4856] 52 | G723,audio/G723,[RFC4856] 53 | G726-16,audio/G726-16,[RFC4856] 54 | G726-24,audio/G726-24,[RFC4856] 55 | G726-32,audio/G726-32,[RFC4856] 56 | G726-40,audio/G726-40,[RFC4856] 57 | G728,audio/G728,[RFC4856] 58 | G729,audio/G729,[RFC4856] 59 | G7291,audio/G7291,[RFC4749][RFC5459] 60 | G729D,audio/G729D,[RFC4856] 61 | G729E,audio/G729E,[RFC4856] 62 | GSM,audio/GSM,[RFC4856] 63 | GSM-EFR,audio/GSM-EFR,[RFC4856] 64 | GSM-HR-08,audio/GSM-HR-08,[RFC5993] 65 | iLBC,audio/iLBC,[RFC3952] 66 | ip-mr_v2.5,audio/ip-mr_v2.5,[RFC6262] 67 | L8,audio/L8,[RFC4856] 68 | L16,audio/L16,[RFC4856] 69 | L20,audio/L20,[RFC3190] 70 | L24,audio/L24,[RFC3190] 71 | LPC,audio/LPC,[RFC4856] 72 | MELP,audio/MELP,[RFC8130] 73 | MELP600,audio/MELP600,[RFC8130] 74 | MELP1200,audio/MELP1200,[RFC8130] 75 | MELP2400,audio/MELP2400,[RFC8130] 76 | mhas,audio/mhas,[ISO-IEC_JTC1][Nils_Peters][Ingo_Hofmann] 77 | mobile-xmf,audio/mobile-xmf,[RFC4723] 78 | MPA,audio/MPA,[RFC3555] 79 | mp4,audio/mp4,[RFC4337][RFC6381] 80 | MP4A-LATM,audio/MP4A-LATM,[RFC6416] 81 | mpa-robust,audio/mpa-robust,[RFC5219] 82 | mpeg,audio/mpeg,[RFC3003] 83 | mpeg4-generic,audio/mpeg4-generic,[RFC3640][RFC5691][RFC6295] 84 | ogg,audio/ogg,[RFC5334][RFC7845] 85 | opus,audio/opus,[RFC7587] 86 | parityfec,audio/parityfec,[RFC3009] 87 | PCMA,audio/PCMA,[RFC4856] 88 | PCMA-WB,audio/PCMA-WB,[RFC5391] 89 | PCMU,audio/PCMU,[RFC4856] 90 | PCMU-WB,audio/PCMU-WB,[RFC5391] 91 | prs.sid,audio/prs.sid,[Linus_Walleij] 92 | QCELP,audio/QCELP,[RFC3555][RFC3625] 93 | raptorfec,audio/raptorfec,[RFC6682] 94 | RED,audio/RED,[RFC3555] 95 | rtp-enc-aescm128,audio/rtp-enc-aescm128,[_3GPP] 96 | rtploopback,audio/rtploopback,[RFC6849] 97 | rtp-midi,audio/rtp-midi,[RFC6295] 98 | rtx,audio/rtx,[RFC4588] 99 | scip,audio/scip,[SCIP][Michael_Faller][Daniel_Hanson] 100 | SMV,audio/SMV,[RFC3558] 101 | SMV0,audio/SMV0,[RFC3558] 102 | SMV-QCP,audio/SMV-QCP,[RFC3625] 103 | sofa,audio/sofa,[AES][Piotr_Majdak] 104 | sp-midi,audio/sp-midi,[Timo_Kosonen][Tom_White] 105 | speex,audio/speex,[RFC5574] 106 | t140c,audio/t140c,[RFC4351] 107 | t38,audio/t38,[RFC4612] 108 | telephone-event,audio/telephone-event,[RFC4733] 109 | TETRA_ACELP,audio/TETRA_ACELP,[ETSI][Miguel_Angel_Reina_Ortega] 110 | TETRA_ACELP_BB,audio/TETRA_ACELP_BB,[ETSI][Miguel_Angel_Reina_Ortega] 111 | tone,audio/tone,[RFC4733] 112 | TSVCIS,audio/TSVCIS,[RFC8817] 113 | UEMCLIP,audio/UEMCLIP,[RFC5686] 114 | ulpfec,audio/ulpfec,[RFC5109] 115 | usac,audio/usac,[ISO-IEC_JTC1][Max_Neuendorf] 116 | VDVI,audio/VDVI,[RFC4856] 117 | VMR-WB,audio/VMR-WB,[RFC4348][RFC4424] 118 | vnd.3gpp.iufp,audio/vnd.3gpp.iufp,[Thomas_Belling] 119 | vnd.4SB,audio/vnd.4SB,[Serge_De_Jaham] 120 | vnd.audiokoz,audio/vnd.audiokoz,[Vicki_DeBarros] 121 | vnd.CELP,audio/vnd.CELP,[Serge_De_Jaham] 122 | vnd.cisco.nse,audio/vnd.cisco.nse,[Rajesh_Kumar] 123 | vnd.cmles.radio-events,audio/vnd.cmles.radio-events,[Jean-Philippe_Goulet] 124 | vnd.cns.anp1,audio/vnd.cns.anp1,[Ann_McLaughlin] 125 | vnd.cns.inf1,audio/vnd.cns.inf1,[Ann_McLaughlin] 126 | vnd.dece.audio,audio/vnd.dece.audio,[Michael_A_Dolan] 127 | vnd.digital-winds,audio/vnd.digital-winds,[Armands_Strazds] 128 | vnd.dlna.adts,audio/vnd.dlna.adts,[Edwin_Heredia] 129 | vnd.dolby.heaac.1,audio/vnd.dolby.heaac.1,[Steve_Hattersley] 130 | vnd.dolby.heaac.2,audio/vnd.dolby.heaac.2,[Steve_Hattersley] 131 | vnd.dolby.mlp,audio/vnd.dolby.mlp,[Mike_Ward] 132 | vnd.dolby.mps,audio/vnd.dolby.mps,[Steve_Hattersley] 133 | vnd.dolby.pl2,audio/vnd.dolby.pl2,[Steve_Hattersley] 134 | vnd.dolby.pl2x,audio/vnd.dolby.pl2x,[Steve_Hattersley] 135 | vnd.dolby.pl2z,audio/vnd.dolby.pl2z,[Steve_Hattersley] 136 | vnd.dolby.pulse.1,audio/vnd.dolby.pulse.1,[Steve_Hattersley] 137 | vnd.dra,audio/vnd.dra,[Jiang_Tian] 138 | vnd.dts,audio/vnd.dts,[William_Zou] 139 | vnd.dts.hd,audio/vnd.dts.hd,[William_Zou] 140 | vnd.dts.uhd,audio/vnd.dts.uhd,[Phillip_Maness] 141 | vnd.dvb.file,audio/vnd.dvb.file,[Peter_Siebert] 142 | vnd.everad.plj,audio/vnd.everad.plj,[Shay_Cicelsky] 143 | vnd.hns.audio,audio/vnd.hns.audio,[Swaminathan] 144 | vnd.lucent.voice,audio/vnd.lucent.voice,[Greg_Vaudreuil] 145 | vnd.ms-playready.media.pya,audio/vnd.ms-playready.media.pya,[Steve_DiAcetis] 146 | vnd.nokia.mobile-xmf,audio/vnd.nokia.mobile-xmf,[Nokia] 147 | vnd.nortel.vbk,audio/vnd.nortel.vbk,[Glenn_Parsons] 148 | vnd.nuera.ecelp4800,audio/vnd.nuera.ecelp4800,[Michael_Fox] 149 | vnd.nuera.ecelp7470,audio/vnd.nuera.ecelp7470,[Michael_Fox] 150 | vnd.nuera.ecelp9600,audio/vnd.nuera.ecelp9600,[Michael_Fox] 151 | vnd.octel.sbc,audio/vnd.octel.sbc,[Greg_Vaudreuil] 152 | vnd.presonus.multitrack,audio/vnd.presonus.multitrack,[Matthias_Juwan] 153 | vnd.qcelp - DEPRECATED in favor of audio/qcelp,audio/vnd.qcelp,[RFC3625] 154 | vnd.rhetorex.32kadpcm,audio/vnd.rhetorex.32kadpcm,[Greg_Vaudreuil] 155 | vnd.rip,audio/vnd.rip,[Martin_Dawe] 156 | vnd.sealedmedia.softseal.mpeg,audio/vnd.sealedmedia.softseal.mpeg,[David_Petersen] 157 | vnd.vmx.cvsd,audio/vnd.vmx.cvsd,[Greg_Vaudreuil] 158 | vorbis,audio/vorbis,[RFC5215] 159 | vorbis-config,audio/vorbis-config,[RFC5215] 160 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/font.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | collection,font/collection,[RFC8081] 3 | otf,font/otf,[RFC8081] 4 | sfnt,font/sfnt,[RFC8081] 5 | ttf,font/ttf,[RFC8081] 6 | woff,font/woff,[RFC8081] 7 | woff2,font/woff2,[RFC8081] 8 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/image.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | aces,image/aces,[SMPTE][Howard_Lukk] 3 | avci,image/avci,[ISO-IEC_JTC1][David_Singer] 4 | avcs,image/avcs,[ISO-IEC_JTC1][David_Singer] 5 | avif,image/avif,[Alliance_for_Open_Media][Cyril_Concolato] 6 | bmp,image/bmp,[RFC7903] 7 | cgm,image/cgm,[Alan_Francis] 8 | dicom-rle,image/dicom-rle,[DICOM_Standards_Committee][David_Clunie] 9 | dpx,image/dpx,[SMPTE][SMPTE_Director_of_Standards_Development] 10 | emf,image/emf,[RFC7903] 11 | example,image/example,[RFC4735] 12 | fits,image/fits,[RFC4047] 13 | g3fax,image/g3fax,[RFC1494] 14 | gif,,[RFC2045][RFC2046] 15 | heic,image/heic,[ISO-IEC_JTC1][David_Singer] 16 | heic-sequence,image/heic-sequence,[ISO-IEC_JTC1][David_Singer] 17 | heif,image/heif,[ISO-IEC_JTC1][David_Singer] 18 | heif-sequence,image/heif-sequence,[ISO-IEC_JTC1][David_Singer] 19 | hej2k,image/hej2k,[ISO-IEC_JTC1][ITU-T] 20 | hsj2,image/hsj2,[ISO-IEC_JTC1][ITU-T] 21 | ief,,[RFC1314] 22 | jls,image/jls,[DICOM_Standards_Committee][David_Clunie] 23 | jp2,image/jp2,[RFC3745] 24 | jpeg,,[RFC2045][RFC2046] 25 | jph,image/jph,[ISO-IEC_JTC1][ITU-T] 26 | jphc,image/jphc,[ISO-IEC_JTC1][ITU-T] 27 | jpm,image/jpm,[RFC3745] 28 | jpx,image/jpx,[RFC3745] 29 | jxr,image/jxr,[ISO-IEC_JTC1][ITU-T] 30 | jxrA,image/jxrA,[ISO-IEC_JTC1][ITU-T] 31 | jxrS,image/jxrS,[ISO-IEC_JTC1][ITU-T] 32 | jxs,image/jxs,[ISO-IEC_JTC1] 33 | jxsc,image/jxsc,[ISO-IEC_JTC1] 34 | jxsi,image/jxsi,[ISO-IEC_JTC1] 35 | jxss,image/jxss,[ISO-IEC_JTC1] 36 | ktx,image/ktx,[Khronos][Mark_Callow] 37 | ktx2,image/ktx2,[Khronos][Mark_Callow] 38 | naplps,image/naplps,[Ilya_Ferber] 39 | png,image/png,[W3C][PNG_Working_Group] 40 | prs.btif,image/prs.btif,[Ben_Simon] 41 | prs.pti,image/prs.pti,[Juern_Laun] 42 | pwg-raster,image/pwg-raster,[Michael_Sweet] 43 | svg+xml,image/svg+xml,[W3C][http://www.w3.org/TR/SVG/mimereg.html] 44 | t38,image/t38,[RFC3362] 45 | tiff,image/tiff,[RFC3302] 46 | tiff-fx,image/tiff-fx,[RFC3950] 47 | vnd.adobe.photoshop,image/vnd.adobe.photoshop,[Kim_Scarborough] 48 | vnd.airzip.accelerator.azv,image/vnd.airzip.accelerator.azv,[Gary_Clueit] 49 | vnd.cns.inf2,image/vnd.cns.inf2,[Ann_McLaughlin] 50 | vnd.dece.graphic,image/vnd.dece.graphic,[Michael_A_Dolan] 51 | vnd.djvu,image/vnd.djvu,[Leon_Bottou] 52 | vnd.dwg,image/vnd.dwg,[Jodi_Moline] 53 | vnd.dxf,image/vnd.dxf,[Jodi_Moline] 54 | vnd.dvb.subtitle,image/vnd.dvb.subtitle,[Peter_Siebert][Michael_Lagally] 55 | vnd.fastbidsheet,image/vnd.fastbidsheet,[Scott_Becker] 56 | vnd.fpx,image/vnd.fpx,[Marc_Douglas_Spencer] 57 | vnd.fst,image/vnd.fst,[Arild_Fuldseth] 58 | vnd.fujixerox.edmics-mmr,image/vnd.fujixerox.edmics-mmr,[Masanori_Onda] 59 | vnd.fujixerox.edmics-rlc,image/vnd.fujixerox.edmics-rlc,[Masanori_Onda] 60 | vnd.globalgraphics.pgb,image/vnd.globalgraphics.pgb,[Martin_Bailey] 61 | vnd.microsoft.icon,image/vnd.microsoft.icon,[Simon_Butcher] 62 | vnd.mix,image/vnd.mix,[Saveen_Reddy] 63 | vnd.ms-modi,image/vnd.ms-modi,[Gregory_Vaughan] 64 | vnd.mozilla.apng,image/vnd.mozilla.apng,[Stuart_Parmenter] 65 | vnd.net-fpx,image/vnd.net-fpx,[Marc_Douglas_Spencer] 66 | vnd.pco.b16,image/vnd.pco.b16,[PCO_AG][Jan_Zeman] 67 | vnd.radiance,image/vnd.radiance,[Randolph_Fritz][Greg_Ward] 68 | vnd.sealed.png,image/vnd.sealed.png,[David_Petersen] 69 | vnd.sealedmedia.softseal.gif,image/vnd.sealedmedia.softseal.gif,[David_Petersen] 70 | vnd.sealedmedia.softseal.jpg,image/vnd.sealedmedia.softseal.jpg,[David_Petersen] 71 | vnd.svf,image/vnd.svf,[Jodi_Moline] 72 | vnd.tencent.tap,image/vnd.tencent.tap,[Ni_Hui] 73 | vnd.valve.source.texture,image/vnd.valve.source.texture,[Henrik_Andersson] 74 | vnd.wap.wbmp,image/vnd.wap.wbmp,[Peter_Stark] 75 | vnd.xiff,image/vnd.xiff,[Steven_Martin] 76 | vnd.zbrush.pcx,image/vnd.zbrush.pcx,[Chris_Charabaruk] 77 | wmf,image/wmf,[RFC7903] 78 | x-emf - DEPRECATED in favor of image/emf,image/emf,[RFC7903] 79 | x-wmf - DEPRECATED in favor of image/wmf,image/wmf,[RFC7903] 80 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/message.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | bhttp,message/bhttp,[RFC-ietf-httpbis-binary-message-06] 3 | CPIM,message/CPIM,[RFC3862] 4 | delivery-status,message/delivery-status,[RFC1894] 5 | disposition-notification,message/disposition-notification,[RFC8098] 6 | example,message/example,[RFC4735] 7 | external-body,,[RFC2045][RFC2046] 8 | feedback-report,message/feedback-report,[RFC5965] 9 | global,message/global,[RFC6532] 10 | global-delivery-status,message/global-delivery-status,[RFC6533] 11 | global-disposition-notification,message/global-disposition-notification,[RFC6533] 12 | global-headers,message/global-headers,[RFC6533] 13 | http,message/http,[RFC9112] 14 | imdn+xml,message/imdn+xml,[RFC5438] 15 | news (OBSOLETED by [RFC5537]),message/news,[RFC5537][Henry_Spencer] 16 | partial,,[RFC2045][RFC2046] 17 | rfc822,,[RFC2045][RFC2046] 18 | s-http (OBSOLETE),message/s-http,[RFC2660][status-change-http-experiments-to-historic] 19 | sip,message/sip,[RFC3261] 20 | sipfrag,message/sipfrag,[RFC3420] 21 | tracking-status,message/tracking-status,[RFC3886] 22 | vnd.si.simp (OBSOLETED by request),message/vnd.si.simp,[Nicholas_Parks_Young] 23 | vnd.wfa.wsc,message/vnd.wfa.wsc,[Mick_Conley] 24 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/model.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | 3mf,model/3mf,[http://www.3mf.io/specification][_3MF][Michael_Sweet] 3 | e57,model/e57,[ASTM] 4 | example,model/example,[RFC4735] 5 | gltf-binary,model/gltf-binary,[Khronos][Saurabh_Bhatia] 6 | gltf+json,model/gltf+json,[Khronos][Uli_Klumpp] 7 | iges,model/iges,[Curtis_Parks] 8 | mesh,,[RFC2077] 9 | mtl,model/mtl,[DICOM_Standards_Committee][Luiza_Kowalczyk] 10 | obj,model/obj,[DICOM_Standards_Committee][Luiza_Kowalczyk] 11 | prc,model/prc,[ISO-TC_171-SC_2][Betsy_Fanning] 12 | step,model/step,[ISO-TC_184-SC_4][Dana_Tripp] 13 | step+xml,model/step+xml,[ISO-TC_184-SC_4][Dana_Tripp] 14 | step+zip,model/step+zip,[ISO-TC_184-SC_4][Dana_Tripp] 15 | step-xml+zip,model/step-xml+zip,[ISO-TC_184-SC_4][Dana_Tripp] 16 | stl,model/stl,[DICOM_Standards_Committee][Lisa_Spellman] 17 | u3d,model/u3d,[PDF_Association][Peter_Wyatt] 18 | vnd.collada+xml,model/vnd.collada+xml,[James_Riordon] 19 | vnd.dwf,model/vnd.dwf,[Jason_Pratt] 20 | vnd.flatland.3dml,model/vnd.flatland.3dml,[Michael_Powers] 21 | vnd.gdl,model/vnd.gdl,[Attila_Babits] 22 | vnd.gs-gdl,model/vnd.gs-gdl,[Attila_Babits] 23 | vnd.gtw,model/vnd.gtw,[Yutaka_Ozaki] 24 | vnd.moml+xml,model/vnd.moml+xml,[Christopher_Brooks] 25 | vnd.mts,model/vnd.mts,[Boris_Rabinovitch] 26 | vnd.opengex,model/vnd.opengex,[Eric_Lengyel] 27 | vnd.parasolid.transmit.binary,model/vnd.parasolid.transmit.binary,[Parasolid] 28 | vnd.parasolid.transmit.text,model/vnd.parasolid.transmit.text,[Parasolid] 29 | vnd.pytha.pyox,model/vnd.pytha.pyox,[Daniel_Flassig] 30 | vnd.rosette.annotated-data-model,model/vnd.rosette.annotated-data-model,[Benson_Margulies] 31 | vnd.sap.vds,model/vnd.sap.vds,[SAP_SE][Igor_Afanasyev] 32 | vnd.usdz+zip,model/vnd.usdz+zip,[Sebastian_Grassia] 33 | vnd.valve.source.compiled-map,model/vnd.valve.source.compiled-map,[Henrik_Andersson] 34 | vnd.vtu,model/vnd.vtu,[Boris_Rabinovitch] 35 | vrml,,[RFC2077] 36 | x3d-vrml,model/x3d-vrml,[Web3D][Web3D_X3D] 37 | x3d+fastinfoset,model/x3d+fastinfoset,[Web3D_X3D] 38 | x3d+xml,model/x3d+xml,[Web3D][Web3D_X3D] 39 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/multipart.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | alternative,,[RFC2046][RFC2045] 3 | appledouble,multipart/appledouble,[Patrik_Faltstrom] 4 | byteranges,multipart/byteranges,[RFC9110] 5 | digest,,[RFC2046][RFC2045] 6 | encrypted,multipart/encrypted,[RFC1847] 7 | example,multipart/example,[RFC4735] 8 | form-data,multipart/form-data,[RFC7578] 9 | header-set,multipart/header-set,[Dave_Crocker] 10 | mixed,,[RFC2046][RFC2045] 11 | multilingual,multipart/multilingual,[RFC8255] 12 | parallel,,[RFC2046][RFC2045] 13 | related,multipart/related,[RFC2387] 14 | report,multipart/report,[RFC6522] 15 | signed,multipart/signed,[RFC1847] 16 | vnd.bint.med-plus,multipart/vnd.bint.med-plus,[Heinz-Peter_Schütz] 17 | voice-message,multipart/voice-message,[RFC3801] 18 | x-mixed-replace,multipart/x-mixed-replace,[W3C][Robin_Berjon] 19 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/text.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | 1d-interleaved-parityfec,text/1d-interleaved-parityfec,[RFC6015] 3 | cache-manifest,text/cache-manifest,[W3C][Robin_Berjon] 4 | calendar,text/calendar,[RFC5545] 5 | cql,text/cql,[HL7][Bryn_Rhodes] 6 | cql-expression,text/cql-expression,[HL7][Bryn_Rhodes] 7 | cql-identifier,text/cql-identifier,[HL7][Bryn_Rhodes] 8 | css,text/css,[RFC2318] 9 | csv,text/csv,[RFC4180][RFC7111] 10 | csv-schema,text/csv-schema,[National_Archives_UK][David_Underdown] 11 | directory - DEPRECATED by RFC6350,text/directory,[RFC2425][RFC6350] 12 | dns,text/dns,[RFC4027] 13 | ecmascript (OBSOLETED in favor of text/javascript),text/ecmascript,[RFC9239] 14 | encaprtp,text/encaprtp,[RFC6849] 15 | enriched,,[RFC1896] 16 | example,text/example,[RFC4735] 17 | fhirpath,text/fhirpath,[HL7][Bryn_Rhodes] 18 | flexfec,text/flexfec,[RFC8627] 19 | fwdred,text/fwdred,[RFC6354] 20 | gff3,text/gff3,[Sequence_Ontology] 21 | grammar-ref-list,text/grammar-ref-list,[RFC6787] 22 | hl7v2,text/hl7v2,[HL7][Marc_Duteau] 23 | html,text/html,[W3C][Robin_Berjon] 24 | javascript,text/javascript,[RFC9239] 25 | jcr-cnd,text/jcr-cnd,[Peeter_Piegaze] 26 | markdown,text/markdown,[RFC7763] 27 | mizar,text/mizar,[Jesse_Alama] 28 | n3,text/n3,[W3C][Eric_Prudhommeaux] 29 | parameters,text/parameters,[RFC7826] 30 | parityfec,text/parityfec,[RFC3009] 31 | plain,,[RFC2046][RFC3676][RFC5147] 32 | provenance-notation,text/provenance-notation,[W3C][Ivan_Herman] 33 | prs.fallenstein.rst,text/prs.fallenstein.rst,[Benja_Fallenstein] 34 | prs.lines.tag,text/prs.lines.tag,[John_Lines] 35 | prs.prop.logic,text/prs.prop.logic,[Hans-Dieter_A._Hiep] 36 | raptorfec,text/raptorfec,[RFC6682] 37 | RED,text/RED,[RFC4102] 38 | rfc822-headers,text/rfc822-headers,[RFC6522] 39 | richtext,,[RFC2045][RFC2046] 40 | rtf,text/rtf,[Paul_Lindner] 41 | rtp-enc-aescm128,text/rtp-enc-aescm128,[_3GPP] 42 | rtploopback,text/rtploopback,[RFC6849] 43 | rtx,text/rtx,[RFC4588] 44 | SGML,text/SGML,[RFC1874] 45 | shaclc,text/shaclc,[W3C_SHACL_Community_Group][Vladimir_Alexiev] 46 | shex,text/shex,[W3C][Eric_Prudhommeaux] 47 | spdx,text/spdx,[Linux_Foundation][Rose_Judge] 48 | strings,text/strings,[IEEE-ISTO-PWG-PPP] 49 | t140,text/t140,[RFC4103] 50 | tab-separated-values,text/tab-separated-values,[Paul_Lindner] 51 | troff,text/troff,[RFC4263] 52 | turtle,text/turtle,[W3C][Eric_Prudhommeaux] 53 | ulpfec,text/ulpfec,[RFC5109] 54 | uri-list,text/uri-list,[RFC2483] 55 | vcard,text/vcard,[RFC6350] 56 | vnd.a,text/vnd.a,[Regis_Dehoux] 57 | vnd.abc,text/vnd.abc,[Steve_Allen] 58 | vnd.ascii-art,text/vnd.ascii-art,[Kim_Scarborough] 59 | vnd.curl,text/vnd.curl,[Robert_Byrnes] 60 | vnd.debian.copyright,text/vnd.debian.copyright,[Charles_Plessy] 61 | vnd.DMClientScript,text/vnd.DMClientScript,[Dan_Bradley] 62 | vnd.dvb.subtitle,text/vnd.dvb.subtitle,[Peter_Siebert][Michael_Lagally] 63 | vnd.esmertec.theme-descriptor,text/vnd.esmertec.theme-descriptor,[Stefan_Eilemann] 64 | vnd.exchangeable,text/vnd.exchangeable,[Martin_Cizek] 65 | vnd.familysearch.gedcom,text/vnd.familysearch.gedcom,[Gordon_Clarke] 66 | vnd.ficlab.flt,text/vnd.ficlab.flt,[Steve_Gilberd] 67 | vnd.fly,text/vnd.fly,[John-Mark_Gurney] 68 | vnd.fmi.flexstor,text/vnd.fmi.flexstor,[Kari_E._Hurtta] 69 | vnd.gml,text/vnd.gml,[Mi_Tar] 70 | vnd.graphviz,text/vnd.graphviz,[John_Ellson] 71 | vnd.hans,text/vnd.hans,[Hill_Hanxv] 72 | vnd.hgl,text/vnd.hgl,[Heungsub_Lee] 73 | vnd.in3d.3dml,text/vnd.in3d.3dml,[Michael_Powers] 74 | vnd.in3d.spot,text/vnd.in3d.spot,[Michael_Powers] 75 | vnd.IPTC.NewsML,text/vnd.IPTC.NewsML,[IPTC] 76 | vnd.IPTC.NITF,text/vnd.IPTC.NITF,[IPTC] 77 | vnd.latex-z,text/vnd.latex-z,[Mikusiak_Lubos] 78 | vnd.motorola.reflex,text/vnd.motorola.reflex,[Mark_Patton] 79 | vnd.ms-mediapackage,text/vnd.ms-mediapackage,[Jan_Nelson] 80 | vnd.net2phone.commcenter.command,text/vnd.net2phone.commcenter.command,[Feiyu_Xie] 81 | vnd.radisys.msml-basic-layout,text/vnd.radisys.msml-basic-layout,[RFC5707] 82 | vnd.senx.warpscript,text/vnd.senx.warpscript,[Pierre_Papin] 83 | vnd.si.uricatalogue (OBSOLETED by request),text/vnd.si.uricatalogue,[Nicholas_Parks_Young] 84 | vnd.sun.j2me.app-descriptor,text/vnd.sun.j2me.app-descriptor,[Gary_Adams] 85 | vnd.sosi,text/vnd.sosi,[Petter_Reinholdtsen] 86 | vnd.trolltech.linguist,text/vnd.trolltech.linguist,[David_Lee_Lambert] 87 | vnd.wap.si,text/vnd.wap.si,[WAP-Forum] 88 | vnd.wap.sl,text/vnd.wap.sl,[WAP-Forum] 89 | vnd.wap.wml,text/vnd.wap.wml,[Peter_Stark] 90 | vnd.wap.wmlscript,text/vnd.wap.wmlscript,[Peter_Stark] 91 | vtt,text/vtt,[W3C][Silvia_Pfeiffer] 92 | xml,text/xml,[RFC7303] 93 | xml-external-parsed-entity,text/xml-external-parsed-entity,[RFC7303] 94 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/mediaTypes/video.csv: -------------------------------------------------------------------------------- 1 | Name,Template,Reference 2 | 1d-interleaved-parityfec,video/1d-interleaved-parityfec,[RFC6015] 3 | 3gpp,video/3gpp,[RFC3839][RFC6381] 4 | 3gpp2,video/3gpp2,[RFC4393][RFC6381] 5 | 3gpp-tt,video/3gpp-tt,[RFC4396] 6 | AV1,video/AV1,[Alliance_for_Open_Media] 7 | BMPEG,video/BMPEG,[RFC3555] 8 | BT656,video/BT656,[RFC3555] 9 | CelB,video/CelB,[RFC3555] 10 | DV,video/DV,[RFC6469] 11 | encaprtp,video/encaprtp,[RFC6849] 12 | example,video/example,[RFC4735] 13 | FFV1,video/FFV1,[RFC9043] 14 | flexfec,video/flexfec,[RFC8627] 15 | H261,video/H261,[RFC4587] 16 | H263,video/H263,[RFC3555] 17 | H263-1998,video/H263-1998,[RFC4629] 18 | H263-2000,video/H263-2000,[RFC4629] 19 | H264,video/H264,[RFC6184] 20 | H264-RCDO,video/H264-RCDO,[RFC6185] 21 | H264-SVC,video/H264-SVC,[RFC6190] 22 | H265,video/H265,[RFC7798] 23 | iso.segment,video/iso.segment,[David_Singer][ISO-IEC_JTC1] 24 | JPEG,video/JPEG,[RFC3555] 25 | jpeg2000,video/jpeg2000,[RFC5371][RFC5372] 26 | jxsv,video/jxsv,[RFC9134] 27 | mj2,video/mj2,[RFC3745] 28 | MP1S,video/MP1S,[RFC3555] 29 | MP2P,video/MP2P,[RFC3555] 30 | MP2T,video/MP2T,[RFC3555] 31 | mp4,video/mp4,[RFC4337][RFC6381] 32 | MP4V-ES,video/MP4V-ES,[RFC6416] 33 | MPV,video/MPV,[RFC3555] 34 | mpeg,,[RFC2045][RFC2046] 35 | mpeg4-generic,video/mpeg4-generic,[RFC3640] 36 | nv,video/nv,[RFC4856] 37 | ogg,video/ogg,[RFC5334][RFC7845] 38 | parityfec,video/parityfec,[RFC3009] 39 | pointer,video/pointer,[RFC2862] 40 | quicktime,video/quicktime,[RFC6381][Paul_Lindner] 41 | raptorfec,video/raptorfec,[RFC6682] 42 | raw,video/raw,[RFC4175] 43 | rtp-enc-aescm128,video/rtp-enc-aescm128,[_3GPP] 44 | rtploopback,video/rtploopback,[RFC6849] 45 | rtx,video/rtx,[RFC4588] 46 | scip,video/scip,[SCIP][Michael_Faller][Daniel_Hanson] 47 | smpte291,video/smpte291,[RFC8331] 48 | SMPTE292M,video/SMPTE292M,[RFC3497] 49 | ulpfec,video/ulpfec,[RFC5109] 50 | vc1,video/vc1,[RFC4425] 51 | vc2,video/vc2,[RFC8450] 52 | vnd.CCTV,video/vnd.CCTV,[Frank_Rottmann] 53 | vnd.dece.hd,video/vnd.dece.hd,[Michael_A_Dolan] 54 | vnd.dece.mobile,video/vnd.dece.mobile,[Michael_A_Dolan] 55 | vnd.dece.mp4,video/vnd.dece.mp4,[Michael_A_Dolan] 56 | vnd.dece.pd,video/vnd.dece.pd,[Michael_A_Dolan] 57 | vnd.dece.sd,video/vnd.dece.sd,[Michael_A_Dolan] 58 | vnd.dece.video,video/vnd.dece.video,[Michael_A_Dolan] 59 | vnd.directv.mpeg,video/vnd.directv.mpeg,[Nathan_Zerbe] 60 | vnd.directv.mpeg-tts,video/vnd.directv.mpeg-tts,[Nathan_Zerbe] 61 | vnd.dlna.mpeg-tts,video/vnd.dlna.mpeg-tts,[Edwin_Heredia] 62 | vnd.dvb.file,video/vnd.dvb.file,[Peter_Siebert][Kevin_Murray] 63 | vnd.fvt,video/vnd.fvt,[Arild_Fuldseth] 64 | vnd.hns.video,video/vnd.hns.video,[Swaminathan] 65 | vnd.iptvforum.1dparityfec-1010,video/vnd.iptvforum.1dparityfec-1010,[Shuji_Nakamura] 66 | vnd.iptvforum.1dparityfec-2005,video/vnd.iptvforum.1dparityfec-2005,[Shuji_Nakamura] 67 | vnd.iptvforum.2dparityfec-1010,video/vnd.iptvforum.2dparityfec-1010,[Shuji_Nakamura] 68 | vnd.iptvforum.2dparityfec-2005,video/vnd.iptvforum.2dparityfec-2005,[Shuji_Nakamura] 69 | vnd.iptvforum.ttsavc,video/vnd.iptvforum.ttsavc,[Shuji_Nakamura] 70 | vnd.iptvforum.ttsmpeg2,video/vnd.iptvforum.ttsmpeg2,[Shuji_Nakamura] 71 | vnd.motorola.video,video/vnd.motorola.video,[Tom_McGinty] 72 | vnd.motorola.videop,video/vnd.motorola.videop,[Tom_McGinty] 73 | vnd.mpegurl,video/vnd.mpegurl,[Heiko_Recktenwald] 74 | vnd.ms-playready.media.pyv,video/vnd.ms-playready.media.pyv,[Steve_DiAcetis] 75 | vnd.nokia.interleaved-multimedia,video/vnd.nokia.interleaved-multimedia,[Petteri_Kangaslampi] 76 | vnd.nokia.mp4vr,video/vnd.nokia.mp4vr,[Miska_M._Hannuksela] 77 | vnd.nokia.videovoip,video/vnd.nokia.videovoip,[Nokia] 78 | vnd.objectvideo,video/vnd.objectvideo,[John_Clark] 79 | vnd.radgamettools.bink,video/vnd.radgamettools.bink,[Henrik_Andersson] 80 | vnd.radgamettools.smacker,video/vnd.radgamettools.smacker,[Henrik_Andersson] 81 | vnd.sealed.mpeg1,video/vnd.sealed.mpeg1,[David_Petersen] 82 | vnd.sealed.mpeg4,video/vnd.sealed.mpeg4,[David_Petersen] 83 | vnd.sealed.swf,video/vnd.sealed.swf,[David_Petersen] 84 | vnd.sealedmedia.softseal.mov,video/vnd.sealedmedia.softseal.mov,[David_Petersen] 85 | vnd.uvvu.mp4,video/vnd.uvvu.mp4,[Michael_A_Dolan] 86 | vnd.youtube.yt,video/vnd.youtube.yt,[Google] 87 | vnd.vivo,video/vnd.vivo,[John_Wolfe] 88 | VP8,video/VP8,[RFC7741] 89 | VP9,video/VP9,[RFC-ietf-payload-vp9-16] 90 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpBodyMapper.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.io.InputStream 4 | import java.net.http.HttpRequest.BodyPublisher 5 | import java.net.http.HttpRequest.BodyPublishers 6 | import java.nio.ByteBuffer 7 | import java.nio.file.Path 8 | import java.util.concurrent.SubmissionPublisher 9 | import java.nio.charset.StandardCharsets 10 | import java.util.concurrent.Executors 11 | import java.util.concurrent.Flow.Publisher 12 | 13 | /** 14 | * Require Http effect to work with body publisher and mediaType. This keeps everything within 15 | * the continuation control dependency. 16 | */ 17 | trait HttpBodyMapper[B]: 18 | def bodyPublisher(b: B): BodyPublisher 19 | def mediaType: MediaType 20 | 21 | object HttpBodyMapper extends HttpBodyMapperLowPriority: 22 | 23 | given HttpBodyMapper[String] with 24 | def mediaType: MediaType = MediaType(MediaTypes.text.`plain`.value ++ "; charset=utf-8") 25 | def bodyPublisher(s: String): BodyPublisher = 26 | BodyPublishers.ofString(s) 27 | 28 | given HttpBodyMapper[Array[Byte]] with 29 | 30 | override def mediaType: MediaType = MediaTypes.application.`octet-stream` 31 | 32 | def bodyPublisher(b: Array[Byte]): BodyPublisher = 33 | BodyPublishers.ofByteArray(b) 34 | 35 | given HttpBodyMapper[Path] with 36 | 37 | override def mediaType: MediaType = MediaTypes.application.`octet-stream` 38 | 39 | def bodyPublisher(p: Path): BodyPublisher = 40 | BodyPublishers.ofFile(p) 41 | 42 | given HttpBodyMapper[InputStream] with 43 | 44 | override def mediaType: MediaType = MediaTypes.application.`octet-stream` 45 | 46 | def bodyPublisher(i: InputStream): BodyPublisher = 47 | BodyPublishers.ofInputStream(() => i) 48 | 49 | /** 50 | * A streaming byte body publisher that sends bytes one at a time, always re-sending when a 51 | * byte is dropped by the receiving subscriber 52 | */ 53 | given HttpBodyMapper[Receive[Byte]] with 54 | 55 | override def mediaType: MediaType = MediaTypes.application.`octet-stream` 56 | 57 | def bodyPublisher(b: Receive[Byte]): BodyPublisher = 58 | new BodyPublisher: 59 | def contentLength(): Long = 60 | -1L 61 | 62 | def subscribe( 63 | subscriber: java.util.concurrent.Flow.Subscriber[? >: java.nio.ByteBuffer]): Unit = 64 | b.grouped(1024) 65 | .transform { bytes => subscriber.onNext(ByteBuffer.wrap(bytes.toArray)) } 66 | .toList 67 | 68 | trait HttpBodyMapperLowPriority: 69 | given HttpBodyMapper[Any] with 70 | 71 | override def mediaType: MediaType = MediaTypes.application.`octet-stream` 72 | 73 | def bodyPublisher(a: Any): BodyPublisher = 74 | BodyPublishers.noBody() 75 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpClient.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.net.URI 4 | import java.net.{http => jnh} 5 | import java.time.Duration 6 | import java.net.http.HttpRequest 7 | 8 | opaque type HttpClient = jnh.HttpClient 9 | 10 | extension (h: HttpClient) def client: jnh.HttpClient = h 11 | 12 | object HttpClient: 13 | System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Content-Length") 14 | def apply(a: jnh.HttpClient): HttpClient = a 15 | def apply(using config: HttpClientConfig): HttpClient = 16 | // need to setup proxy and ssl contexts 17 | jnh 18 | .HttpClient 19 | .newBuilder() 20 | .connectTimeout(Duration.ofSeconds( 21 | config.connectionTimeout.getOrElse(summon[HttpConnectionTimeout]).value)) 22 | .followRedirects(config.followRedirects.getOrElse(summon[HttpFollowRedirects]).value) 23 | .version(jnh.HttpClient.Version.HTTP_2) 24 | .build() 25 | 26 | given HttpClient = apply 27 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpClientConfig.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Defines the client configuration options for an http client. Not open for extension. 5 | */ 6 | final class HttpClientConfig( 7 | val connectionTimeout: Nullable[HttpConnectionTimeout], 8 | val followRedirects: Nullable[HttpFollowRedirects], 9 | val maximumRetries: Nullable[HttpRetries] 10 | ) 11 | 12 | object HttpClientConfig: 13 | given HttpClientConfig = 14 | HttpClientConfig(Nullable.none, Nullable.none, Nullable.none) 15 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpConnectionTimeout.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models http connection timeouts as finite durations 5 | */ 6 | opaque type HttpConnectionTimeout = Long 7 | 8 | object HttpConnectionTimeout: 9 | 10 | given Manifest[HttpConnectionTimeout] = 11 | Manifest.classType(HttpConnectionTimeout(1).getClass) 12 | 13 | /** 14 | * @constructor 15 | */ 16 | inline def apply(durationInSeconds: Long): HttpConnectionTimeout = 17 | requires(durationInSeconds > 0, "Durations must be positive", durationInSeconds) 18 | 19 | def of(durationInSeconds: Long)(using Errors[String]): HttpConnectionTimeout = 20 | if (durationInSeconds > 0) 21 | durationInSeconds 22 | else 23 | "Durations must be positive".shift 24 | 25 | /** 26 | * Default connection timeout is 30 seconds 27 | */ 28 | given defaultHttpConnectionTimeout: HttpConnectionTimeout = 29 | HttpConnectionTimeout(30) 30 | 31 | extension (h: HttpConnectionTimeout) 32 | /** 33 | * @return 34 | * The long value of the HttpConnectionTimeout 35 | */ 36 | def value: Long = h 37 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpExecutionException.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | type HttpExecutionException = Exception 4 | 5 | object HttpExecutionException { 6 | def apply(a: Exception): HttpExecutionException = a 7 | } 8 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpFollowRedirects.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.net.http.HttpClient.Redirect 4 | 5 | opaque type HttpFollowRedirects = Redirect 6 | 7 | object HttpFollowRedirects: 8 | given Manifest[HttpFollowRedirects] = 9 | Manifest.classType(Redirect.ALWAYS.getClass) 10 | 11 | val always: HttpFollowRedirects = Redirect.ALWAYS 12 | val never: HttpFollowRedirects = Redirect.NEVER 13 | val normal: HttpFollowRedirects = Redirect.NORMAL 14 | given HttpFollowRedirects = normal 15 | extension (h: HttpFollowRedirects) def value: Redirect = h 16 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpHeader.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | type HttpHeader = (String, ::[String]) 4 | 5 | extension (header: HttpHeader) 6 | def name: String = header._1 7 | def values: ::[String] = header._2 8 | 9 | object HttpHeader: 10 | def apply(header: (String, ::[String])): HttpHeader = 11 | header 12 | 13 | def apply(headerName: String, value: String, values: String*): HttpHeader = 14 | headerName -> ::(value, values.toList) 15 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpResponseMapper.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.net.http.HttpResponse 4 | import java.net.http.HttpResponse.BodyHandlers 5 | import java.io.InputStream 6 | import java.nio.file.Path 7 | import java.nio.file.OpenOption 8 | import java.net.http.HttpHeaders 9 | import java.net.http.HttpResponse.BodyHandler 10 | import java.net.http.HttpResponse.ResponseInfo 11 | import java.util.concurrent.Flow 12 | import java.util.concurrent.CompletionStage 13 | import java.util.concurrent.CompletableFuture 14 | import java.util.concurrent.ConcurrentLinkedQueue 15 | import java.util.concurrent.atomic.AtomicBoolean 16 | import java.util.concurrent.Callable 17 | import java.util.concurrent.Executors 18 | import scala.jdk.FunctionConverters.* 19 | import scala.jdk.CollectionConverters.* 20 | import java.util.concurrent.TimeUnit 21 | import java.util.concurrent.atomic.AtomicReference 22 | import java.nio.ByteBuffer 23 | import java.io.StringWriter 24 | import java.io.PrintWriter 25 | 26 | trait HttpResponseMapper[A]: 27 | def bodyHandler: HttpResponse.BodyHandler[A] 28 | 29 | object HttpResponseMapper: 30 | 31 | given HttpResponseMapper[T: Serde]: HttpResponseMapper[T] = new HttpResponseMapper[T] { 32 | def bodyHandler: BodyHandler[T] = new HttpResponse.BodyHandler[T] { 33 | def apply(responseInfo: HttpResponse.ResponseInfo) = new HttpResponse.BodySubscriber[T] { 34 | val accumulator = new AtomicReference(List[ByteBuffer]()) 35 | val result = new CompletableFuture[T]() 36 | override def getBody(): CompletionStage[T] = 37 | result 38 | override def onComplete(): Unit = 39 | try { // getting the value can actually fail 40 | val x = Serde[T]().deserialize(accumulator.get()) 41 | x match { 42 | case errors @ h :: t => 43 | onError( 44 | HttpExecutionException( 45 | new RuntimeException( 46 | s"Errors deserializing http response: ${errors.mkString("\n")}"))) 47 | case _ => result.complete(x.asInstanceOf[T]) 48 | } 49 | } catch { 50 | case ex: Throwable => 51 | onError(HttpExecutionException(RuntimeException(ex.getMessage(), ex))) 52 | } 53 | override def onError(ex: Throwable): Unit = 54 | result.completeExceptionally(ex) 55 | override def onNext(byteBuffers: java.util.List[java.nio.ByteBuffer]): Unit = 56 | accumulator.updateAndGet(current => current ++ byteBuffers.asScala.toList) 57 | () 58 | override def onSubscribe(subscription: Flow.Subscription): Unit = 59 | subscription.request(Long.MaxValue) 60 | } 61 | } 62 | } 63 | given HttpResponseMapper[Void] with 64 | def bodyHandler = BodyHandlers.discarding 65 | 66 | given HttpResponseMapper[String] with 67 | def bodyHandler = BodyHandlers.ofString() 68 | 69 | given HttpResponseMapper[Array[Byte]] with 70 | def bodyHandler = BodyHandlers.ofByteArray() 71 | 72 | given HttpResponseMapper[InputStream] with 73 | def bodyHandler = BodyHandlers.buffering(BodyHandlers.ofInputStream(), 4096) 74 | 75 | given HttpResponseReceiveMapper: HttpResponseMapper[unwrapped.Receive[Byte]] = 76 | new HttpResponseMapper[unwrapped.Receive[Byte]] { 77 | def bodyHandler: BodyHandler[unwrapped.Receive[Byte]] = BodyHandlers.buffering( 78 | new BodyHandler[unwrapped.Receive[Byte]] { 79 | def apply(responseInfo: HttpResponse.ResponseInfo) 80 | : HttpResponse.BodySubscriber[unwrapped.Receive[Byte]] = 81 | new HttpResponse.BodySubscriber[unwrapped.Receive[Byte]] { 82 | val debug = false 83 | def printDebugMessage(message: String): Unit = 84 | if (debug) 85 | println(message) 86 | else 87 | () 88 | val queue = new ConcurrentLinkedQueue[Byte] {} 89 | val err: AtomicReference[Throwable] = new AtomicReference() 90 | val executor = Executors.newVirtualThreadPerTaskExecutor 91 | val shutdownExecutor = Executors.newScheduledThreadPool(1) 92 | val isDone = AtomicBoolean(false) 93 | def getBody(): CompletionStage[unwrapped.Receive[Byte]] = 94 | CompletableFuture.supplyAsync( 95 | () => 96 | streamed { 97 | def loop(b: Boolean): Unit = { 98 | printDebugMessage(s"getBody:loop: $b") 99 | if (b) 100 | val ex = err.get 101 | if (ex != null) 102 | printDebugMessage(s"getBody:loop:${ex.getMessage()}") 103 | shutdown(true) 104 | throw ex 105 | else 106 | Nullable(queue.poll) 107 | .map { byte => 108 | printDebugMessage(s"getBody:loop:byte:${byte.toInt}") 109 | send(byte) 110 | } 111 | .getOrElse(()) 112 | printDebugMessage( 113 | s"getBody:loop:yielding on ${Thread.currentThread.getName}") 114 | Thread.`yield` 115 | printDebugMessage("getBody:loop:resuming loop") 116 | loop(!isDone.get()) 117 | else () 118 | } 119 | loop(!isDone.get()) 120 | }, 121 | executor 122 | ) 123 | 124 | private def shutdown(now: Boolean): Unit = 125 | if (queue.size > 0 && !now) { 126 | printDebugMessage(s"shutdown: items remain in queue to be sent, waiting...") 127 | shutdownExecutor.schedule( 128 | new Runnable { 129 | override def run() = shutdown(false) 130 | }, 131 | 10, 132 | TimeUnit.MILLISECONDS) 133 | } else { 134 | printDebugMessage("shutdown:shutting down") 135 | isDone.set(true) 136 | try 137 | if (!executor.isShutdown()) 138 | executor.awaitTermination(10, TimeUnit.SECONDS) 139 | executor.shutdown 140 | shutdownExecutor.shutdown 141 | else () 142 | catch case _ => () 143 | } 144 | 145 | def onComplete(): Unit = 146 | printDebugMessage("onComplete:complete") 147 | shutdown(false) 148 | def onError(ex: Throwable): Unit = 149 | val sw: StringWriter = new StringWriter() 150 | val pw: PrintWriter = new PrintWriter(sw) 151 | ex.printStackTrace(pw) 152 | printDebugMessage(s"onError: ${ex}, ${ex.getMessage()}, ${sw.toString()}") 153 | err.updateAndGet(t => ex) 154 | def onNext(byteBuffers: java.util.List[java.nio.ByteBuffer]): Unit = 155 | printDebugMessage(s"onNext: ${byteBuffers.size}") 156 | for { 157 | bb: ByteBuffer <- byteBuffers.asScala.toList 158 | } while (bb.hasRemaining()) { 159 | val b = bb.get() 160 | printDebugMessage(s"onNext: enqueueing current byte: ${b.toInt.toHexString}") 161 | queue.add(b) 162 | } 163 | def onSubscribe(subscription: Flow.Subscription): Unit = 164 | printDebugMessage(s"onSubscribe:subscribed: ${subscription}") 165 | subscription.request(Long.MaxValue) // unbounded subscription 166 | isDone.set(false) 167 | err.updateAndGet(_ => null) 168 | } 169 | }, 170 | 4096 171 | ) 172 | } 173 | 174 | given fileDownloadHttpResponseMapper( 175 | using path: Path, 176 | options: Seq[OpenOption]): HttpResponseMapper[Path] = 177 | new HttpResponseMapper[Path] { 178 | def bodyHandler = BodyHandlers.ofFile(path, options: _*) 179 | } 180 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpRetries.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models HttpRetries as an Int 5 | */ 6 | opaque type HttpRetries = Int 7 | 8 | object HttpRetries: 9 | 10 | given Manifest[HttpRetries] = 11 | Manifest.classType[HttpRetries](HttpRetries(1).getClass) 12 | 13 | /** 14 | * @constructor 15 | */ 16 | inline def apply(i: Int): HttpRetries = 17 | requires(i >= 0, "HttpRetries must be greater than 0", i) 18 | 19 | /** 20 | * Safe constructor for runtime instantiation 21 | */ 22 | def of(i: Int)(using Errors[HttpExecutionException]): HttpRetries = 23 | if (i >= 0) 24 | i 25 | else 26 | HttpExecutionException(RuntimeException("HttpRetries must be greater than 0")).shift 27 | 28 | extension (h: HttpRetries) 29 | /** 30 | * @return 31 | * The int value of the HttpRetries object 32 | */ 33 | def value: Int = h 34 | 35 | /** 36 | * @param z 37 | * The HttpRetries to add to this Http retries 38 | * @return 39 | * The HttpRetries added to this Http retries 40 | */ 41 | def +(z: HttpRetries): HttpRetries = value + z.value 42 | 43 | given HttpRetries = 44 | HttpRetries(3) 45 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpRetryPolicy.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.net.http.HttpResponse 4 | 5 | /** 6 | * Models a retry policy as a function from HttpResponse to Boolean. 7 | */ 8 | type HttpRetryPolicy[A] = 9 | (HttpResponse[A], HttpRetries, HttpRetries) => Boolean 10 | 11 | /** 12 | * By default, retry if the request is not a bad request. 13 | */ 14 | given defaultRetryPolicy[A]: HttpRetryPolicy[A] = 15 | (r, retryCount, maxRetries) => { 16 | retryCount.value < maxRetries.value && (500 to 599).contains(r.statusCode) 17 | } 18 | 19 | extension [A](a: HttpResponse[A]) 20 | /** 21 | * Returns true if the policy determines the request should be retried. 22 | */ 23 | def shouldRetry(retryCount: HttpRetries)( 24 | using policy: HttpRetryPolicy[A], 25 | config: HttpClientConfig): Boolean = 26 | policy(a, retryCount, config.maximumRetries.getOrElse(HttpRetries(3))) 27 | 28 | object HttpRetryPolicy: 29 | /** 30 | * @constructor 31 | */ 32 | def apply[A](f: (HttpResponse[A], HttpRetries, HttpRetries) => Boolean): HttpRetryPolicy[A] = 33 | f 34 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/Https.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models the http scheme 5 | */ 6 | opaque type Https[A] = 7 | (Structured, Control[HttpExecutionException], Resource[HttpClientConfig]) ?=> A 8 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/HttpsClient.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | opaque type HttpsClient[A] = Https[A] ?=> A 4 | 5 | object HttpsClient { 6 | def apply[A](a: A): HttpsClient[A] = a 7 | } 8 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/MediaType.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | opaque type MediaType = String 4 | 5 | object MediaType: 6 | extension (m: MediaType) 7 | def value: String = 8 | m 9 | 10 | def apply(s: String): MediaType = 11 | s 12 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/RetryHttpException.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.net.http.HttpResponse 4 | 5 | opaque type RetryPolicyHttpException[A] = 6 | RetryPolicyHttpException.UnhandledRetryPolicyException | 7 | RetryPolicyHttpException.RetriesExhaustedException[A] 8 | 9 | object RetryPolicyHttpException: 10 | private[unwrapped] final case class UnhandledRetryPolicyException( 11 | message: String, 12 | cause: Throwable | Null) 13 | extends RuntimeException(message, cause) 14 | private[unwrapped] final case class RetriesExhaustedException[A](r: HttpResponse[A])( 15 | using S: Show[HttpResponse[A]]) 16 | extends RuntimeException: 17 | override def getMessage: String = s"${S.show(r)} retries exhausted." 18 | 19 | def unhandledException[A](ex: Throwable): RetryPolicyHttpException[A] = 20 | UnhandledRetryPolicyException(ex.getMessage, ex) 21 | def retriesExhausted[A](r: HttpResponse[A])( 22 | using Show[HttpResponse[A]]): RetryPolicyHttpException[A] = RetriesExhaustedException(r) 23 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/Serde.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.deriving.Mirror 4 | import scala.compiletime.erasedValue 5 | 6 | import java.nio.ByteBuffer 7 | 8 | /** 9 | * Represents the ability to serialize / deserialize a list of byte buffers to some type A. 10 | * Serialization must round-trip. That is: 11 | * 12 | * {{{ 13 | * Serde[A].deserialize(Serde[A].serialize(a)) == a 14 | * }}} 15 | */ 16 | trait Serde[A]: 17 | /** 18 | * Deserializing may fail, for many different reasons. The non-empty string allows you to 19 | * report them all at one time. 20 | */ 21 | def deserialize(a: List[ByteBuffer]): ::[String] | A 22 | 23 | /** 24 | * Serializes the A to a list of bytebuffers. 25 | */ 26 | def serialize(a: A): List[ByteBuffer] 27 | 28 | object Serde: 29 | def apply[A: Serde](): Serde[A] = summon[Serde[A]] 30 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/StatusCode.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models http status codes as Integers 5 | */ 6 | opaque type StatusCode = Int 7 | 8 | object StatusCode { 9 | extension (s: StatusCode) { 10 | def value: Int = s 11 | def statusText = s match { 12 | case Continue => "Continue" 13 | case `Switching Protocols` => "Switching Protocols" 14 | case Processing => "Processing" 15 | case `Early Hints` => "Early Hints" 16 | case OK => "OK" 17 | case Created => "Created" 18 | case Accepted => "Accepted" 19 | case `Non-Authoritative Information` => "Non-Authoritative Information" 20 | case `No-Content` => "No-Content" 21 | case `Reset Content` => "Reset Content" 22 | case `Partial Content` => "Partial Content" 23 | case `Multi-Status` => "Multi-Status" 24 | case `Already Reported` => "Already Reported" 25 | case `IM Used` => "IM Used" 26 | case `Multiple Choices` => "Multiple Choices" 27 | case `Moved Permanently` => "Moved Permanently" 28 | case `Found` => "Found" 29 | case `See Other` => "See Other" 30 | case `Not Modified` => "Not Modified" 31 | case `Use Proxy` => "Use Proxy" 32 | case `Temporary Redirect` => "Temporary Redirect" 33 | case `Permanent Redirect` => "Permanent Redirect" 34 | case `Bad Request` => "Bad Request" 35 | case Unauthorized => "Unauthorized" 36 | case `Payment Required` => "Payment Required" 37 | case Forbidden => "Forbidden" 38 | case `Not Found` => "Not Found" 39 | case `Method Not Allowed` => "Method Not Allowed" 40 | case `NotAcceptable` => "NotAcceptable" 41 | case `Proxy Authentication Required` => "Proxy Authentication Required" 42 | case `Request Timeout` => "Request Timeout" 43 | case `Conflict` => "Conflict" 44 | case `Gone` => "Gone" 45 | case `Length Required` => "Length Required" 46 | case `Precondition Failed` => "Precondition Failed" 47 | case `Payload Too Large` => "Payload Too Large" 48 | case `URI Too Long` => "URI Too Long" 49 | case `Unsupported Media Type` => "Unsupported Media Type" 50 | case `Range Not Satisfiable` => "Range Not Satisfiable" 51 | case `Expectation Failed` => "Expectation Failed" 52 | case `I'm a teapot` => "I'm a teapot" 53 | case `Misdirected Request` => "Misdirected Request" 54 | case `Unprocessable Entity` => "Unprocessable Entity" 55 | case `Locked` => "Locked" 56 | case `Failed Dependency` => "Failed Dependency" 57 | case `Too Early` => "Too Early" 58 | case `Upgrade Required` => "Upgrade Required" 59 | case `Precondition Required` => "Precondition Required" 60 | case `Too Many Requests` => "Too Many Requests" 61 | case `Request Header Fields Too Large` => "Request Header Fields Too Large" 62 | case `Unavailable For Legal Reasons` => "Unavailable For Legal Reasons" 63 | case `Internal Server Error` => "Internal Server Error" 64 | case `Not Implemented` => "Not Implemented" 65 | case `Bad Gateway` => "Bad Gateway" 66 | case `Service Unavailable` => "Service Unavailable" 67 | case `Gateway Timeout` => "Gateway Timeout" 68 | case `Http Version Not Supported` => "Http Version Not Supported" 69 | case `Variant Also Negotiates` => "Variant Also Negotiates" 70 | case `Insufficient Storage` => "Insufficient Storage" 71 | case `Loop Detected` => "Loop Detected" 72 | case `Not Extended` => "Not Extended" 73 | case `Network Authentication Required` => "Network Authentication Required" 74 | case unused => "unused" 75 | 76 | } 77 | } 78 | 79 | def statusCodes = 80 | (100 to 103).toSet ++ (200 to 208).toSet + 226 ++ (300 to 308).toSet ++ (400 to 418).toSet ++ (421 to 426).toSet ++ Set( 81 | 428, 82 | 429, 83 | 431, 84 | 451) ++ (500 to 511).toSet 85 | 86 | private[this] val predicateError = "status code must be a valid status code" 87 | 88 | inline def apply(i: Int): StatusCode = 89 | requires( 90 | (i >= 100 && i <= 103) || 91 | (i >= 200 && i <= 208) || 92 | i == 226 || 93 | (i >= 300 && i <= 308) || 94 | (i >= 400 && i <= 418) || 95 | (i >= 421 && i <= 426) || 96 | i == 428 || 97 | i == 429 || 98 | i == 431 || 99 | i == 451 || 100 | (i >= 500 && i <= 511), 101 | "status code must be a valid status code", 102 | i 103 | ) 104 | def of(i: Int)(using Errors[String]): StatusCode = 105 | if (statusCodes.contains(i)) 106 | i 107 | else 108 | predicateError.shift 109 | 110 | def unsafeOf(i: Int): StatusCode = 111 | assert(statusCodes.contains(i)) 112 | i 113 | } 114 | -------------------------------------------------------------------------------- /http-unwrapped/src/main/scala/unwrapped/StatusCodes.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | val Continue = StatusCode(100) 4 | val `Switching Protocols` = StatusCode(101) 5 | val Processing = StatusCode(102) 6 | val `Early Hints` = StatusCode(103) 7 | val OK = StatusCode(200) 8 | val Created = StatusCode(201) 9 | val Accepted = StatusCode(202) 10 | val `Non-Authoritative Information` = StatusCode(203) 11 | val `No-Content` = StatusCode(204) 12 | val `Reset Content` = StatusCode(205) 13 | val `Partial Content` = StatusCode(206) 14 | val `Multi-Status` = StatusCode(207) 15 | val `Already Reported` = StatusCode(208) 16 | val `IM Used` = StatusCode(226) 17 | val `Multiple Choices` = StatusCode(300) 18 | val `Moved Permanently` = StatusCode(301) 19 | val `Found` = StatusCode(302) 20 | val `See Other` = StatusCode(303) 21 | val `Not Modified` = StatusCode(304) 22 | val `Use Proxy` = StatusCode(305) 23 | val unused = StatusCode(306) 24 | val `Temporary Redirect` = StatusCode(307) 25 | val `Permanent Redirect` = StatusCode(308) 26 | val `Bad Request` = StatusCode(400) 27 | val Unauthorized = StatusCode(401) 28 | val `Payment Required` = StatusCode(402) 29 | val Forbidden = StatusCode(403) 30 | val `Not Found` = StatusCode(404) 31 | val `Method Not Allowed` = StatusCode(405) 32 | val `NotAcceptable` = StatusCode(406) 33 | val `Proxy Authentication Required` = StatusCode(407) 34 | val `Request Timeout` = StatusCode(408) 35 | val `Conflict` = StatusCode(409) 36 | val `Gone` = StatusCode(410) 37 | val `Length Required` = StatusCode(411) 38 | val `Precondition Failed` = StatusCode(412) 39 | val `Payload Too Large` = StatusCode(413) 40 | val `URI Too Long` = StatusCode(414) 41 | val `Unsupported Media Type` = StatusCode(415) 42 | val `Range Not Satisfiable` = StatusCode(416) 43 | val `Expectation Failed` = StatusCode(417) 44 | val `I'm a teapot` = StatusCode(418) 45 | val `Misdirected Request` = StatusCode(421) 46 | val `Unprocessable Entity` = StatusCode(422) 47 | val `Locked` = StatusCode(423) 48 | val `Failed Dependency` = StatusCode(424) 49 | val `Too Early` = StatusCode(425) 50 | val `Upgrade Required` = StatusCode(426) 51 | val `Precondition Required` = StatusCode(428) 52 | val `Too Many Requests` = StatusCode(429) 53 | val `Request Header Fields Too Large` = StatusCode(431) 54 | val `Unavailable For Legal Reasons` = StatusCode(451) 55 | val `Internal Server Error` = StatusCode(500) 56 | val `Not Implemented` = StatusCode(501) 57 | val `Bad Gateway` = StatusCode(502) 58 | val `Service Unavailable` = StatusCode(503) 59 | val `Gateway Timeout` = StatusCode(504) 60 | val `Http Version Not Supported` = StatusCode(505) 61 | val `Variant Also Negotiates` = StatusCode(506) 62 | val `Insufficient Storage` = StatusCode(507) 63 | val `Loop Detected` = StatusCode(508) 64 | val `Not Extended` = StatusCode(510) 65 | val `Network Authentication Required` = StatusCode(511) 66 | -------------------------------------------------------------------------------- /http-unwrapped/src/test/scala/unwrapped/FakeHttpResponse.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.net.http.HttpResponse 4 | 5 | class FakeHttpResponse[A]( 6 | val status: StatusCode 7 | ) extends HttpResponse[A]: 8 | def body(): A = ??? 9 | def headers(): java.net.http.HttpHeaders = ??? 10 | def previousResponse(): java.util.Optional[java.net.http.HttpResponse[A]] = ??? 11 | def request(): java.net.http.HttpRequest = ??? 12 | def sslSession(): java.util.Optional[javax.net.ssl.SSLSession] = ??? 13 | def uri(): java.net.URI = ??? 14 | def version(): java.net.http.HttpClient.Version = ??? 15 | 16 | override def statusCode(): Int = status.value 17 | -------------------------------------------------------------------------------- /http-unwrapped/src/test/scala/unwrapped/HttpConnectionTimeoutSuite.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import munit.ScalaCheckSuite 4 | import org.scalacheck.Prop._ 5 | 6 | import scala.annotation.nowarn 7 | 8 | class HttpConnectionTimeoutSuite extends ScalaCheckSuite: 9 | 10 | property("Any positive long can be a HttpConnectionTimeout") { 11 | forAll { (i: Long) => 12 | (i > 0) ==> { 13 | @nowarn // value will not resolve without the fruitless typecheck 14 | val result = run(HttpConnectionTimeout.of(i)) match 15 | case s: String => fail(s"unexpected shift to error: $s") 16 | case x: HttpConnectionTimeout => x.value == i 17 | result 18 | } 19 | (i <= 0) ==> { 20 | run(HttpConnectionTimeout.of(i)) match 21 | case s: String => s == "Durations must be positive" 22 | case _ => 23 | fail("0 or negative numbers should have failed") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /http-unwrapped/src/test/scala/unwrapped/HttpResponseMapperSuite.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import munit.unwrapped.UnwrappedSuite 4 | 5 | import scala.jdk.CollectionConverters.* 6 | import java.net.URI 7 | 8 | class HttpResponseMapperSuite extends UnwrappedSuite, HttpServerFixtures: 9 | val debug = false 10 | httpServer(getLoremHandler(None)).testUnwrapped( 11 | "an http request expecting a Receive byte body should return the body in a stream") { 12 | serverAddressResource => 13 | serverAddressResource.use { baseServerAddress => 14 | val bodyAsReceive: unwrapped.Receive[Byte] = 15 | structured(URI.create(s"$baseServerAddress/stream").GET[Receive[Byte]]()).body 16 | val bodyAsString = new String( 17 | bodyAsReceive 18 | .map { b => 19 | if (debug) { 20 | println(s"in test receive, showing streaming behavior: ${b.toInt.toHexString}") 21 | }; b 22 | } 23 | .toList 24 | .toArray) 25 | assertEqualsUnwrapped(bodyAsString, loremIpsumBody) 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /http-unwrapped/src/test/scala/unwrapped/HttpRetriesSuite.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import munit.ScalaCheckSuite 4 | import org.scalacheck.Prop._ 5 | 6 | import scala.annotation.nowarn 7 | 8 | class HttpRetriesSuite extends ScalaCheckSuite: 9 | 10 | property("Any positive int can be a HttpRetries") { 11 | forAll { (i: Int) => 12 | (i >= 0) ==> { 13 | @nowarn 14 | val result = run(HttpRetries.of(i)) match 15 | case ex: HttpExecutionException => fail(s"unexpected shift to error: $ex.getMessage") 16 | case x: HttpRetries => x.value == i 17 | result 18 | } 19 | (i < 0) ==> { 20 | run(HttpRetries.of(i)) match 21 | case ex: HttpExecutionException => 22 | ex.getMessage == "HttpRetries must be greater than 0" 23 | 24 | case _ => 25 | fail("negative numbers should have failed") 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /http-unwrapped/src/test/scala/unwrapped/HttpRetryPolicySuite.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import munit.ScalaCheckSuite 4 | import org.scalacheck.Arbitrary 5 | import org.scalacheck.Gen 6 | import org.scalacheck.Prop._ 7 | 8 | import java.net.http.HttpResponse 9 | 10 | class HttpRetryPolicySuite extends ScalaCheckSuite: 11 | 12 | property( 13 | "The default retry policy should retry any HttpResponse with a statusCode in the 500s") { 14 | given arbHttpResponse: Arbitrary[HttpResponse[Any]] = Arbitrary { 15 | Gen.oneOf(StatusCode.statusCodes).map(StatusCode.unsafeOf).map(FakeHttpResponse[Any](_)) 16 | } 17 | forAll { (httpResponse: HttpResponse[Any]) => 18 | val f: (HttpResponse[Any]) => Boolean = 19 | response => response.shouldRetry(HttpRetries(1)) 20 | (httpResponse.statusCode >= 500 && httpResponse.statusCode < 600) ==> { 21 | f(httpResponse) 22 | } 23 | (httpResponse.statusCode < 400) ==> { 24 | !f(httpResponse) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /http-unwrapped/src/test/scala/unwrapped/HttpSuite.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import com.sun.net.httpserver.* 4 | import munit.unwrapped.UnwrappedSuite 5 | 6 | import java.net.URI 7 | import java.util.concurrent.Executors 8 | import java.util.concurrent.atomic.AtomicInteger 9 | 10 | class HttpSuite extends UnwrappedSuite, HttpServerFixtures: 11 | 12 | httpServer(getHttpHandler(Option.empty)) 13 | .testUnwrapped("requests should be returned in non-blocking fibers") { serverResource => 14 | serverResource.use { baseServerAddress => 15 | val pongResponse: Structured ?=> ( 16 | String, 17 | String, 18 | String, 19 | String, 20 | String, 21 | String, 22 | String, 23 | String, 24 | String, 25 | String, 26 | String, 27 | String, 28 | String, 29 | String, 30 | String, 31 | String, 32 | String, 33 | String, 34 | String, 35 | String) = 36 | parallel( 37 | () => URI.create(s"$baseServerAddress/ping/1").GET[String]().body, 38 | () => URI.create(s"$baseServerAddress/ping/2").GET[String]().body, 39 | () => URI.create(s"$baseServerAddress/ping/3").GET[String]().body, 40 | () => URI.create(s"$baseServerAddress/ping/4").GET[String]().body, 41 | () => URI.create(s"$baseServerAddress/ping/5").GET[String]().body, 42 | () => URI.create(s"$baseServerAddress/ping/6").GET[String]().body, 43 | () => URI.create(s"$baseServerAddress/ping/7").GET[String]().body, 44 | () => URI.create(s"$baseServerAddress/ping/8").GET[String]().body, 45 | () => URI.create(s"$baseServerAddress/ping/9").GET[String]().body, 46 | () => URI.create(s"$baseServerAddress/ping/0").GET[String]().body, 47 | () => URI.create(s"$baseServerAddress/ping/11").GET[String]().body, 48 | () => URI.create(s"$baseServerAddress/ping/12").GET[String]().body, 49 | () => URI.create(s"$baseServerAddress/ping/13").GET[String]().body, 50 | () => URI.create(s"$baseServerAddress/ping/14").GET[String]().body, 51 | () => URI.create(s"$baseServerAddress/ping/15").GET[String]().body, 52 | () => URI.create(s"$baseServerAddress/ping/16").GET[String]().body, 53 | () => URI.create(s"$baseServerAddress/ping/17").GET[String]().body, 54 | () => URI.create(s"$baseServerAddress/ping/18").GET[String]().body, 55 | () => URI.create(s"$baseServerAddress/ping/19").GET[String]().body, 56 | () => URI.create(s"$baseServerAddress/ping/20").GET[String]().body 57 | ) 58 | 59 | assertEqualsUnwrapped( 60 | structured(pongResponse), 61 | ( 62 | "pong", 63 | "pong", 64 | "pong", 65 | "pong", 66 | "pong", 67 | "pong", 68 | "pong", 69 | "pong", 70 | "pong", 71 | "pong", 72 | "pong", 73 | "pong", 74 | "pong", 75 | "pong", 76 | "pong", 77 | "pong", 78 | "pong", 79 | "pong", 80 | "pong", 81 | "pong" 82 | ) 83 | ) 84 | } 85 | } 86 | 87 | httpServer(getHttpHandler(Option(Headers.of("X-Extended-Test-Header", "HttpSuite")))) 88 | .testUnwrapped("Expected headers are sent with requests, 404 means headers not sent") { 89 | (serverAddressResource: Resource[String]) => 90 | serverAddressResource.use { baseServerAddress => 91 | assertEqualsUnwrapped( 92 | structured( 93 | URI 94 | .create(s"$baseServerAddress/ping/1") 95 | .GET[String](HttpHeader("X-Extended-Test-Header", "HttpSuite")) 96 | .body), 97 | "pong" 98 | ) 99 | } 100 | } 101 | 102 | httpServer(getHttpFailureHandler(Option.empty, AtomicInteger(3))) 103 | .testUnwrapped("By default, failed requests should retry 3 times") { 104 | serverAddressResource => 105 | serverAddressResource.use { baseServerAddress => 106 | assertEqualsUnwrapped( 107 | structured(URI.create(s"$baseServerAddress/ping/fail").GET[String]().statusCode), 108 | 200) 109 | } 110 | } 111 | 112 | httpServer(getHttpFailureHandler(Option.empty, AtomicInteger(4))).testUnwrapped( 113 | "More than three request failures should return the server error by default") { 114 | serverAddressResource => 115 | serverAddressResource.use { baseServerAddress => 116 | assertEqualsUnwrapped( 117 | structured(URI.create(s"$baseServerAddress/ping/fail").GET[String]().statusCode), 118 | 500) 119 | } 120 | } 121 | 122 | httpServer(postHttpSuccessHandler).testUnwrapped( 123 | "requests with string post bodies are successuful") { serverAddressResource => 124 | serverAddressResource.use { baseServeraddress => 125 | assertEqualsUnwrapped( 126 | structured( 127 | URI.create(s"$baseServeraddress/ping").POST[String, String]("paddle")).statusCode, 128 | 201) 129 | } 130 | } 131 | 132 | httpServer(headHttpSuccessHandler).testUnwrapped("Head should send a HEAD request") { 133 | serverAddressResource => 134 | serverAddressResource.use { baseServerAddress => 135 | assertEqualsUnwrapped( 136 | structured(URI.create(s"$baseServerAddress/ping").HEAD()).statusCode, 137 | 200 138 | ) 139 | } 140 | } 141 | 142 | httpServer(putHttpSuccessHandler).testUnwrapped("PUT should send a PUT request") { 143 | serverAddressResource => 144 | serverAddressResource.use { baseServerAddress => 145 | assertEqualsUnwrapped( 146 | structured( 147 | URI.create(s"$baseServerAddress/ping").PUT[String, String]("paddle")).statusCode, 148 | 204 149 | ) 150 | } 151 | } 152 | 153 | httpServer(deleteHttpSuccessHandler).testUnwrapped("DELETE should send a DELETE request") { 154 | serverAddressResource => 155 | serverAddressResource.use { baseServerAddress => 156 | assertEqualsUnwrapped( 157 | structured(URI.create(s"$baseServerAddress/ping").DELETE[String]()).statusCode, 158 | 204) 159 | } 160 | } 161 | 162 | httpServer(optionsHttpSuccessHandler).testUnwrapped( 163 | "OPTIONS should send an Options request") { serverAddressResource => 164 | serverAddressResource.use { baseServerAddress => 165 | assertEqualsUnwrapped( 166 | structured(URI.create(s"$baseServerAddress/ping").OPTIONS[String]().statusCode), 167 | 200) 168 | } 169 | } 170 | 171 | httpServer(traceHttpSuccessHandler).testUnwrapped("TRACE should send a trace request") { 172 | serverAddressResource => 173 | serverAddressResource.use { baseServerAddress => 174 | assertEqualsUnwrapped( 175 | structured(URI.create(s"$baseServerAddress/ping").TRACE[String]().statusCode), 176 | 200) 177 | } 178 | } 179 | 180 | httpServer(patchHttpSuccessHandler).testUnwrapped("PATCH should send a patch request") { 181 | serverAddressResource => 182 | serverAddressResource.use { baseServerAddress => 183 | assertEqualsUnwrapped( 184 | structured( 185 | URI.create(s"$baseServerAddress/ping").PATCH[String, String]("paddle").statusCode), 186 | 200) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/Boundary.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Represents a multipart boundary 5 | */ 6 | opaque type Boundary = String 7 | 8 | object Boundary: 9 | /** 10 | * Gets the string value out of the boundary opaque type 11 | */ 12 | extension (b: Boundary) def value: String = b 13 | 14 | /** 15 | * Creates a multi-part boundary from a string. 16 | */ 17 | def apply(s: String): Boundary = 18 | s 19 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/MultipartBodyPublisher.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International 3 | * License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ 4 | * or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 5 | * 6 | * From https://stackoverflow.com/a/54675316/708929 Accessed on 2022/08/07T16:05:00.000Z-5:00 7 | * Authors: https://stackoverflow.com/users/6558116/ittupelo 8 | * https://stackoverflow.com/users/1746118/naman Converted to scala by 9 | * https://stackoverflow.com/users/708929/jack-viers 10 | */ 11 | package unwrapped 12 | 13 | import java.io.IOException 14 | import java.io.InputStream 15 | import java.io.UncheckedIOException 16 | import java.net.http.HttpRequest 17 | import java.net.http.HttpRequest.BodyPublisher 18 | import java.net.http.HttpRequest.BodyPublishers 19 | import java.nio.charset.StandardCharsets 20 | import java.nio.file.Files 21 | import java.nio.file.Path 22 | import java.util.* 23 | import java.util.function.Supplier 24 | import scala.jdk.CollectionConverters.* 25 | 26 | /** 27 | * An immutable multi-part body publisher. Given a boundary, and a non empty queue of part 28 | * specifications added with addPart, this will publish multipart body requests via http. 29 | * 30 | * {{{ 31 | * val publisher = MultiPartBodyPublisher(scala.util.Random(new SecureRandom())) 32 | * .addPart("myField", "myFieldValue") 33 | * .addPart("myFile", "/Users/alice/Documents/someFile.txt") 34 | * .addPart( 35 | * "secondFile", 36 | * () => readFile(myPath), 37 | * myPath.toFile().getName(), 38 | * myPath, 39 | * "application/octet-stream" 40 | * ) 41 | * .bodyPublisher() 42 | * }}} 43 | */ 44 | class MultiPartBodyPublisher private ( 45 | // immutable queues have constant time append, so this helps 46 | private val parts: scala.collection.immutable.Queue[PartSpecification], 47 | val boundary: Boundary) { 48 | 49 | /** 50 | * Adds a string body part to the multipart body. 51 | */ 52 | def addPart(name: String, value: String): MultiPartBodyPublisher = new MultiPartBodyPublisher( 53 | parts.appended( 54 | PartSpecification(PartSpecificationName(name), PartSpecificationValue(value), boundary)), 55 | boundary) 56 | 57 | /** 58 | * Adds a path body part to the multipart body. Throws when the file at the path is 59 | * inaccessible. 60 | */ 61 | def addPart(name: String, value: Path): MultiPartBodyPublisher = new MultiPartBodyPublisher( 62 | parts.appended(PartSpecification(PartSpecificationName(name), value, boundary)), 63 | boundary) 64 | 65 | /** 66 | * Adds a file body part to the multipart body. Throws when the file at the path is 67 | * inaccessible. 68 | */ 69 | def addPart( 70 | name: String, 71 | value: () => InputStream, 72 | filename: String, 73 | path: Path, 74 | contentType: String): MultiPartBodyPublisher = new MultiPartBodyPublisher( 75 | parts.appended( 76 | PartSpecification( 77 | PartSpecificationName(name), 78 | PartSpecificationFilename(filename), 79 | path, 80 | PartSpecificationInputStream(value), 81 | PartSpecificationContentType(contentType), 82 | boundary 83 | )), 84 | boundary) 85 | 86 | /** 87 | * Adds a generic input stream to the multi-part body 88 | */ 89 | def addPart( 90 | name: String, 91 | value: () => InputStream, 92 | contentType: String): MultiPartBodyPublisher = new MultiPartBodyPublisher( 93 | parts.appended( 94 | PartSpecification( 95 | PartSpecificationName(name), 96 | PartSpecificationInputStream(value), 97 | PartSpecificationContentType(contentType), 98 | boundary 99 | ) 100 | ), 101 | boundary 102 | ) 103 | 104 | /** 105 | * Builds a body publisher from the bytes of all the added parts. Throws when there are no 106 | * body parts to send. Throws when parts is empty or cannot be encoded. 107 | */ 108 | def unsafeTobodyPublisher(): BodyPublisher = 109 | if (parts.nonEmpty) 110 | BodyPublishers.ofByteArrays( 111 | parts 112 | .appended(PartSpecification(boundary)) 113 | .iterator 114 | .map(PartSpecification.toPartSpec(_)) 115 | .toList 116 | .asJava) 117 | else throw IllegalStateException("parts must be non-empty") 118 | } 119 | 120 | object MultiPartBodyPublisher: 121 | /** 122 | * Creates an empty publisher when given a string boundary. 123 | */ 124 | def apply(boundary: Boundary): MultiPartBodyPublisher = 125 | new MultiPartBodyPublisher(scala.collection.immutable.Queue.empty, boundary) 126 | 127 | /** 128 | * Creates an empty publisher when given a random from which to produce a boundary. 129 | */ 130 | def apply(random: scala.util.Random): MultiPartBodyPublisher = 131 | new MultiPartBodyPublisher( 132 | scala.collection.immutable.Queue.empty, 133 | Boundary(random.nextLong().toHexString)) 134 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/PartSpecification.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.nio.charset.Charset 4 | import java.nio.file.Files 5 | import java.nio.file.Path 6 | import scala.util.Try 7 | 8 | sealed trait PartSpecification 9 | 10 | object PartSpecification: 11 | private def fold[B](partSpec: PartSpecification)( 12 | whenStringPartSpec: (PartSpecification.StringPartSpec) => B, 13 | whenFilePartSpec: (PartSpecification.FilePartSpec) => B, 14 | whenPathPartSpec: (PartSpecification.PathPartSpec) => B, 15 | whenStreamPartSpec: (PartSpecification.StreamPartSpec) => B, 16 | whenFinalBoundaryPartSpec: (PartSpecification.FinalBoundaryPartSpec) => B): B = 17 | partSpec match 18 | case x: PartSpecification.StringPartSpec => whenStringPartSpec(x) 19 | case x: PartSpecification.PathPartSpec => whenPathPartSpec(x) 20 | case x: PartSpecification.FilePartSpec => whenFilePartSpec(x) 21 | case x: PartSpecification.StreamPartSpec => whenStreamPartSpec(x) 22 | case x: PartSpecification.FinalBoundaryPartSpec => whenFinalBoundaryPartSpec(x) 23 | 24 | private case class StringPartSpec( 25 | name: PartSpecificationName, 26 | value: PartSpecificationValue, 27 | boundary: Boundary) 28 | extends PartSpecification 29 | 30 | private case class PathPartSpec(name: PartSpecificationName, value: Path, boundary: Boundary) 31 | extends PartSpecification 32 | 33 | private case class FilePartSpec( 34 | name: PartSpecificationName, 35 | filename: PartSpecificationFilename, 36 | path: Path, 37 | value: PartSpecificationInputStream, 38 | contentType: PartSpecificationContentType, 39 | boundary: Boundary) 40 | extends PartSpecification 41 | 42 | private case class StreamPartSpec( 43 | name: PartSpecificationName, 44 | value: PartSpecificationInputStream, 45 | contentType: PartSpecificationContentType, 46 | boundary: Boundary 47 | ) extends PartSpecification 48 | 49 | private case class FinalBoundaryPartSpec(val boundary: Boundary) extends PartSpecification 50 | 51 | def apply( 52 | name: PartSpecificationName, 53 | value: PartSpecificationValue, 54 | boundary: Boundary): PartSpecification = 55 | StringPartSpec(name, value, boundary) 56 | 57 | def apply(name: PartSpecificationName, value: Path, boundary: Boundary): PartSpecification = 58 | PathPartSpec(name, value, boundary) 59 | 60 | def apply( 61 | name: PartSpecificationName, 62 | filename: PartSpecificationFilename, 63 | path: Path, 64 | value: PartSpecificationInputStream, 65 | contentType: PartSpecificationContentType, 66 | boundary: Boundary 67 | ): PartSpecification = 68 | FilePartSpec(name, filename, path, value, contentType, boundary) 69 | 70 | def apply( 71 | name: PartSpecificationName, 72 | value: PartSpecificationInputStream, 73 | contentType: PartSpecificationContentType, 74 | boundary: Boundary): PartSpecification = 75 | StreamPartSpec(name, value, contentType, boundary) 76 | 77 | def apply(boundary: Boundary): PartSpecification = 78 | FinalBoundaryPartSpec(boundary) 79 | 80 | private given ToPartSpec[StringPartSpec] with 81 | extension (p: StringPartSpec) 82 | def toPartSpec: Array[Byte] = 83 | s"""|--${p.boundary.value} 84 | |Content-Disposition: form-data; name=${p.name.value} 85 | |Content-Type: text/plain; charset=UTF-8 86 | | 87 | |${p.value.value} 88 | |""".stripMargin.replace("/n", "/r/n").getBytes(Charset.forName("UTF-8")) 89 | 90 | private given ToPartSpec[FilePartSpec] with 91 | extension (p: FilePartSpec) 92 | def toPartSpec: Array[Byte] = 93 | val path = p.path 94 | s"""|--${p.boundary} 95 | |Content-Disposition: form-data; name=${p.name.value}; filename=${p.filename.value} 96 | |Content-Type: ${Try { Option(Files.probeContentType(path)).get } 97 | .fold(_ => "application/octet-stream", identity)} 98 | |""" 99 | .stripMargin 100 | .getBytes() ++ Files.newInputStream(path).readAllBytes() ++ "\r\n".getBytes 101 | 102 | private given ToPartSpec[PathPartSpec] with 103 | extension (p: PathPartSpec) 104 | def toPartSpec: Array[Byte] = 105 | val path: Path = p.value 106 | s"""|--${p.boundary.value} 107 | |Content-Disposition: form-data; name=${p 108 | .name 109 | .value}; filename=${path.toFile().getName()} 110 | |Content-Type: ${Try { Option(Files.probeContentType(path)).get } 111 | .fold(_ => "application/octet-stream", identity)}""" 112 | .stripMargin 113 | .getBytes() ++ Files.newInputStream(path).readAllBytes() ++ "\r\n".getBytes 114 | 115 | private given ToPartSpec[StreamPartSpec] with 116 | extension (p: StreamPartSpec) 117 | def toPartSpec: Array[Byte] = 118 | s"""|--${p.boundary.value} 119 | |Content-Disposition: form-data; name="${p.name.value}; filename=${p.name.value} 120 | |Content-Type: ${p.contentType.value} 121 | | 122 | |""".stripMargin.replace("\n", "\r\n").getBytes(Charset.forName("UTF-8")) ++ p 123 | .value 124 | .value() 125 | .readAllBytes() ++ "\r\n".getBytes 126 | 127 | private given ToPartSpec[FinalBoundaryPartSpec] with 128 | extension (p: FinalBoundaryPartSpec) 129 | def toPartSpec: Array[Byte] = 130 | s"""|--${p.boundary.value}--""".stripMargin.getBytes(Charset.forName("UTF-8")) 131 | 132 | def toPartSpec(p: PartSpecification): Array[Byte] = 133 | fold(p)( 134 | _.toPartSpec, 135 | _.toPartSpec, 136 | _.toPartSpec, 137 | _.toPartSpec, 138 | _.toPartSpec 139 | ) 140 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/PartSpecificationContentType.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models the content type for a multipart part. 5 | */ 6 | opaque type PartSpecificationContentType = String 7 | 8 | object PartSpecificationContentType: 9 | def apply(s: String): PartSpecificationContentType = s 10 | 11 | /** 12 | * Gets the string value out of the opaque type. 13 | */ 14 | extension (p: PartSpecificationContentType) def value: String = p 15 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/PartSpecificationFileName.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models the filename part of a multipart part. 5 | */ 6 | opaque type PartSpecificationFilename = String 7 | 8 | object PartSpecificationFilename: 9 | def apply(s: String): PartSpecificationFilename = s 10 | 11 | /** 12 | * Gets the string value of this filename 13 | */ 14 | extension (p: PartSpecificationFilename) def value: String = p 15 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/PartSpecificationInputStream.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.io.InputStream 4 | 5 | /** 6 | * Models the input stream supplier for a multi-part part. 7 | */ 8 | opaque type PartSpecificationInputStream = () => InputStream 9 | 10 | object PartSpecificationInputStream: 11 | def apply(i: () => InputStream): PartSpecificationInputStream = i 12 | def apply(i: InputStream): PartSpecificationInputStream = () => i 13 | 14 | /** 15 | * Gets the input stream supplier value for this input stream specification. 16 | */ 17 | extension (p: PartSpecificationInputStream) def value: () => InputStream = p 18 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/PartSpecificationName.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models the name of this multipart part. 5 | */ 6 | opaque type PartSpecificationName = String 7 | 8 | object PartSpecificationName: 9 | def apply(s: String): PartSpecificationName = s 10 | 11 | /** 12 | * Gets the string value of this name. 13 | */ 14 | extension (p: PartSpecificationName) def value: String = p 15 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/PartSpecificationValue.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Models the value of a multipart part. 5 | */ 6 | opaque type PartSpecificationValue = String 7 | 8 | object PartSpecificationValue: 9 | def apply(s: String): PartSpecificationValue = s 10 | 11 | /** 12 | * Gets the string value of this value. 13 | */ 14 | extension (p: PartSpecificationValue) def value: String = p 15 | -------------------------------------------------------------------------------- /java-net-mulitpart-body-publisher/src/main/scala/fx/ToPartSpec.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Provides a conversion from a PartSpecification to an array of bytes for http request 5 | * publishing. 6 | */ 7 | trait ToPartSpec[A <: PartSpecification]: 8 | extension (p: A) def toPartSpec: Array[Byte] 9 | -------------------------------------------------------------------------------- /munit-unwrapped/src/it/scala/example/ScalaFXSuiteIntegrationTest.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import unwrapped.* 4 | import munit.unwrapped.ScalaFXSuite 5 | 6 | class ScalaFXSuiteIntegrationSuite extends UnwrappedSuite: 7 | def myFunction( 8 | value1: Either[String, Int], 9 | value2: Option[Int]): Errors[String | None.type] ?=> Int = 10 | value1.bind + value2.bind 11 | 12 | testFX("The computation's result satisfies the assertion") { 13 | assertFX(myFunction(Right(1), Option(2)) == 3) 14 | } 15 | 16 | testFX("The computation's result doesn't satisfy the assertion".fail) { 17 | assertFX(myFunction(Right(1), Option(2)) == 0) 18 | } 19 | 20 | testFX("The computation is short circuited with a None".fail) { 21 | assertFX(myFunction(Right(1), None) == 1) 22 | } 23 | 24 | testFX("The computation is short circuited with a Left".fail) { 25 | assertFX(myFunction(Left("BOOM!"), Option(2)) == 2) 26 | } 27 | 28 | testFX("Example 1".fail) { 29 | assertFX(false) 30 | } 31 | testFX("Example 2") { 32 | val x: Console ?=> Unit = "example 1".writeLine() 33 | val y: Console ?=> Unit = "example 2".writeLine() 34 | structured( 35 | parallel( 36 | () => { 37 | x 38 | assertFX(true) 39 | }, 40 | () => { 41 | y 42 | assertFX(true) 43 | } 44 | ) 45 | ) 46 | } 47 | 48 | fixture.testFX("Fixtures should work as well") { int => 49 | assertEqualsDouble(int * 3.00, int * 3.00, 0.00) 50 | } 51 | 52 | fixture.testFX("Fixtures should work with options, too".fail) { int => 53 | assertEqualsDouble(int * 3.00, int, 0.00) 54 | } 55 | 56 | fixturePair.testFX("Pair fixtures also work") { pair => assertEquals(pair._2, pair._2) } 57 | 58 | fixturePair.testFX("Pair fixtures also work with options".fail) { pair => 59 | assertEquals(pair._2, "doesn't match the pair") 60 | } 61 | 62 | fixtureTriple.testFX("Triple fixtures also work") { pair => assertEquals(pair._3, pair._3) } 63 | 64 | fixtureTriple.testFX("Triple fixtures also work with options".fail) { pair => 65 | assertEquals(pair._3, 'c') 66 | } 67 | 68 | lazy val fixture = FunFixture(setup = testOptions => 1, teardown = intNum => ()) 69 | 70 | lazy val fixtureString = FunFixture(setup = options => options.name, str => ()) 71 | 72 | lazy val fixtureChar = FunFixture(setup = options => options.name.head, char => ()) 73 | 74 | lazy val fixturePair = FunFixture.map2(fixture, fixtureString) 75 | 76 | lazy val fixtureTriple = FunFixture.map3(fixture, fixtureString, fixtureChar) 77 | -------------------------------------------------------------------------------- /munit-unwrapped/src/main/scala/munit/unwrapped/Asserts.scala: -------------------------------------------------------------------------------- 1 | package munit 2 | package unwrapped 3 | 4 | import scala.annotation.implicitNotFound 5 | 6 | /** 7 | * The Asserts capability models assertion error-handling capabilities. 8 | * 9 | * AssertionError falls outside the hierarchy covered by the Throws capability, 10 | * @tparam R 11 | * Contravariant assertion error subtype 12 | */ 13 | @implicitNotFound( 14 | "Missing capability:\n" + 15 | "* Asserts[${R}]\n" + 16 | "alternatively you may resolve this call with \n" + 17 | "```scala\nhandleAssert(f)(recover)\n```\n" + 18 | "or\n ignore thrown exceptions with import munit.unwrapped.Asserts.unsafeAsserts") 19 | opaque type Asserts[-R <: AssertionError] = Unit 20 | 21 | object Asserts { 22 | 23 | /** 24 | * Importable capability evidence that allows unsafely throwing assertion errors when 25 | * imported. 26 | * @tparam R 27 | * The assertion error subtype that is allowed to be thrown. 28 | */ 29 | given unsafeAsserts[R <: AssertionError]: Asserts[R] = () 30 | } 31 | 32 | /** 33 | * Inline-optimization for handling AssertionErrors safely. 34 | * 35 | * @tparam R 36 | * The assertion error subtype to handle 37 | * @tparam A 38 | * The expected type of successful evaluation 39 | * @recover 40 | * A total function that can recover from a state generating an R. 41 | * @return 42 | * The result of running f or recover 43 | */ 44 | inline def handleAssert[R <: AssertionError, A]( 45 | inline f: Asserts[R] ?=> A 46 | )(inline recover: R => A): A = 47 | try 48 | import munit.unwrapped.Asserts.unsafeAsserts 49 | f 50 | catch 51 | case r: R => 52 | recover(r) 53 | -------------------------------------------------------------------------------- /munit-unwrapped/src/main/scala/munit/unwrapped/UnwrappedAssertions.scala: -------------------------------------------------------------------------------- 1 | package munit 2 | package unwrapped 3 | 4 | import _root_.unwrapped.* 5 | import org.junit.AssumptionViolatedException 6 | 7 | /** 8 | * Wraps munit assertions in an Errors effect for integration within scala-unwrapped test 9 | * bodies. 10 | */ 11 | trait UnwrappedAssertions: 12 | self: Assertions => 13 | 14 | /** 15 | * Lifts munit.Assertions#assert into Errors for testing. Will not return until given an 16 | * Error[AssertionError], typically within run or structured. 17 | * 18 | * @param cond 19 | * The assertion value 20 | * @param clue 21 | * @see 22 | * [munit.Clue] 23 | */ 24 | def assertUnwrapped[R](cond: => Errors[R] ?=> Boolean, clue: => Any = "assertion failed") 25 | : Location ?=> (Errors[AssertionError], Errors[R]) ?=> Unit = 26 | liftToUnwrapped(assert(cond, clue)) 27 | 28 | /** 29 | * Lifts munit.Assertions#assertEquals into Errors for testing. Will not return until given an 30 | * Error[AssertionError], typically within run or structured. 31 | * 32 | * @tparam A 33 | * the type of the obtained value 34 | * @tparam B 35 | * the type of the expected value 36 | * @param obtained 37 | * The actual value under test 38 | * @param expected 39 | * @param clue 40 | * @see 41 | * [munit.Clue] 42 | */ 43 | def assertEqualsUnwrapped[R, A, B]( 44 | obtained: Errors[R] ?=> A, 45 | expected: B, 46 | clue: => Any = "values are not the same" 47 | ): Location ?=> B <:< A ?=> (Errors[AssertionError], Errors[R]) ?=> Unit = 48 | liftToUnwrapped(assertEquals(obtained, expected, clue)) 49 | 50 | /** 51 | * Lifts munit.Assertions#assume into Errors for testing. Will not return until given an 52 | * Error[AssumptionViolatedException], typically within run or structured. 53 | * 54 | * @param cond 55 | * The assertion value 56 | * @param clue 57 | * @see 58 | * [munit.Clue] 59 | */ 60 | def assumeUnwrapped[R](cond: => Errors[R] ?=> Boolean, clue: => Any = "assumption failed") 61 | : Location ?=> (Errors[AssumptionViolatedException], Errors[R]) ?=> Unit = 62 | try assume(cond, clue) 63 | catch case ex: AssumptionViolatedException => ex.raise 64 | 65 | /** 66 | * Lifts munit.Assertions#assertNoDiff into Errors for testing. Will not return until given an 67 | * Error[AssertionError], typically within run or structured. 68 | * 69 | * @param obtained 70 | * The actual value under test 71 | * @param expected 72 | * @param clue 73 | * @see 74 | * [munit.Clue] 75 | */ 76 | def assertNoDiffUnwrapped[R]( 77 | obtained: Errors[R] ?=> String, 78 | expected: String, 79 | clue: => Any = "diff assertion failed") 80 | : Location ?=> (Errors[AssertionError], Errors[R]) ?=> Unit = 81 | liftToUnwrapped(assertNoDiff(obtained, expected, clue)) 82 | 83 | /** 84 | * Lifts munit.Assertions#assertNotEquals into Errors for testing. Will not return until given 85 | * an Error[AssertionError], typically within run or structured. 86 | * 87 | * @tparam A 88 | * the type of the obtained value 89 | * @tparam B 90 | * the type of the expected value 91 | * @param obtained 92 | * The actual value under test 93 | * @param expected 94 | * @param clue 95 | * @see 96 | * [munit.Clue] 97 | */ 98 | def assertNotEqualsUnwrapped[R, A, B]( 99 | obtained: Errors[R] ?=> A, 100 | expected: B, 101 | clue: => Any = "values are the same" 102 | ): Location ?=> A =:= B ?=> (Errors[AssertionError], Errors[R]) ?=> Unit = 103 | liftToUnwrapped(assertNotEquals(obtained, expected, clue)) 104 | 105 | /** 106 | * Lifts munit.Assertions#assertEqualsDoubleUnwrapped into Errors for testing. Will not return 107 | * until given an Error[AssertionError], typically within run or structured. 108 | * 109 | * @param obtained 110 | * The actual value under test 111 | * @param expected 112 | * @param delta 113 | * Acceptable error tolerance between the expected and obtained values 114 | * @param clue 115 | * @see 116 | * [munit.Clue] 117 | */ 118 | def assertEqualsDoubleUnwrapped[R]( 119 | obtained: Errors[R] ?=> Double, 120 | expected: Double, 121 | delta: Double, 122 | clue: => Any = "values are not the same" 123 | ): Location ?=> (Errors[AssertionError], Errors[R]) ?=> Unit = liftToUnwrapped( 124 | assertEqualsDouble(obtained, expected, delta, clue)) 125 | 126 | /** 127 | * Lifts munit.Assertions#assertEquals into Errors for testing. Will not return until given an 128 | * Error[AssertionError], typically within run or structured. 129 | * 130 | * @param obtained 131 | * The actual value under test 132 | * @param expected 133 | * @param delta 134 | * Acceptable error tolerance between the expected and obtained values 135 | * @param clue 136 | * @see 137 | * munit.Clue 138 | */ 139 | def assertEqualsFloatUnwrapped[R]( 140 | obtained: Errors[R] ?=> Float, 141 | expected: Float, 142 | delta: Float, 143 | clue: => Any = "values are not the same" 144 | ): Location ?=> (Errors[AssertionError], Errors[R]) ?=> Unit = liftToUnwrapped( 145 | assertEqualsFloat(obtained, expected, delta, clue)) 146 | 147 | def assertsShiftsToUnwrapped[R, T, A]( 148 | obtained: Errors[R] ?=> A, 149 | expected: T): (Location, Errors[R | AssertionError]) ?=> Unit = 150 | obtained 151 | .toEither 152 | .fold( 153 | r => assertEqualsUnwrapped(r, expected), 154 | a => FailException(s"expected $expected got $a", summon[Location])) 155 | 156 | private def liftToUnwrapped[A]( 157 | body: munit.unwrapped.Asserts[AssertionError] ?=> A): Errors[AssertionError] ?=> Unit = 158 | munit.unwrapped.handleAssert(body)(_.raise) 159 | -------------------------------------------------------------------------------- /munit-unwrapped/src/main/scala/munit/unwrapped/UnwrappedSuite.scala: -------------------------------------------------------------------------------- 1 | package munit 2 | package unwrapped 3 | 4 | import _root_.unwrapped._ 5 | import org.junit.AssumptionViolatedException 6 | import hedgehog.core.Seed 7 | import hedgehog.{runner => hr} 8 | 9 | import scala.annotation.targetName 10 | import scala.reflect.Typeable 11 | import hedgehog.core.PropertyConfig 12 | import hedgehog.Property 13 | import hedgehog.munit.HedgehogAssertions 14 | import hedgehog.core.Status 15 | 16 | /** 17 | * Provides functionality for testing within a scala-unwrapped context. 18 | */ 19 | abstract class UnwrappedSuite extends FunSuite, UnwrappedAssertions, HedgehogAssertions: 20 | 21 | private val seedSource = hr.SeedSource.fromEnvOrTime() 22 | private val seed = Seed.fromLong(seedSource.seed) 23 | 24 | private def check(test: hr.Test, config: PropertyConfig)(implicit loc: Location): Any = { 25 | val report = Property.check(test.withConfig(config), test.result, seed) 26 | if (report.status != Status.ok) { 27 | val reason = hr 28 | .Test 29 | .renderReport( 30 | this.getClass.getName, 31 | test, 32 | report, 33 | ansiCodesSupported = true 34 | ) 35 | withMunitAssertions(assertions => assertions.fail(s"$reason\n${seedSource.renderLog}")) 36 | } 37 | } 38 | 39 | /** 40 | * Provides fixtures to effectful tests. 41 | * @tparam A 42 | * @param a 43 | * The fixture to provide to the tests 44 | */ 45 | extension [A](a: FunFixture[A]) { 46 | 47 | /** 48 | * Provides testFX for [munit.FunFixtures.FunFixture] 49 | * 50 | * @tparam R 51 | * The error type declared by the test body effect. 52 | * @tparam B 53 | * @param name 54 | * The unique name of the test within the suite. 55 | * @param body 56 | * A test body that requires an error effect from given scope and the fixture. 57 | * @return 58 | * The result of the test when a location can be pulled from given scope. 59 | */ 60 | def testUnwrapped[R, F <: AssertionError: Typeable](name: String)( 61 | body: Errors[R | F] ?=> A => Unit): Location ?=> Unit = 62 | a.test(TestOptions(name)) { fixture => 63 | val x: R | F | Unit = run(structured(body(fixture))) 64 | x match 65 | case _: Unit => () 66 | case ex: F => throw ex 67 | case x => throw AssertionError(x) 68 | } 69 | 70 | /** 71 | * Provides testFX for [munit.FunFixtures.FunFixture] 72 | * 73 | * @tparam R 74 | * The error type declared by the test body effect. 75 | * @tparam B 76 | * @param options 77 | * The TestOptions containing then names, tags, and other options munit provides. 78 | * @param body 79 | * A test body that requires an error effect from given scope and the fixture. 80 | * @return 81 | * The result of the test when a location can be pulled from given scope. 82 | */ 83 | def testUnwrapped[R, F <: AssertionError: Typeable](options: TestOptions)( 84 | body: Errors[R | F] ?=> A => Unit): Location ?=> Unit = 85 | a.test(options) { fixture => 86 | val x: R | F | Unit = run(structured(body(fixture))) 87 | x match 88 | case _: Unit => () 89 | case ex: F => throw ex 90 | case x => throw AssertionError(x) 91 | } 92 | 93 | /** 94 | * Provides testFX for [munit.FunFixtures.FunFixture] 95 | * 96 | * @tparam R 97 | * The error type declared by the test body effect. 98 | * @tparam F 99 | * AssertionError thrown by any assertions 100 | * @param name 101 | * The name of the test 102 | * @param withConfig 103 | * A hedgehog PropertyConfig transformer 104 | * @return 105 | * The result of the test when a location can be pulled from given scope. 106 | */ 107 | def propertyWithFixtureFX[R, F <: AssertionError: Typeable]( 108 | name: String, 109 | withConfig: PropertyConfig => PropertyConfig = identity)( 110 | prop: Errors[R | F] ?=> A => Property): Location ?=> Unit = 111 | a.testUnwrapped(name) { fixture => 112 | val t = hedgehog.runner.property(name, prop(fixture)).config(withConfig) 113 | check(t, t.withConfig(PropertyConfig.default)) 114 | } 115 | } 116 | 117 | /** 118 | * Provides testFX for [munit.FunFixtures.FunFixture] 119 | * 120 | * @tparam R 121 | * The error type declared by the test body effect. 122 | * @tparam F 123 | * AssertionError thrown by any assertions 124 | * @param name 125 | * The name of the test 126 | * @param withConfig 127 | * A hedgehog PropertyConfig transformer 128 | * @return 129 | * The result of the test when a location can be pulled from given scope. 130 | */ 131 | def propertyFX[R, F <: AssertionError: Typeable]( 132 | name: String, 133 | withConfig: PropertyConfig => PropertyConfig = identity)( 134 | prop: Errors[R | F] ?=> Property): Location ?=> Unit = 135 | testUnwrapped(name) { 136 | val t = hedgehog.runner.property(name, prop).config(withConfig) 137 | check(t, t.withConfig(PropertyConfig.default)) 138 | } 139 | 140 | /** 141 | * Runs a test inside an effect. The execution of the effect is delayed until the test is run. 142 | * 143 | * @tparam R 144 | * The error type returned by running the test body effect. Required to be a throwable due 145 | * to munit test evaluations. 146 | * @tparam A 147 | * The return type of the test body 148 | * @param name 149 | * A unique string identifying the test within the suite. 150 | * @param body 151 | * A test program suspended in an effect that at minumum can shift control to the R error 152 | * type. 153 | * @return 154 | * Unit 155 | */ 156 | def testUnwrapped[R, F <: AssertionError: Typeable](name: String)( 157 | body: Errors[R | F] ?=> Unit): Location ?=> Unit = 158 | test(name) { 159 | val x: R | F | Unit = run(structured(body)) 160 | x match { 161 | case _: Unit => () 162 | case ex: F => throw ex 163 | case x => throw AssertionError(x) 164 | } 165 | } 166 | 167 | /** 168 | * Runs a test inside an effect. The execution of the effect is delayed until the test is run. 169 | * 170 | * @tparam R 171 | * The error type returned by running the test body effect. Required to be a throwable due 172 | * to munit test evaluations. 173 | * @tparam A 174 | * The return type of the test body 175 | * @param options 176 | * The options, including the name, tag, and other munit test configuration options 177 | * @param body 178 | * A test program suspended in an effect that at minumum can shift control to the R error 179 | * type. 180 | * @return 181 | * Unit 182 | */ 183 | def testUnwrapped[R, F <: AssertionError: Typeable](options: TestOptions)( 184 | body: => Errors[R | F] ?=> Unit): Location ?=> Unit = 185 | test(options) { 186 | val x: R | F | Unit = run(structured(body)) 187 | x match { 188 | case _: Unit => () 189 | case ex: F => throw ex 190 | case x => throw AssertionError(x) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | ./project/Dependencies.scala -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.6.2 -------------------------------------------------------------------------------- /project/build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.1.0-SNAPSHOT" 2 | ThisBuild / organization := "com.47Deg" 3 | ThisBuild / homepage := Some(url("https://47Deg.com")) 4 | 5 | lazy val root = (project in file(".")) 6 | .enablePlugins(SbtPlugin) 7 | .settings( 8 | name := "sbt-hello", 9 | libraryDependencies += "com.fasterxml.jackson.dataformat" % "jackson-dataformat-csv" % "2.13.3", 10 | pluginCrossBuild / sbtVersion := { 11 | scalaBinaryVersion.value match { 12 | case "2.12" => "1.2.8" // set minimum sbt version 13 | } 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies.Plugins._ 2 | addSbtPlugin(sbtCiRelease) 3 | addSbtPlugin(sbtScalafmt) 4 | addSbtPlugin(sbtJmh) 5 | addSbtPlugin(sbtMdoc) 6 | addSbtPlugin(sbtGithub) 7 | addSbtPlugin(sbtGithubMdoc) 8 | addSbtPlugin(sbtDependencyUpdates) 9 | addSbtPlugin(sbtExplicitDependencies) 10 | -------------------------------------------------------------------------------- /project/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | // define versions, The variable name must be camel case by module name 6 | object Versions { 7 | val junit = "4.13.2" 8 | val sbtJmh = "0.4.3" 9 | val munit = "0.7.29" 10 | val jmhCore = "1.35" 11 | val sbtMdoc = "2.3.2" 12 | val sbtGithub = "0.11.2" 13 | val sbtScalafmt = "2.4.6" 14 | val scalacheck = "1.16.0" 15 | val sbtCiRelease = "1.5.10" 16 | val sbtGithubMdoc = "0.11.2" 17 | val junitInterface = "0.7.29" 18 | val munitScalacheck = "0.7.29" 19 | val jmhGeneratorBytecode = "1.35" 20 | val sbtDependencyUpdates = "1.2.1" 21 | val jmhGeneratorReflection = "1.35" 22 | val sbtExplicitDependencies = "0.2.16" 23 | val catsEffect = "3.3.12" 24 | val scalikeJdbc = "4.0.0" 25 | val h2Database = "2.1.212" 26 | val logback = "1.2.11" 27 | val postgres = "42.4.0" 28 | val testContainers = "0.40.8" 29 | val flyway = "8.5.12" 30 | val sttp = "3.6.2" 31 | val httpCore5 = "5.1.4" 32 | val hedgehog = "0.9.0" 33 | val circe = "0.14.3" 34 | } 35 | 36 | object Compile { 37 | val catsEffect = "org.typelevel" %% "cats-effect" % Versions.catsEffect 38 | // in compile because munit-scala-fx depends on munit directly 39 | val munitScalacheck = "org.scalameta" %% "munit-scalacheck" % Versions.munitScalacheck 40 | // munit transitive dependency conflict 41 | val junit = "junit" % "junit" % Versions.junit 42 | val munit = "org.scalameta" %% "munit" % Versions.munit 43 | val junitInterface = "org.scalameta" % "junit-interface" % Versions.junitInterface 44 | val scalikejdbc = "org.scalikejdbc" %% "scalikejdbc" % Versions.scalikeJdbc 45 | val h2Database = "com.h2database" % "h2" % Versions.h2Database 46 | val logback = "ch.qos.logback" % "logback-classic" % Versions.logback 47 | val sttp = "com.softwaremill.sttp.client3" %% "core" % Versions.sttp 48 | val httpCore5 = "org.apache.httpcomponents.core5" % "httpcore5" % Versions.httpCore5 49 | val circe = "io.circe" %% "circe-core" % Versions.circe 50 | val circeGeneric = "io.circe" %% "circe-generic" % Versions.circe 51 | val circeParser = "io.circe" %% "circe-parser" % Versions.circe 52 | } 53 | 54 | object Test { 55 | val scalacheck = "org.scalacheck" %% "scalacheck" % Versions.scalacheck 56 | val postgres = "org.postgresql" % "postgresql" % Versions.postgres 57 | val testContainers = "com.dimafeng" %% "testcontainers-scala" % Versions.testContainers 58 | val testContainersMunit = 59 | "com.dimafeng" %% "testcontainers-scala-munit" % Versions.testContainers 60 | val testContainersPostgres = 61 | "com.dimafeng" %% "testcontainers-scala-postgresql" % Versions.testContainers 62 | val flyway = "org.flywaydb" % "flyway-core" % Versions.flyway 63 | val hedgehog = "qa.hedgehog" %% "hedgehog-munit" % Versions.hedgehog 64 | } 65 | 66 | object Plugins { 67 | val sbtCiRelease = "com.github.sbt" % "sbt-ci-release" % Versions.sbtCiRelease 68 | val sbtScalafmt = "org.scalameta" % "sbt-scalafmt" % Versions.sbtScalafmt 69 | val sbtJmh = "pl.project13.scala" % "sbt-jmh" % Versions.sbtJmh 70 | val sbtMdoc = "org.scalameta" % "sbt-mdoc" % Versions.sbtMdoc 71 | val sbtGithub = "com.alejandrohdezma" %% "sbt-github" % Versions.sbtGithub 72 | val sbtGithubMdoc = "com.alejandrohdezma" % "sbt-github-mdoc" % Versions.sbtGithubMdoc 73 | val sbtDependencyUpdates = 74 | "org.jmotor.sbt" % "sbt-dependency-updates" % Versions.sbtDependencyUpdates 75 | val sbtExplicitDependencies = 76 | "com.github.cb372" % "sbt-explicit-dependencies" % Versions.sbtExplicitDependencies 77 | } 78 | 79 | import Compile._ 80 | import Test._ 81 | import Plugins._ 82 | 83 | lazy val dependencies = Seq( 84 | munitScalacheck, 85 | circe, 86 | circeGeneric, 87 | circeParser, 88 | scalacheck, 89 | sbtExplicitDependencies, 90 | sbtDependencyUpdates, 91 | sbtGithub, 92 | sbtGithubMdoc, 93 | sbtMdoc, 94 | sbtJmh, 95 | sbtScalafmt, 96 | sbtCiRelease, 97 | munitScalacheck, 98 | munit, 99 | junit, 100 | junitInterface 101 | ) 102 | 103 | } 104 | -------------------------------------------------------------------------------- /project/src/main/scala/fx/HttpScalaFxPlugin.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import sbt._ 4 | import Keys._ 5 | import com.fasterxml.jackson.dataformat.csv.{CsvMapper, CsvParser, CsvSchema} 6 | import scala.collection.JavaConverters._ 7 | import scala.collection.mutable.ListBuffer 8 | 9 | object HttpUnwrappedPlugin extends AutoPlugin { 10 | 11 | object autoImport { 12 | lazy val generateMediaTypes = taskKey[Seq[File]]("Generate unwrapped/MediaTypes.scala") 13 | lazy val mediaTypeCsvDir = settingKey[File]( 14 | "The source directory containing the IANA media types. Default is baseDirectory.value/src/main/mediaTypes.") 15 | lazy val targetPackage = settingKey[String]( 16 | "the target package for the MediaTypes object generated. Defaults to 'unwrapped'") 17 | lazy val generateMediaTypeSettings = Seq( 18 | Compile / targetPackage := "unwrapped", 19 | Compile / generateMediaTypes := { 20 | println("generating MediaTypes.scala") 21 | val schema: CsvSchema = 22 | CsvSchema.builder().setUseHeader(true).build() 23 | val mapper: CsvMapper = 24 | new CsvMapper() 25 | val outputDirectory = (Compile / sourceManaged).value / (Compile / targetPackage).value 26 | val outputFile = outputDirectory / "MediaTypes.scala" 27 | val newline = IO.Newline 28 | val indent = " " 29 | val sb = new StringBuilder("") 30 | try { IO.delete(outputFile) } 31 | catch { case _: Throwable => () } 32 | try { IO.createDirectory(outputDirectory) } 33 | catch { case _: Throwable => () } 34 | 35 | val lb = ListBuffer("string") 36 | 37 | sb.append(s"package unwrapped$newline$newline") 38 | sb.append(s"object MediaTypes:$newline") 39 | 40 | val entryFiles = IO.listFiles(mediaTypeCsvDir.value) 41 | 42 | IO.listFiles(mediaTypeCsvDir.value) 43 | .map { file: File => 44 | print(".") 45 | val fileAsString = IO.read(file) 46 | val values = mapper 47 | .readerForMapOf(classOf[String]) 48 | .`with`(schema) 49 | .readValues[java.util.Map[String, String]](fileAsString) 50 | .readAll() 51 | .asScala 52 | .toList 53 | .map(_.asScala.toMap) 54 | val objectType = s"${file.getName().replaceAllLiterally(".csv", "")}" 55 | sb.append(s"""${indent}object $objectType:$newline""") 56 | values.map { value => 57 | for { 58 | name <- value.get("Name") 59 | template <- value.get("Template") 60 | mediaType = 61 | if (template == "") s"""MediaType("$name")""" 62 | else s"""MediaType("$template")""" 63 | } { 64 | lb.append(s"$objectType.`$name`") 65 | sb.append( 66 | s"""$indent${indent}val `$name` = $newline$indent$indent$indent$mediaType$newline""" 67 | ) 68 | } 69 | } 70 | sb.append(s"$newline") 71 | } 72 | .toList 73 | 74 | sb.append( 75 | s"$newline${indent}val mediaTypes: Set[MediaType] = $newline$indent${indent}Set(") 76 | 77 | for { 78 | reference <- lb 79 | if reference != "string" 80 | } sb.append(s"""$newline$indent$indent$indent$reference,""") 81 | 82 | sb.append(s"$newline$indent$indent)") 83 | 84 | val result = if (entryFiles.nonEmpty) { 85 | IO.append(outputFile, sb.toString()) 86 | Seq(outputFile) 87 | } else Seq.empty 88 | print("finished generating MediaTypes.scala\n") 89 | result 90 | }, 91 | mediaTypeCsvDir := baseDirectory.value / "src" / "main" / "mediaTypes", 92 | Compile / sourceGenerators += Compile / generateMediaTypes 93 | ) 94 | 95 | } 96 | 97 | import autoImport._ 98 | 99 | } 100 | -------------------------------------------------------------------------------- /scalike-jdbc-unwrapped/src/main/scala/fx/DatabaseScala.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scalikejdbc.* 4 | import scalikejdbc.DB.{readOnly, CPContext, NoCPContext} 5 | 6 | import java.sql.SQLException 7 | import unwrapped.ensure 8 | 9 | type Database[A] = Control[SQLException] ?=> A 10 | type Transaction[A] = Control[Exception] ?=> A 11 | 12 | extension (db: DB.type) 13 | def readOnlyWithControl[A](execution: DBSession => A)( 14 | using context: CPContext = NoCPContext, 15 | settings: SettingsProvider = SettingsProvider.default): Database[A] = 16 | handle(DB.readOnly(execution))((e: SQLException) => e.shift) 17 | 18 | def localTransaction[A](execution: DBSession => A)( 19 | using context: CPContext = NoCPContext, 20 | boundary: TxBoundary[A] = TxBoundary.Exception.exceptionTxBoundary[A], 21 | settings: SettingsProvider = SettingsProvider.default): Transaction[A] = 22 | handle(DB.localTx(execution))((e: Exception) => e.shift) 23 | -------------------------------------------------------------------------------- /scalike-jdbc-unwrapped/src/test/resources/db/migration/V0__init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE emp( 2 | id SERIAL PRIMARY KEY, 3 | name VARCHAR, 4 | department VARCHAR 5 | ); 6 | 7 | INSERT INTO emp(name, department) VALUES('Ana', 'IT'); 8 | INSERT INTO emp(name, department) VALUES('Mike', 'Marketing'); -------------------------------------------------------------------------------- /scalike-jdbc-unwrapped/src/test/scala/unwrapped/DatabaseSpec.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.sql.SQLException 4 | import scalikejdbc.* 5 | import scalikejdbc.DB.* 6 | import java.util.concurrent.atomic.AtomicInteger 7 | 8 | class DatabaseSpec extends DatabaseSuite { 9 | 10 | val counter: AtomicInteger = new AtomicInteger(0) 11 | val names: Database[List[String]] = DB.readOnlyWithControl { 12 | case given DBSession => 13 | counter.incrementAndGet() 14 | sql"select name from emp".map { rs => rs.string("name") }.list.apply() 15 | } 16 | testUnwrapped("Read only operation executes properly") { 17 | val result = structured(parallel(() => names, () => names)) 18 | assertEqualsUnwrapped(result, (List("Ana", "Mike"), List("Ana", "Mike"))) 19 | assertEqualsUnwrapped(counter.get(), 2) 20 | } 21 | 22 | testUnwrapped("Read only shift when tries to execute a non-read only operation") { 23 | assertUnwrapped(toEither(DB.readOnlyWithControl { 24 | case given DBSession => 25 | sql"update emp set name = 'Never changed in a readonly operation' where id = 1" 26 | .update 27 | .apply() 28 | }).isLeft) 29 | } 30 | 31 | val txCounter: AtomicInteger = new AtomicInteger(0) 32 | val transactResult: Transaction[Int] = DB.localTransaction { 33 | case given DBSession => 34 | txCounter.incrementAndGet() 35 | sql"update emp set name = 'Abc' where id = 1".update.apply() 36 | sql"update emp set name = 'Emp' where id = 2".update.apply() 37 | } 38 | testUnwrapped("Transaction goes well after updating 2 rows.") { 39 | val result = structured(parallel(() => transactResult, () => transactResult)) 40 | assertEqualsUnwrapped(result, (1, 1)) 41 | assertEqualsUnwrapped(txCounter.get(), 2) 42 | } 43 | 44 | val readAndWriteCounter: AtomicInteger = new AtomicInteger(0) 45 | val readAndWriteResult: Transaction[List[String]] = DB.localTransaction { 46 | case given DBSession => 47 | readAndWriteCounter.incrementAndGet() 48 | sql"update emp set name = 'Claire' where id = 1".update.apply() 49 | sql"update emp set name = 'John' where id = 2".update.apply() 50 | sql"select name from emp".map { rs => rs.string("name") }.list.apply() 51 | } 52 | testUnwrapped("Transaction goes well after updating 2 rows and read the result.") { 53 | val result = structured(parallel(() => readAndWriteResult, () => readAndWriteResult)) 54 | assertEqualsUnwrapped(result, (List("Claire", "John"), List("Claire", "John"))) 55 | assertEqualsUnwrapped(txCounter.get(), 2) 56 | } 57 | 58 | testUnwrapped("Transaction goes wrong and rollback after second update raises an exception") { 59 | assertUnwrapped(toEither(DB.localTransaction { 60 | case given DBSession => 61 | sql"update emp set name = 'Never changed in a failed transaction' where id = 1" 62 | .update 63 | .apply() 64 | sql"update emp set name = 'Empty' where inventing_row = 0".update.apply() 65 | }).isLeft) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /scalike-jdbc-unwrapped/src/test/scala/unwrapped/DatabaseSuite.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import munit.unwrapped.UnwrappedSuite 4 | 5 | import scalikejdbc.* 6 | import scalikejdbc.DB.* 7 | import com.dimafeng.testcontainers.PostgreSQLContainer 8 | import com.dimafeng.testcontainers.munit.TestContainerForAll 9 | import org.flywaydb.core.Flyway 10 | import org.testcontainers.utility.DockerImageName 11 | 12 | class DatabaseSuite extends UnwrappedSuite with TestContainerForAll { 13 | 14 | private val postgresV = "12.6" 15 | 16 | private val dbName = "scalafx" 17 | private val dbUserName = "test" 18 | private val dbPassword = "password" 19 | private val driverName = "org.postgresql.Driver" 20 | 21 | override val containerDef: PostgreSQLContainer.Def = PostgreSQLContainer.Def( 22 | DockerImageName.parse(s"postgres:$postgresV"), 23 | databaseName = dbName, 24 | username = dbUserName, 25 | password = dbPassword 26 | ) 27 | 28 | override def afterContainersStart(containers: PostgreSQLContainer): Unit = 29 | Class.forName(driverName) 30 | val ipAddress = containers.container.getHost 31 | val port = containers.container.getMappedPort(5432) 32 | ConnectionPool.singleton(containers.jdbcUrl, dbUserName, dbPassword) 33 | Flyway.configure.dataSource(containers.jdbcUrl, dbUserName, dbPassword).load.migrate 34 | given DB.CPContext = NoCPContext 35 | given SettingsProvider = SettingsProvider.default 36 | } 37 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/main/scala/sttp/fx/HttpExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, *} 5 | import java.net.URI 6 | import java.net.http.HttpResponse 7 | 8 | /** 9 | * Sttp's BodyFromResponseAs can only really work with responses bound to a statically known 10 | * response body type, which must be some kind of byte array or convertible to a byte array. To 11 | * maintain the capability of Http of serving any type of response with body type inference, we 12 | * are using [Rob Norris's Kinda-Curried Type 13 | * Parameters](https://tpolecat.github.io/2015/07/30/infer.html), accessed 14 | * 2022/08/11T23:01:00.000-5:00. 15 | */ 16 | extension (uri: URI) 17 | 18 | private[unwrapped] def patch[A] = PatchPartiallyApplied[A](uri) 19 | 20 | private[unwrapped] def post[A] = PostPartiallyApplied[A](uri) 21 | 22 | private[unwrapped] def put[A] = PutPartiallyApplied[A](uri) 23 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/main/scala/sttp/fx/PatchPartiallyApplied.scala: -------------------------------------------------------------------------------- 1 | package sttp.unwrapped 2 | 3 | import _root_.unwrapped.{given, *} 4 | import java.net.URI 5 | import java.net.http.HttpResponse 6 | import scala.reflect.TypeTest 7 | import scala.concurrent.duration.Duration 8 | 9 | /** 10 | * Sttp's BodyFromResponseAs can only really work with responses bound to a statically known 11 | * response body type, which must be some kind of byte array or convertible to a byte array. To 12 | * maintain the capability of Http of serving any type of response with body type inference, we 13 | * are using [Rob Norris's Kinda-Curried Type 14 | * Parameters](https://tpolecat.github.io/2015/07/30/infer.html), accessed 15 | * 2022/08/11T23:01:00.000-5:00. 16 | * 17 | * @tparam A 18 | * The type of the response to be returned when apply is called. 19 | */ 20 | private[unwrapped] class PatchPartiallyApplied[A](private val uri: URI) extends Equals: 21 | /** 22 | * Applies the Http effect, returning the HttpResponse 23 | * 24 | * @tparam B 25 | * The type of the body pased to the request. 26 | * @param body 27 | * The body of the request. 28 | * @param headels 29 | * Variable-length list of headers to send with the request. 30 | * @return 31 | * The http response in an Http effect. 32 | */ 33 | def apply[B](body: B, timeout: Duration, headers: HttpHeader*)( 34 | using HttpResponseMapper[A], 35 | HttpBodyMapper[B]): Http[HttpResponse[A]] = 36 | uri.PATCH[A, B](body, timeout, headers: _*) 37 | 38 | override def canEqual(that: Any): Boolean = 39 | that.isInstanceOf[PatchPartiallyApplied[A]] // cannot disable unchecked warning 40 | 41 | override def equals(x: Any): Boolean = 42 | canEqual(x) && x.asInstanceOf[PatchPartiallyApplied[A]].uri == uri 43 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/main/scala/sttp/fx/PostPartiallyApplied.scala: -------------------------------------------------------------------------------- 1 | package sttp.unwrapped 2 | 3 | import _root_.unwrapped.{given, *} 4 | import java.net.URI 5 | import java.net.http.HttpResponse 6 | import scala.concurrent.duration.Duration 7 | 8 | /** 9 | * Sttp's BodyFromResponseAs can only really work with responses bound to a statically known 10 | * response body type, which must be some kind of byte array or convertible to a byte array. To 11 | * maintain the capability of Http of serving any type of response with body type inference, we 12 | * are using [Rob Norris's Kinda-Curried Type 13 | * Parameters](https://tpolecat.github.io/2015/07/30/infer.html), accessed 14 | * 2022/08/11T23:01:00.000-5:00. 15 | */ 16 | private[unwrapped] class PostPartiallyApplied[A](private val uri: URI) extends Equals { 17 | def apply[B](body: B, timeout: Duration, headers: HttpHeader*)( 18 | using HttpResponseMapper[A], 19 | HttpBodyMapper[B]): Http[HttpResponse[A]] = 20 | uri.POST[A, B](body, timeout, headers: _*) 21 | 22 | override def canEqual(that: Any): Boolean = that.isInstanceOf[PostPartiallyApplied[?]] 23 | override def equals(that: Any): Boolean = 24 | canEqual(that) && that.asInstanceOf[PostPartiallyApplied[A]].uri == uri 25 | } 26 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/main/scala/sttp/fx/PutPartiallyApplied.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, *} 5 | import java.net.URI 6 | import java.net.http.HttpResponse 7 | import scala.concurrent.duration.Duration 8 | 9 | /** 10 | * Sttp's BodyFromResponseAs can only really work with responses bound to a statically known 11 | * response body type, which must be some kind of byte array or convertible to a byte array. To 12 | * maintain the capability of Http of serving any type of response with body type inference, we 13 | * are using [Rob Norris's Kinda-Curried Type 14 | * Parameters](https://tpolecat.github.io/2015/07/30/infer.html), accessed 15 | * 2022/08/11T23:01:00.000-5:00. 16 | */ 17 | private[unwrapped] class PutPartiallyApplied[A](private val uri: URI) extends Equals { 18 | def apply[B](body: B, timeout: Duration, headers: HttpHeader*)( 19 | using HttpResponseMapper[A], 20 | HttpBodyMapper[B]): Http[HttpResponse[A]] = 21 | uri.PUT[A, B](body, timeout, headers: _*) 22 | override def canEqual(that: Any): Boolean = that.isInstanceOf[PutPartiallyApplied[?]] 23 | override def equals(that: Any): Boolean = that.asInstanceOf[PutPartiallyApplied[A]].uri == uri 24 | } 25 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/main/scala/sttp/fx/ReceiveStreams.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import sttp.capabilities.Streams 5 | import _root_.unwrapped.Receive 6 | import _root_.unwrapped.Send 7 | 8 | /** 9 | * Models the streaming capability of TBD. 10 | */ 11 | trait ReceiveStreams extends Streams[ReceiveStreams]: 12 | override type BinaryStream = Receive[Byte] 13 | 14 | /** 15 | * Pipe[A, B] is Receive[A]#transform in TBD. 16 | */ 17 | override type Pipe[A, B] = Send[B] ?=> (A => Unit) => Receive[B] 18 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/resources/brand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .brand 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/FullBackendFixtures.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, _} 5 | import com.sun.net.httpserver.* 6 | import munit.unwrapped.UnwrappedSuite 7 | import sttp.client3.* 8 | import sttp.model.headers.Accepts 9 | 10 | import java.net.InetSocketAddress 11 | import java.nio.file.Files 12 | import java.time.* 13 | import java.util.concurrent.Executors 14 | import scala.io.Source 15 | import scala.jdk.CollectionConverters.* 16 | import java.nio.charset.StandardCharsets 17 | import java.util.concurrent.atomic.AtomicReference 18 | import java.util.concurrent.ExecutorService 19 | 20 | private[unwrapped] trait FullBackendFixtures { self: UnwrappedSuite => 21 | 22 | val server = new AtomicReference[HttpServer]() 23 | val executor = new AtomicReference[ExecutorService](Executors.newVirtualThreadPerTaskExecutor) 24 | 25 | def getServerAddress() = { 26 | s"http:/${server.get.getAddress()}/" 27 | } 28 | 29 | override def beforeAll() = { 30 | val httpServer = HttpServer.create(InetSocketAddress(0), 0) 31 | httpServer.setExecutor(executor.get) 32 | httpServer.createContext("/", echoHandler) 33 | httpServer.start 34 | server.set(httpServer) 35 | } 36 | 37 | override def afterAll() = { 38 | server.get.stop(0) 39 | executor.get.shutdown 40 | } 41 | 42 | val imageFileResource = FunFixture( 43 | setup = _ => 44 | Resource( 45 | Files.createTempFile("47DegLogo", "svg").toFile(), 46 | (file, _) => () 47 | ), 48 | teardown = _ => ()) 49 | 50 | val resultFileResource = FunFixture( 51 | setup = _ => 52 | Resource( 53 | Files.createTempFile("result", ".txt").toFile(), 54 | (file, _) => () 55 | ), 56 | teardown = _ => ()) 57 | 58 | val resultPutFileResource = FunFixture( 59 | setup = _ => 60 | Resource( 61 | Files.createTempFile("resultPut", ".txt").toFile(), 62 | (file, _) => () 63 | ), 64 | teardown = _ => ()) 65 | 66 | val resultPatchFileResource = FunFixture( 67 | setup = _ => 68 | Resource( 69 | Files.createTempFile("resultPatch", ".txt").toFile(), 70 | (file, _) => () 71 | ), 72 | teardown = _ => ()) 73 | 74 | val testBody = FunFixture(setup = _ => "test", teardown = _ => ()) 75 | 76 | val testBodyAndFile = FunFixture.map2(testBody, resultFileResource) 77 | 78 | val testBodyAndPutFile = 79 | FunFixture.map2(testBody, resultPutFileResource) 80 | 81 | val testBodyAndPatchFile = 82 | FunFixture.map2(testBody, resultPatchFileResource) 83 | 84 | val file = imageFileResource 85 | 86 | lazy val echoHandler = HttpHandlers.handleOrElse( 87 | request => { 88 | val method = request.getRequestMethod() 89 | val path = request.getRequestURI().getPath() 90 | method match { 91 | case "POST" | "PATCH" | "PUT" => path.contains("echo") 92 | case _ => false 93 | } 94 | }, 95 | (exchange: HttpExchange) => { 96 | try { 97 | val requestInputStream = exchange.getRequestBody() 98 | val outputStream = exchange.getResponseBody() 99 | val requestHeaders = exchange.getRequestHeaders() 100 | val requestHeaderKeys = requestHeaders.keySet().iterator().asScala 101 | val mutableResponseHeaders = exchange.getResponseHeaders() 102 | for { 103 | key <- requestHeaderKeys 104 | } mutableResponseHeaders.put(key, requestHeaders.get(key)) 105 | val result = new String(requestInputStream.readAllBytes) 106 | exchange.sendResponseHeaders(200, result.length()) 107 | outputStream.write(result.getBytes) 108 | } catch { 109 | case _ => () 110 | } finally { 111 | exchange.close() 112 | } 113 | }, 114 | echoStreamHandler 115 | ) 116 | 117 | lazy val echoStreamHandler = HttpHandlers.handleOrElse( 118 | request => { 119 | val method = request.getRequestMethod() 120 | val path = request.getRequestURI().getPath() 121 | method match { 122 | case "POST" | "PATCH" | "PUT" => path.contains("stream") 123 | case _ => false 124 | } 125 | }, 126 | (exchange: HttpExchange) => { 127 | try { 128 | val requestInputStream = exchange.getRequestBody() 129 | val outputStream = exchange.getResponseBody() 130 | val requestHeaders = exchange.getRequestHeaders() 131 | val requestHeaderKeys = requestHeaders.keySet().iterator().asScala 132 | val mutableResponseHeaders = exchange.getResponseHeaders() 133 | for { 134 | key <- requestHeaderKeys 135 | } mutableResponseHeaders.put(key, requestHeaders.get(key)) 136 | val result = new String(requestInputStream.readAllBytes()) 137 | exchange.sendResponseHeaders(200, result.length()) 138 | outputStream.write(result.getBytes()) 139 | outputStream.flush() 140 | } catch { 141 | case _ => () 142 | } finally { 143 | exchange.close() 144 | } 145 | }, 146 | deleteHandler 147 | ) 148 | 149 | lazy val getImageHandler = HttpHandlers.handleOrElse( 150 | request => { 151 | val method = request.getRequestMethod() 152 | val path = request.getRequestURI().getPath() 153 | method match { 154 | case "GET" => path.contains("47DegLogo.svg") 155 | case _ => false 156 | } 157 | }, 158 | (exchange: HttpExchange) => { 159 | try { 160 | val outputStream = exchange.getResponseBody() 161 | val mutableResponseHeaders = exchange.getResponseHeaders() 162 | mutableResponseHeaders.add("Content-Type", MediaTypes.image.`svg+xml`.value) 163 | exchange.sendResponseHeaders(200, 0) 164 | getClass.getResourceAsStream("/brand.svg").transferTo(outputStream) 165 | } catch { 166 | case _ => () 167 | } finally { 168 | exchange.close() 169 | } 170 | }, 171 | headHandler 172 | ) 173 | 174 | lazy val headHandler = HttpHandlers.handleOrElse( 175 | request => { 176 | val method = request.getRequestMethod() 177 | val path = request.getRequestURI().getPath() 178 | method match { 179 | case "HEAD" | "OPTIONS" | "TRACE" => true 180 | case _ => false 181 | } 182 | }, 183 | (exchange: HttpExchange) => { 184 | try { 185 | val outputStream = exchange.getResponseBody() 186 | val requestHeaders = exchange.getRequestHeaders() 187 | val mutableResponseHeaders = exchange.getResponseHeaders() 188 | requestHeaders.forEach { (headerName, headerValues) => 189 | headerValues.forEach(value => mutableResponseHeaders.add(headerName, value)) 190 | } 191 | exchange.sendResponseHeaders(200, 0) 192 | } catch { 193 | case _ => () 194 | } finally { 195 | exchange.close() 196 | } 197 | }, 198 | timeoutHandler 199 | ) 200 | 201 | lazy val timeoutHandler = HttpHandlers.handleOrElse( 202 | request => { 203 | val path = request.getRequestURI().getPath() 204 | path.contains("shouldTimeout") 205 | }, 206 | (exchange: HttpExchange) => { 207 | try { 208 | val outputStream = exchange.getResponseBody() 209 | val requestHeaders = exchange.getRequestHeaders() 210 | val mutableResponseHeaders = exchange.getResponseHeaders() 211 | requestHeaders.forEach { (headerName, headerValues) => 212 | headerValues.forEach(value => mutableResponseHeaders.add(headerName, value)) 213 | } 214 | Thread.sleep(30000) 215 | exchange.sendResponseHeaders(200, 0) 216 | outputStream.flush() 217 | } catch { 218 | case _ => () 219 | } finally { 220 | exchange.close() 221 | } 222 | }, 223 | fallbackHttpHandler 224 | ) 225 | 226 | lazy val deleteHandler = HttpHandlers.handleOrElse( 227 | request => { 228 | val method = request.getRequestMethod() 229 | val path = request.getRequestURI().getPath() 230 | method match { 231 | case "DELETE" => path.contains("toDelete") 232 | case _ => false 233 | } 234 | }, 235 | HttpHandlers.of(OK.value, getSuccessHeaders, OK.statusText), 236 | getImageHandler 237 | ) 238 | 239 | lazy val fallbackHttpHandler = 240 | HttpHandlers.of(404, notFoundHeaders, "Not Found") 241 | 242 | lazy val getSuccessHeaders = 243 | Headers.of( 244 | "Content-Type", 245 | "text/plain; charset=UTF-8", 246 | "Last-Modified", 247 | "Thu, 10 Mar 2022 23:21:48 GMT", 248 | "Connection", 249 | "keep-alive", 250 | "Date", 251 | "Fri, 01 Jul 2022 04:22:42 GMT" 252 | ) 253 | 254 | lazy val notFoundHeaders = 255 | Headers.of( 256 | "Content-Type", 257 | "text/plain; charset=UTF-8", 258 | "Last-Modified", 259 | "Thu, 10 Mar 2022 23:21:48 GMT", 260 | "Connection", 261 | "keep-alive", 262 | "Date", 263 | "Fri, 01 Jul 2022 04:22:42 GMT" 264 | ) 265 | 266 | } 267 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/HttpExtensionsSuite.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import munit.unwrapped.UnwrappedSuite 5 | import java.net.URI 6 | 7 | class HttpExtensionsSuite extends UnwrappedSuite, HttpExtensionsSuiteFixtures { 8 | 9 | uri.testUnwrapped("uri.patch should return a PatchPartiallyApplied") { uri => 10 | assertEqualsUnwrapped(uri.patch[String], PatchPartiallyApplied[String](uri)) 11 | } 12 | 13 | uri.testUnwrapped("uri.post should return a PostPartiallyApplied") { uri => 14 | assertEqualsUnwrapped(uri.post[String], PostPartiallyApplied[String](uri)) 15 | } 16 | 17 | uri.testUnwrapped("uri.put should return a PutPartiallyApplied") { uri => 18 | assertEqualsUnwrapped(uri.put[String], PutPartiallyApplied(uri)) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/HttpExtensionsSuiteFixtures.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{Accepted, Created} 5 | import _root_.unwrapped.Resource 6 | import com.sun.net.httpserver.* 7 | import munit.unwrapped.UnwrappedSuite 8 | import sttp.client3.StringBody 9 | 10 | import java.net.InetSocketAddress 11 | import java.net.URI 12 | import java.util.concurrent.Executors 13 | 14 | trait HttpExtensionsSuiteFixtures { self: UnwrappedSuite => 15 | val uri = FunFixture(setup = _ => URI("https://47Deg.com"), teardown = _ => ()) 16 | 17 | val stringBody = FunFixture(setup = _ => StringBody("hello", "UTF-8"), teardown = _ => ()) 18 | 19 | def httpServer(handler: HttpHandler) = 20 | FunFixture( 21 | setup = _ => { 22 | for { 23 | server <- Resource( 24 | HttpServer.create(InetSocketAddress(0), 0), 25 | (server, _) => server.stop(0)) 26 | serverExecutor <- Resource( 27 | Executors.newVirtualThreadPerTaskExecutor, 28 | (executor, _) => executor.shutdown()) 29 | _ = server.setExecutor(serverExecutor) 30 | httpContext = server.createContext("/root", handler) 31 | _ = server.start 32 | } yield s"http:/${server.getAddress()}/root" 33 | }, 34 | teardown = server => { 35 | () 36 | } 37 | ) 38 | 39 | lazy val notFoundHeaders = 40 | Headers.of( 41 | "Content-Type", 42 | "text/plain; charset=UTF-8", 43 | "Last-Modified", 44 | "Thu, 10 Mar 2022 23:21:48 GMT", 45 | "Connection", 46 | "keep-alive", 47 | "Date", 48 | "Fri, 01 Jul 2022 04:22:42 GMT" 49 | ) 50 | 51 | lazy val getSuccessHeaders = 52 | Headers.of( 53 | "Content-Type", 54 | "text/plain; charset=UTF-8", 55 | "Last-Modified", 56 | "Thu, 10 Mar 2022 23:21:48 GMT", 57 | "Connection", 58 | "keep-alive", 59 | "Date", 60 | "Fri, 01 Jul 2022 04:22:42 GMT" 61 | ) 62 | 63 | lazy val fallbackHttpHandler = 64 | HttpHandlers.of(404, notFoundHeaders, "Not Found") 65 | 66 | lazy val patchHandler = 67 | HttpHandlers.handleOrElse( 68 | request => 69 | request 70 | .getRequestMethod() == "PATCH" && request.getRequestURI().getPath().contains("ping"), 71 | HttpHandlers.of(Accepted.value, getSuccessHeaders, Accepted.statusText), 72 | fallbackHttpHandler 73 | ) 74 | 75 | lazy val postHandler = HttpHandlers.handleOrElse( 76 | request => 77 | request 78 | .getRequestMethod() == "POST" && request.getRequestURI().getPath().contains("ping"), 79 | HttpHandlers.of(Created.value, getSuccessHeaders, Created.statusText), 80 | fallbackHttpHandler 81 | ) 82 | 83 | lazy val putHandler = HttpHandlers.handleOrElse( 84 | request => 85 | request.getRequestMethod() == "PUT" && request.getRequestURI().getPath().contains("ping"), 86 | HttpHandlers.of(Accepted.value, getSuccessHeaders, Accepted.statusText), 87 | fallbackHttpHandler 88 | ) 89 | 90 | } 91 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/PatchPartiallyAppliedSuite.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, *} 5 | import munit.unwrapped.UnwrappedSuite 6 | import sttp.client3.StringBody 7 | 8 | import java.net.URI 9 | import java.net.http.HttpResponse 10 | import scala.concurrent.duration.* 11 | import scala.language.postfixOps 12 | 13 | class PatchPartiallyAppliedSuite extends UnwrappedSuite, HttpExtensionsSuiteFixtures: 14 | 15 | FunFixture 16 | .map2(httpServer(patchHandler), stringBody) 17 | .testUnwrapped("apply[Receive[Byte] should return the correct stream of bytes") { 18 | case (serverAddressResource, body) => 19 | serverAddressResource.use { serverAddressBase => 20 | given HttpBodyMapper[StringBody] = body.toHttpBodyMapper() 21 | val response: Http[HttpResponse[Receive[Byte]]] = 22 | URI(s"$serverAddressBase/ping").patch[Receive[Byte]](body, 5 seconds) 23 | val result: Http[String] = response.fmap { response => 24 | new String(response.body().toList.toArray) 25 | } 26 | assertEqualsUnwrapped(result.httpValue, Accepted.statusText) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/PostPartiallyAppliedSuite.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, *} 5 | import munit.unwrapped.UnwrappedSuite 6 | import sttp.client3.StringBody 7 | import scala.concurrent.duration.* 8 | 9 | import java.net.URI 10 | import scala.language.postfixOps 11 | 12 | class PostPartiallyAppliedSuite extends UnwrappedSuite, HttpExtensionsSuiteFixtures: 13 | 14 | FunFixture 15 | .map2(httpServer(postHandler), stringBody) 16 | .testUnwrapped("URI#post[Receive[Byte]](stringBody) should return as a stream of bytes") { 17 | case (serverBaseAddressResource, stringBody) => 18 | serverBaseAddressResource.use { serverBaseAddress => 19 | given HttpBodyMapper[StringBody] = stringBody.toHttpBodyMapper() 20 | assertEqualsUnwrapped( 21 | URI(s"$serverBaseAddress/ping") 22 | .post[Receive[Byte]](stringBody, 5 seconds) 23 | .fmap { response => new String(response.body().toList.toArray) } 24 | .httpValue, 25 | Created.statusText 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/PutPartiallyAppliedSuite.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, *} 5 | import munit.unwrapped.UnwrappedSuite 6 | import sttp.client3.StringBody 7 | import scala.concurrent.duration.* 8 | 9 | import java.net.URI 10 | import scala.language.postfixOps 11 | 12 | class PutPartiallyAppliedSuite extends UnwrappedSuite, HttpExtensionsSuiteFixtures { 13 | 14 | FunFixture 15 | .map2(httpServer(putHandler), stringBody) 16 | .testUnwrapped("URI#put[Receive[Byte]](body) should return a the correct stream of bytes") { 17 | case (serverAddressResource, body) => 18 | serverAddressResource.use { serverAddressBase => 19 | given HttpBodyMapper[StringBody] = body.toHttpBodyMapper() 20 | assertEqualsUnwrapped( 21 | URI(s"$serverAddressBase/ping") 22 | .put[Receive[Byte]](body, 5 seconds) 23 | .fmap { response => new String(response.body.toList.toArray) } 24 | .httpValue, 25 | Accepted.statusText 26 | ) 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/StatusCodeToStatusCodeSuite.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import munit.unwrapped.UnwrappedSuite 5 | import hedgehog.munit.HedgehogAssertions 6 | import _root_.unwrapped.{given, *} 7 | import sttp.unwrapped.StatusCodeToStatusCode.given_StatusCodeToStatusCode_Int_StatusCode 8 | import sttp.unwrapped.StatusCodeToStatusCode.smStatusCode 9 | import sttp.unwrapped.StatusCodeToStatusCode.given_StatusCodeToStatusCode_StatusCode_StatusCode 10 | 11 | import hedgehog.Gen 12 | 13 | class StatusCodeToStatusCodeSuite extends UnwrappedSuite, HedgehogAssertions: 14 | lazy val statusCodeInts = 15 | StatusCode.statusCodes.toList.filterNot(c => c == 509 || c == 425 || c == 418) 16 | lazy val statusCodeGen = Gen.element(statusCodeInts.head, statusCodeInts.tail) 17 | 18 | propertyFX( 19 | "Valid status code integers.toStatusCode.toStatusCode.value should be the original integer") { 20 | for { 21 | x <- statusCodeGen.forAll 22 | } yield assertEquals(x.toStatusCode.toStatusCode.toStatusCode.httpValue.code, x) 23 | } 24 | -------------------------------------------------------------------------------- /sttp-unwrapped/src/test/scala/sttp/fx/ToHttpBodyMapperFixtures.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | package unwrapped 3 | 4 | import _root_.unwrapped.{given, *} 5 | import munit.unwrapped.UnwrappedSuite 6 | import sttp.client3.NoBody 7 | import sttp.client3.ByteArrayBody 8 | import sttp.client3.StringBody 9 | 10 | import java.nio.ByteBuffer 11 | import java.util.concurrent.Flow 12 | import java.nio.charset.Charset 13 | import java.nio.charset.StandardCharsets 14 | import sttp.client3.ByteBufferBody 15 | import java.io.ByteArrayInputStream 16 | import sttp.client3.InputStreamBody 17 | import java.nio.file.Files 18 | import sttp.client3.FileBody 19 | import sttp.client3.internal.SttpFile 20 | import sttp.client3.StreamBody 21 | import sttp.client3.MultipartBody 22 | import sttp.capabilities.Effect 23 | 24 | trait ToHttpBodyMapperFixtures { self: UnwrappedSuite => 25 | 26 | val noBody = FunFixture(setup = _ => NoBody, teardown = _ => ()) 27 | 28 | val byteArrayBody = 29 | FunFixture(setup = _ => ByteArrayBody("test".getBytes()), teardown = _ => ()) 30 | 31 | val byteBufferBody = FunFixture( 32 | setup = _ => ByteBufferBody(ByteBuffer.wrap("test".getBytes())), 33 | teardown = _ => ()) 34 | 35 | val inputStreamBody = FunFixture( 36 | setup = _ => InputStreamBody(new ByteArrayInputStream("test".getBytes)), 37 | teardown = _ => ()) 38 | 39 | val fileBody = FunFixture( 40 | setup = _ => { 41 | Resource.apply( 42 | { 43 | val file = Files.createTempFile("fileBodyTest", ".txt") 44 | Files.write(file, "test".getBytes()) 45 | FileBody(SttpFile.fromPath(file)) 46 | }, 47 | (f: FileBody, exitCase: ExitCase) => { 48 | Files.deleteIfExists(f.f.toPath) 49 | } 50 | ) 51 | }, 52 | teardown = _ => () 53 | ) 54 | 55 | val streamBody = FunFixture( 56 | setup = _ => StreamBody(new ReceiveStreams {})(streamOf("test".getBytes().toList: _*)), 57 | teardown = _ => ()) 58 | 59 | val multiparts = FunFixture.map3( 60 | noBody, 61 | byteArrayBody, 62 | FunFixture.map3(byteBufferBody, inputStreamBody, FunFixture.map2(fileBody, streamBody))) 63 | 64 | val stringBody = 65 | FunFixture(setup = _ => StringBody("test", StandardCharsets.UTF_8.name), teardown = _ => ()) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Bind.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | extension [R, A](fa: Either[R, A]) def bind(using Errors[R]): A = fa.fold(_.shift, identity) 4 | 5 | extension [R, A](fa: Right[R, A]) def bind: A = fa.value 6 | 7 | extension [R, A](fa: List[Either[R, A]]) def bind(using Errors[R]): List[A] = fa.map(_.bind) 8 | 9 | extension [A](fa: Option[A]) 10 | def bind(using Errors[None.type]): A = fa.fold(None.shift)(identity) 11 | 12 | extension [A](fa: Some[A]) def bind: A = fa.value 13 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Console.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import unwrapped.* 4 | import scala.annotation.implicitNotFound 5 | import scala.io.StdIn.readLine 6 | import scala.annotation.tailrec 7 | 8 | class EndOfLine extends RuntimeException("reached end of line") 9 | 10 | @implicitNotFound( 11 | "Missing capability:\n% Console" 12 | ) 13 | trait Console: 14 | 15 | def read(): String 16 | 17 | extension (s: String) 18 | def write(): Unit 19 | def writeLine(): Unit 20 | 21 | object Console: 22 | given default(using Throws[EndOfLine]): Console = StandardConsole() 23 | 24 | /** 25 | * If your method is not an extension then it needs side syntax like these or users would need 26 | * to summon. 27 | */ 28 | def read()(using console: Console): String = 29 | console.read() 30 | 31 | class StandardConsole(using Throws[EndOfLine]) extends Console: 32 | def read(): String = 33 | val r = readLine() 34 | if (r != null) r 35 | else throw EndOfLine() 36 | 37 | extension (s: String) 38 | def write(): Unit = print(s) 39 | def writeLine(): Unit = println(s) 40 | 41 | class FakeConsole(var input: String)(using Throws[EndOfLine]) extends Console: 42 | var output: String = "" 43 | 44 | def read(): String = 45 | if input.isEmpty then throw EndOfLine() 46 | else 47 | input.split('\n') match 48 | case Array(r, rest*) => 49 | input = rest.mkString("\n") 50 | r 51 | case _ => 52 | null 53 | 54 | extension (s: String) 55 | def write(): Unit = output += s 56 | def writeLine(): Unit = output += (s + "\n") 57 | 58 | @tailrec 59 | def program(using Errors[String], Console): String = 60 | "what is your name?".writeLine() 61 | read() match 62 | case "" => 63 | "empty name".writeLine() 64 | program 65 | case "me" => 66 | "wrong name!".writeLine() 67 | "wrong name".raise 68 | case name => 69 | s"hello $name" 70 | 71 | @main def consoleStandard() = 72 | import unwrapped.Throws.unsafeExceptions 73 | 74 | val value: String = 75 | run(program) 76 | 77 | @main def consoleFake() = 78 | import unwrapped.Throws.unsafeExceptions 79 | given Console = FakeConsole("") 80 | val value: String = 81 | try run(program) 82 | catch case eol: EndOfLine => "Reached end" 83 | 84 | println(value) 85 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Continuation.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.annotation.implicitNotFound 4 | import scala.util.control.ControlThrowable 5 | import java.util.UUID 6 | import scala.util.control.NonFatal 7 | import java.util.concurrent.ExecutionException 8 | import scala.annotation.tailrec 9 | 10 | object Continuation: 11 | inline def fold[R, A, B]( 12 | inline program: Control[R] ?=> A 13 | )(inline recover: R => B, inline transform: A => B): B = { 14 | var result: Any | Null = null 15 | implicit val control = new Control[R] { 16 | private[unwrapped] val token: String = UUID.randomUUID.toString 17 | 18 | extension (r: R) 19 | def shift[A]: A = 20 | throw ControlToken(token, r, recover.asInstanceOf[Any => Any]) 21 | } 22 | try { 23 | result = transform(program(using control)) 24 | } catch { 25 | case e: Throwable => 26 | result = handleControl(control, e) 27 | } 28 | result.asInstanceOf[B] 29 | } 30 | 31 | @tailrec def handleControl(control: Control[_], e: Throwable): Any = 32 | e match 33 | case e: ExecutionException => 34 | handleControl(control, e.getCause) 35 | case e @ ControlToken(token, shifted, recover) => 36 | if (control.token == token) 37 | recover(shifted) 38 | else 39 | throw e 40 | case _ => throw e 41 | 42 | private case class ControlToken( 43 | token: String, 44 | shifted: Any, 45 | recover: (Any) => Any 46 | ) extends ControlThrowable 47 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Control.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | /** 6 | * [[Control]] describes the ability to short-circuit an abilities function with a value of 7 | * [[R]] 8 | */ 9 | @implicitNotFound( 10 | "this function may shift control to ${R} and requires capability:\n% Control[${R}]" 11 | ) 12 | trait Control[-R]: 13 | private[unwrapped] val token: String 14 | 15 | /** 16 | * Short-circuits the computation of [[A]] with a value of [[R]] 17 | */ 18 | extension (r: R) def shift[A]: A 19 | 20 | object Control: 21 | /** 22 | * All functions that follow the happy path and consider no control automatically obtain the 23 | * ability of no Control. 24 | * 25 | * Evidence of no control helps monadic values that frequently short-circuit like Either, 26 | * Option, etc to disregard the Control capability if users just compute through happy paths. 27 | */ 28 | given Pure: Control[Nothing] with 29 | 30 | private[unwrapped] val token: String = "Control.nothing.token" 31 | 32 | extension (r: Nothing) def shift[A]: A = throw new RuntimeException("impossible") 33 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Errors.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | @implicitNotFound( 6 | "Missing capability:\n% Errors[${R}]" 7 | ) 8 | type Errors[R] = Control[R] 9 | 10 | extension [R](r: R) 11 | def raise[A](using Errors[R]): A = r.shift 12 | def ensure(value: Boolean)(using Errors[R]): Unit = 13 | if (value) () else r.shift 14 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Fiber.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.util.concurrent.Future 4 | import java.util.concurrent.CompletableFuture 5 | 6 | opaque type Fiber[A] = Future[A] 7 | 8 | extension [A](fiber: Fiber[A]) 9 | def join: A = fiber.get 10 | def cancel(mayInterrupt: Boolean = true): Boolean = 11 | fiber.cancel(mayInterrupt) 12 | 13 | def uncancellable[A](fn: () => A): A = { 14 | val promise = new CompletableFuture[A]() 15 | Thread 16 | .ofVirtual() 17 | .start(() => { 18 | try promise.complete(fn()) 19 | catch 20 | case t: Throwable => 21 | promise.completeExceptionally(t) 22 | }) 23 | promise.join 24 | } 25 | 26 | def fork[B](f: () => B)(using structured: Structured): Fiber[B] = 27 | structured.forked(callableOf(f)) 28 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Nullable.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * An unwrapped option type. Allows for nullability handling without wrapping the nullable 5 | * value. 6 | */ 7 | opaque type Nullable[A] = A | Null 8 | 9 | object Nullable: 10 | extension [A: Manifest](a: Nullable[A]) 11 | /** 12 | * Alias of value 13 | */ 14 | def v(using Errors[NullPointerException]): A = value 15 | 16 | /** 17 | * True if the nullable is non-null 18 | */ 19 | def exists: Boolean = 20 | a match 21 | case x: A => true 22 | case _ => false 23 | 24 | /** 25 | * @return 26 | * true If this nullable contains a value. 27 | */ 28 | def nonEmpty: Boolean = exists 29 | 30 | /** 31 | * Unifies the value to A or shifts control to a NullPointerException, 32 | * @return 33 | * the value of a in a NullPointerException context 34 | */ 35 | def value(using Errors[NullPointerException]): A = 36 | getOrElse(NullPointerException().shift) 37 | 38 | /** 39 | * @param default 40 | * A non-null default value to use if the current value is null. 41 | * @return 42 | * the value as a nullable if the nullable is not null, or a default nullable if it is 43 | * null. 44 | */ 45 | def getOrElse(default: => A): A = 46 | a match 47 | case x: A => x 48 | case _ => default 49 | 50 | /** 51 | * @param default 52 | * A nullable value to use as the default 53 | * @return 54 | * the value as a nullable if the nullable is not null, or a default nullable if it is 55 | * null. 56 | */ 57 | def orElse(default: => Nullable[A]): Nullable[A] = 58 | if (exists) a else default 59 | 60 | /** 61 | * @return 62 | * f applied to the value if it exists 63 | */ 64 | def map[B](f: A => B): Nullable[B] = 65 | a match 66 | case x: A => f(x) 67 | case _ => Nullable.none 68 | 69 | /** 70 | * @return 71 | * f applied to the value if it exists 72 | */ 73 | def bind[B](f: A => Nullable[B]): Nullable[B] = 74 | a match 75 | case x: A => f(x) 76 | case _ => null 77 | 78 | /** 79 | * Creates a Nullable from a value. 80 | */ 81 | def apply[A](a: A | Null): Nullable[A] = a 82 | 83 | /** 84 | * Returns a none 85 | */ 86 | def none[A]: Nullable[A] = null 87 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Parallel.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.annotation.{implicitNotFound, tailrec, targetName} 4 | import unwrapped.Id 5 | 6 | import scala.Tuple.{InverseMap, IsMappedBy} 7 | 8 | type ParBind[F[_]] = [t] => F[t] => t 9 | 10 | private type Id[+A] = A 11 | 12 | def par[F[_], X <: Tuple](x: X, parBind: ParBind[F])( 13 | using IsMappedBy[F][X], 14 | Structured): InverseMap[X, F] = 15 | val fibers = 16 | x.map[Fiber]( 17 | [r] => (r: r) => fork(() => parBind(r.asInstanceOf[F[r]])) 18 | ) 19 | joinAll 20 | val awaited = 21 | fibers.map[Id]( 22 | [r] => (r: r) => r.asInstanceOf[Fiber[r]].join 23 | ) 24 | awaited.asInstanceOf[InverseMap[X, F]] 25 | 26 | extension [X <: Tuple](x: X)(using IsMappedBy[Function0][X], Structured) 27 | def parallel: InverseMap[X, Function0] = 28 | par(x, [t] => (f: () => t) => f.apply) 29 | 30 | @main def ParallelFunctionExample = 31 | val results: Structured ?=> (String, Int, Double) = 32 | parallel( 33 | () => "1", 34 | () => 0, 35 | () => 47.03 36 | ) 37 | println(structured(results)) 38 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Resource.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | import scala.util.control.NonFatal 5 | import Tuple.{InverseMap, IsMappedBy, Map} 6 | import scala.annotation.implicitNotFound 7 | 8 | class Resource[A]( 9 | val acquire: Resources ?=> A, 10 | val release: (A, ExitCase) => Unit 11 | ): 12 | 13 | def this(value: Resources ?=> A) = this(value, (_, _) => ()) 14 | 15 | def use[B](f: A => B): B = bracketCase(() => acquire, f, release) 16 | 17 | def map[B](f: (A) => B): Resource[B] = 18 | Resource(f(this.bind)) 19 | 20 | def flatMap[B](f: (A) => Resource[B]): Resource[B] = 21 | Resource(f(this.bind).bind) 22 | 23 | def bind: A = 24 | bracketCase( 25 | () => { 26 | val a = acquire 27 | val finalizer: (ExitCase) => Unit = (ex: ExitCase) => release(a, ex) 28 | summon[Resources].finalizers.updateAndGet(finalizer +: _) 29 | a 30 | }, 31 | identity, 32 | (a, ex) => 33 | // Only if ExitCase.Failure, or ExitCase.Cancelled during acquire we cancel 34 | // Otherwise we've saved the finalizer, and it will be called from somewhere else. 35 | if (ex != ExitCase.Completed) 36 | val e = cancelAll(summon[Resources].finalizers.get(), ex, null) 37 | val e2 = 38 | try 39 | release(a, ex) 40 | null 41 | catch case NonFatal(e) => e 42 | val error = composeErrors(e, e2) 43 | throw error 44 | ) 45 | 46 | class Resources(val finalizers: AtomicReference[List[(ExitCase) => Unit]]) 47 | 48 | object Resources: 49 | given Resources = new Resources( 50 | AtomicReference( 51 | List.empty 52 | )) 53 | 54 | enum ExitCase: 55 | case Completed 56 | case Cancelled(exception: Throwable) 57 | case Failure(failure: Throwable) 58 | 59 | private[unwrapped] def cancelAll( 60 | finalizers: List[(ExitCase) => Unit], 61 | exitCase: ExitCase, 62 | first: Throwable | Null = null 63 | ): Throwable | Null = finalizers 64 | .map(f => { 65 | try 66 | f(exitCase) 67 | null 68 | catch case NonFatal(e) => e 69 | }) 70 | .fold(first)((it, acc) => 71 | if (acc != null) composeErrors(acc, it) 72 | else it) 73 | 74 | private[unwrapped] def composeErrors( 75 | left: Throwable | Null, 76 | right: Throwable | Null 77 | ): Throwable | Null = 78 | left match 79 | case l: Throwable => 80 | right match 81 | case r: Throwable => 82 | l.addSuppressed(r) 83 | l 84 | case null => l 85 | case null => right 86 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Runtime.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | extension [R, A](c: Control[R] ?=> A) 6 | 7 | def toEither: Either[R, A] = 8 | Continuation.fold(c)(Left(_), Right(_)) 9 | 10 | def toOption: Option[A] = 11 | Continuation.fold(c)(_ => None, Some(_)) 12 | 13 | def run: (R | A) = Continuation.fold(c)(identity, identity) 14 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Show.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * The capability to print a formatted version of some type. Modeling 5 | * 6 | * @tparam A 7 | * The type that needs printing 8 | */ 9 | opaque type Show[-A] = A => String 10 | 11 | /** 12 | * @tparam A 13 | * The type to print 14 | */ 15 | extension [A](f: Show[A]) 16 | /** 17 | * Prints a value of type A 18 | */ 19 | def show(a: A): String = f(a) 20 | 21 | object Show: 22 | /** 23 | * @constructor 24 | */ 25 | def apply[A](f: A => String): Show[A] = f 26 | 27 | /** 28 | * Default implementation is a simple to string 29 | */ 30 | given defaultShow[A]: Show[A] = a => a.toString 31 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/ShowImpl.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | /** 4 | * Gives the ability for overrideable external string formatting of any object. 5 | */ 6 | sealed trait ShowImpl[A] { 7 | extension (a: A) def show: String = a.toString 8 | } 9 | 10 | object ShowImpl: 11 | given defaultShowImpl[A]: ShowImpl[A] = 12 | new ShowImpl[A] {} 13 | 14 | def apply[A](formatter: A => String): ShowImpl[A] = 15 | new ShowImpl[A] { 16 | extension (a: A) override def show: String = formatter(a) 17 | } 18 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Streams.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import java.util.concurrent.Semaphore 4 | import scala.annotation.implicitNotFound 5 | import scala.collection.mutable.ListBuffer 6 | 7 | @implicitNotFound( 8 | "Receiving values from streams or channels require capability:\n% Receive[${A}]" 9 | ) 10 | trait Receive[+A]: 11 | def receive(f: A => Unit): Unit 12 | 13 | extension [A](r: Receive[Receive[A]]) 14 | def flatten: Receive[A] = 15 | streamed(r.receive(sendAll)) 16 | 17 | def flattenMerge( 18 | concurrency: Int 19 | ): Receive[A] = 20 | val semaphore = Semaphore(concurrency) 21 | streamed(r.receive { (inner: Receive[A]) => 22 | semaphore.acquire() 23 | uncancellable(() => { 24 | try sendAll(inner) 25 | finally semaphore.release() 26 | }) 27 | }) 28 | 29 | extension [A](r: Receive[A]) 30 | 31 | def transform[B](f: Send[B] ?=> (A => Unit)): Receive[B] = 32 | streamed(receive(f)(using r)) 33 | 34 | def filter(predicate: Send[A] ?=> A => Boolean): Receive[A] = 35 | transform { value => if (predicate(value)) send(value) } 36 | 37 | def map[B](f: A => B): Receive[B] = 38 | transform { v => send(f(v)) } 39 | 40 | def flatMap[B](transform: A => Receive[B]): Receive[B] = 41 | map(transform).flatten 42 | 43 | def flatMapMerge[B](concurrency: Int)( 44 | transform: A => Receive[B] 45 | )(using Structured): Receive[B] = 46 | map(transform).flattenMerge(concurrency) 47 | 48 | def zipWithIndex: Receive[(A, Int)] = 49 | var index = 0 50 | map { (value: A) => 51 | if (index < 0) throw ArithmeticException("Overflow") 52 | val v = (value, index) 53 | index = index + 1 54 | v 55 | } 56 | 57 | def grouped(n: Int): Receive[Vector[A]] = { // this could be written 58 | // as a tail rec function, 59 | // but it is unlikely the 60 | // streamed body will be 61 | // extracted and thus from 62 | // the outside is RT. 63 | streamed { 64 | var acc: Vector[A] = Vector.empty[A] 65 | r.receive((value: A) => 66 | if (acc.size < n) { 67 | acc = acc :+ value 68 | } else { 69 | send(acc) 70 | acc = Vector(value) 71 | }) 72 | send(acc) 73 | } 74 | } 75 | 76 | def fold[R](initial: R, operation: (R, A) => R): Receive[R] = 77 | streamed { 78 | var acc: R = initial 79 | r.receive { (value: A) => acc = operation(acc, value) } 80 | send(acc) 81 | } 82 | 83 | def toList: List[A] = 84 | val buffer = new ListBuffer[A] 85 | r.receive(buffer.addOne) 86 | buffer.toList 87 | 88 | def receive[A](f: A => Unit)(using r: Receive[A]): Unit = 89 | r.receive(f) 90 | 91 | @implicitNotFound( 92 | "Sending values to streams or channels require capability:\n% Send[${A}]" 93 | ) 94 | trait Send[A]: 95 | def send(value: A): Unit 96 | def sendAll(receive: Receive[A]): Unit = 97 | receive.receive(send) 98 | 99 | def send[A](value: A)(using s: Send[A]): Unit = 100 | s.send(value) 101 | 102 | def sendAll[A](receive: Receive[A])(using s: Send[A]): Unit = 103 | s.sendAll(receive) 104 | 105 | def streamed[A](f: Send[A] ?=> Unit): Receive[A] = 106 | (receive: (A) => Unit) => 107 | given Send[A] = (a: A) => receive(a) 108 | f 109 | 110 | def streamOf[A](values: A*): Receive[A] = 111 | streamed { 112 | for (value <- values) send(value) 113 | } 114 | 115 | private[this] def repeat(n: Int)(f: (Int) => Unit): Unit = 116 | for (i <- 0 to n) f(i) 117 | 118 | val source: Send[Int] ?=> Unit = 119 | repeat(100)(send) 120 | 121 | @main def SimpleFlow: Unit = 122 | 123 | val listed = streamed(source) 124 | .transform((n: Int) => send(n + 1)) 125 | .filter((n: Int) => n % 2 == 0) 126 | .map((n: Int) => n * 10) 127 | .zipWithIndex 128 | .toList 129 | 130 | println(listed) 131 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Structured.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import jdk.incubator.concurrent.StructuredTaskScope 4 | import jdk.incubator.concurrent.StructuredTaskScope.FactoryHolder 5 | 6 | import scala.annotation.implicitNotFound 7 | import java.util.concurrent.{ 8 | Callable, 9 | CancellationException, 10 | Executor, 11 | ExecutorService, 12 | Executors, 13 | Future, 14 | ThreadFactory 15 | } 16 | import java.util.concurrent.atomic.AtomicReference 17 | import scala.annotation.implicitNotFound 18 | 19 | @implicitNotFound( 20 | "Structured concurrency requires capability:\n% Structured" 21 | ) 22 | case class Structured( 23 | name: String, 24 | threadFactory: ThreadFactory, 25 | externalFibers: AtomicReference[List[Future[Any]]], 26 | scope: StructuredTaskScope[Any]) 27 | 28 | extension (s: Structured) 29 | private[unwrapped] def forked[A](callable: Callable[A]): Future[A] = 30 | s.scope.fork(callable) 31 | 32 | inline def structured[B](f: Structured ?=> B): B = 33 | val name = "structured" 34 | val threadFactory: ThreadFactory = Thread.ofVirtual().factory() 35 | val scope = new StructuredTaskScope[Any]() 36 | given Structured = Structured(name, threadFactory, AtomicReference(List.empty), scope) 37 | try f 38 | finally 39 | joinAll 40 | scope.close() 41 | 42 | def joinAll(using structured: Structured): Unit = 43 | structured.externalFibers.get().foreach { fiber => if (!fiber.isCancelled) fiber.join } 44 | structured.scope.join 45 | 46 | def track[A](fiber: Future[A])(using structured: Structured): Unit = 47 | structured.externalFibers.updateAndGet(_.appended(fiber.asInstanceOf[Future[Any]])) 48 | 49 | private[unwrapped] inline def callableOf[A](f: () => A): Callable[A] = 50 | () => f() 51 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Throws.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | // TODO this can `erased` with scala3 still experimental erased feature 6 | @implicitNotFound( 7 | "Missing capability:\n" + 8 | "* Throws[${R}]\n" + 9 | "alternatively you may resolve this call with \n" + 10 | "```scala\nhandle(f)(recover)\n```\n" + 11 | "or\n ignore thrown exceptions with import unwrapped.unsafe.unsafeExceptions`" 12 | ) 13 | opaque type Throws[-R <: Exception] = Unit 14 | 15 | object Throws: 16 | given unsafeExceptions[R <: Exception]: Throws[R] = () 17 | 18 | inline def handle[R <: Exception, A]( 19 | inline f: Throws[R] ?=> A 20 | )(inline recover: (R) => A): A = 21 | try 22 | import unwrapped.Throws.unsafeExceptions 23 | f 24 | catch 25 | case r: R => 26 | recover(r) 27 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/Use.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.util.control.NonFatal 4 | import java.util.concurrent.CancellationException 5 | import java.util.concurrent.ExecutionException 6 | 7 | inline def runReleaseAndRethrow( 8 | inline original: Throwable, 9 | inline f: () => Unit 10 | ): Nothing = 11 | try { 12 | uncancellable(f) 13 | } catch 14 | case NonFatal(e) => 15 | original.addSuppressed(e) 16 | throw original 17 | 18 | inline def guarantee[A]( 19 | inline fa: () => A, 20 | inline finalizer: () => Unit 21 | ): A = 22 | val res = 23 | try fa() 24 | catch 25 | case (e: CancellationException) => 26 | runReleaseAndRethrow(e, finalizer) 27 | case (e: ExecutionException) => 28 | runReleaseAndRethrow(e.getCause, finalizer) 29 | case NonFatal(t) => 30 | runReleaseAndRethrow(t, finalizer) 31 | structured(uncancellable(finalizer)) 32 | res 33 | 34 | inline def guaranteeCase[A]( 35 | inline fa: () => A, 36 | inline finalizer: (ExitCase) => Unit 37 | ): A = 38 | val res = 39 | try fa() 40 | catch 41 | case (e: CancellationException) => 42 | runReleaseAndRethrow(e, () => finalizer(ExitCase.Cancelled(e))) 43 | case (e: ExecutionException) => 44 | runReleaseAndRethrow( 45 | e.getCause, 46 | () => finalizer(ExitCase.Cancelled(e.getCause)) 47 | ) 48 | case NonFatal(t) => 49 | runReleaseAndRethrow(t, () => finalizer(ExitCase.Failure(t))) 50 | structured(uncancellable(() => finalizer(ExitCase.Completed))) 51 | res 52 | 53 | inline def bracket[A, B]( 54 | inline acquire: () => A, 55 | inline use: (A) => B, 56 | inline release: (A) => Unit 57 | ): B = 58 | val acquired = uncancellable(acquire) 59 | val res = 60 | try use(acquired) 61 | catch 62 | case (e: CancellationException) => 63 | runReleaseAndRethrow(e, () => release(acquired)) 64 | case (e: ExecutionException) => 65 | runReleaseAndRethrow(e.getCause, () => release(acquired)) 66 | case NonFatal(t) => 67 | runReleaseAndRethrow(t, () => release(acquired)) 68 | structured(uncancellable(() => release(acquired))) 69 | res 70 | 71 | inline def bracketCase[A, B]( 72 | inline acquire: () => A, 73 | inline use: (A) => B, 74 | inline release: (A, ExitCase) => Unit 75 | ): B = 76 | val acquired = uncancellable(acquire) 77 | val res = 78 | try use(acquired) 79 | catch 80 | case (e: CancellationException) => 81 | runReleaseAndRethrow(e, () => release(acquired, ExitCase.Cancelled(e))) 82 | case (e: ExecutionException) => 83 | runReleaseAndRethrow( 84 | e.getCause, 85 | () => release(acquired, ExitCase.Cancelled(e.getCause)) 86 | ) 87 | case NonFatal(t) => 88 | runReleaseAndRethrow(t, () => release(acquired, ExitCase.Failure(t))) 89 | uncancellable(() => release(acquired, ExitCase.Completed)) 90 | res 91 | -------------------------------------------------------------------------------- /unwrapped/src/main/scala/unwrapped/requires.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import scala.compiletime.error 4 | import scala.quoted.* 5 | 6 | /** 7 | * Super-simple compile time requirements. 8 | * 9 | * If the predicate fails, the compile time error message will print as an error. 10 | * 11 | * ### Usage 12 | * {{{ 13 | * import unwrapped.requires 14 | * opaque type MyInt = Int 15 | * object MyInt: 16 | * def apply(x: Int): MyInt = 17 | * require(x < 5, "MyInt must be less than 5") 18 | * 5 19 | * MyInt(3) // compiles 20 | * MyInt(7) // fails compilation with "MyInt must be less than 5" 21 | * }}} 22 | */ 23 | inline def requires[A](assertion: Boolean, errorMessage: String, value: A): A = { 24 | ${ requiresImpl('assertion, 'errorMessage, 'value) } 25 | } 26 | 27 | private[unwrapped] def requiresImpl[A]( 28 | assertionExpression: Expr[Boolean], 29 | errorMessageExpression: Expr[String], 30 | value: Expr[A])(using Quotes)(using Type[A]): Expr[A] = 31 | '{ 32 | if $assertionExpression == false then error($errorMessageExpression) else $value 33 | } 34 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/BindTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Properties 4 | import org.scalacheck.Prop.forAll 5 | 6 | object BindTests extends Properties("Bind Tests"): 7 | property("Binding two values of the same type") = forAll { (a: Int, b: Int) => 8 | val effect: Int = Right(a).bind + Right(b).bind 9 | run(effect) == a + b 10 | } 11 | 12 | property("Binding two values of different types") = forAll { (a: Int, b: Int) => 13 | val effect: Int = Right(a).bind + Some(b).bind 14 | run(effect) == a + b 15 | } 16 | 17 | property("Short-circuiting with Either.Left") = forAll { (n: Int, s: String) => 18 | val effect: Control[String | None.type] ?=> Int = 19 | Left[String, Int](s).bind + Option(n).bind 20 | run(effect) == s 21 | } 22 | 23 | property("Short-circuiting with Option.None") = forAll { (n: Int) => 24 | val effect: Control[None.type] ?=> Int = 25 | Right(n).bind + Option.empty[Int].bind 26 | run(effect) == None 27 | } 28 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/ControlTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Properties 4 | import org.scalacheck.Prop.forAll 5 | 6 | object ControlTests extends Properties("Control Tests"): 7 | 8 | property("short-circuit from nested control") = forAll { (s: String) => 9 | def outer: Control[String] ?=> Int = 1 10 | def inner: Control[String] ?=> Int = 11 | s.shift[Int] + outer + 1 12 | run(inner) == s 13 | } 14 | 15 | property("happy path") = forAll { (n: Int) => 16 | def effect: Control[Nothing] ?=> Int = n 17 | run(effect) == n 18 | } 19 | 20 | property("shift short-circuits") = forAll { (s: String) => 21 | def effect: Control[String] ?=> Int = s.shift 22 | run(effect) == s 23 | } 24 | 25 | end ControlTests 26 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/NullableTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Arbitrary 4 | import org.scalacheck.Gen 5 | import org.scalacheck.Prop.forAll 6 | import org.scalacheck.Prop.propBoolean 7 | import org.scalacheck.Properties 8 | 9 | object NullableTests extends Properties("Nullable Tests"): 10 | given nullConst: Arbitrary[Null] = Arbitrary { Gen.const[Null](null) } 11 | given nullableInt: Arbitrary[Nullable[Int]] = Arbitrary { 12 | Gen.oneOf( 13 | Gen.choose(Int.MinValue, Int.MaxValue).map(Nullable.apply), 14 | nullConst.arbitrary.map(Nullable.apply[Int])) 15 | } 16 | property("Nullable[Int]#value when the value is not null should be the given value") = 17 | forAll { (a: Int) => 18 | val x: Nullable[Int] = Nullable(a) 19 | run(x.value) == a 20 | } 21 | property("Nullable[Int]#value when the value is null should be a null pointer exception") = 22 | forAll { (a: Null) => 23 | val x: Nullable[Int] = Nullable(a) 24 | val result = run(x.value) 25 | result match 26 | case z: Int => false 27 | case _ => true 28 | } 29 | property("Nullable[A]#v is an alias for Nullable[A]#value") = forAll { (a: Int) => 30 | val x = Nullable(a) 31 | 32 | a == run(x.v) && run(x.value) == run(x.v) 33 | } 34 | 35 | property("Nullable[A]#exists is true for non null values") = forAll { (a: Int) => 36 | run(Nullable(a).exists) 37 | } 38 | property("Nullable[Int]#exists is false for null values") = forAll { (a: Null) => 39 | !run(Nullable[Int](a).exists) 40 | } 41 | property("Nullable[A]#nonEmpty should be an alias for Nullable[A]#exists") = forAll { 42 | (a: Int) => 43 | val x = Nullable(a) 44 | run(x.nonEmpty) && run(x.exists) == run(x.nonEmpty) 45 | } 46 | property( 47 | "Nullable[A]#getOrElse(b:A) should return the contained value when the value is non-null") = 48 | forAll { (a: Nullable[Int], b: Int) => (a.nonEmpty) ==> (a.getOrElse(b) == run(a.v)) } 49 | property( 50 | "Nullable#getOrElse(b: A) should return the default value when !Nullable[A]#exists") = 51 | forAll { (a: Nullable[Int], b: Int) => (!a.exists) ==> (a.getOrElse(b) == b) } 52 | property( 53 | "Nullable[A]#orElse(b: Nullable[A]) should return the original nullable when Nullable[A]#nonEmpty") = 54 | forAll { (a: Nullable[Int], b: Nullable[Int]) => (a.nonEmpty) ==> (a.orElse(b) == a) } 55 | property( 56 | "Nullable[A]#orElse(b: Nullable[A]) should return the passed b value when !Nullable[A].exists") = 57 | forAll { (a: Nullable[Int], b: Nullable[Int]) => 58 | (!a.exists && b.nonEmpty) ==> (a.orElse(b) == b) 59 | } 60 | property( 61 | "Nullable[A]#map(f: A => B) should return the application of f to the contained value when Nullable[A].nonEmpty") = 62 | forAll { (a: Nullable[Int], f: Int => String) => 63 | (a.nonEmpty) ==> (run(a.map(f).v) == f(run(a.v).asInstanceOf[Int])) 64 | } 65 | property( 66 | "Nullable[A]#map(f: A => B) should return a non-exstent nullable when !Nullable[A].exists") = 67 | forAll { (a: Nullable[Int], f: Int => String) => (!a.exists) ==> (!a.map(f).exists) } 68 | property( 69 | "Nullable[A].bind(f: A => Nullable[B]) should return the result of applying f to the contained value when Nullable[A]#.nonEmpty") = 70 | forAll { (a: Nullable[Int]) => 71 | val f = (x: Int) => Nullable(x.toString) 72 | (a.nonEmpty) ==> { 73 | val x = run(a.value) 74 | a.bind(f) == f(run(x.asInstanceOf[Int])) 75 | } 76 | } 77 | property("Nullable#none[A] should return a Nullable[A] that does not exist") = forAll { 78 | (_: Int) => !Nullable.none[Int].exists 79 | } 80 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/RequiresTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Prop._ 4 | import org.scalacheck.Properties 5 | 6 | import scala.compiletime.testing.typeCheckErrors 7 | import scala.compiletime.testing.typeChecks 8 | 9 | object RequiresTests extends Properties("requires compile-time assertions"): 10 | 11 | property("requires detects false assertions at compile time") = forAll { 12 | (assertion: Boolean) => 13 | assertion ==> { 14 | val x = typeChecks(""" 15 | import unwrapped.* 16 | 17 | val x: Boolean = requires(true, "Expected true, was false.", true)""") 18 | x 19 | } 20 | (!assertion) ==> { 21 | val x = typeCheckErrors(""" 22 | import unwrapped.* 23 | 24 | val x: Boolean = requires(false, "Some Error Message.", false)""") 25 | x.head.message == "Some Error Message." 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/ResourcesTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Properties 4 | import org.scalacheck.Prop.forAll 5 | import java.util.concurrent.CompletableFuture 6 | import scala.util.control.NonFatal 7 | import java.util.concurrent.CompletionException 8 | import java.util.concurrent.ExecutionException 9 | 10 | object ResourcesTests extends Properties("Resources Tests"): 11 | 12 | property("Can consume resource") = forAll { (n: Int) => 13 | val r = Resource(n, (_, _) => ()) 14 | r.use(_ + 1) == n + 1 15 | } 16 | 17 | property("value resource is released with Complete") = forAll { (n: Int) => 18 | val p = CompletableFuture[ExitCase]() 19 | val r = Resource(n, (_, ex) => require(p.complete(ex))) 20 | r.use(_ => ()) 21 | p.join == ExitCase.Completed 22 | } 23 | 24 | case class CustomEx(val token: String) extends RuntimeException 25 | 26 | property("error resource finishes with error") = forAll { (n: String) => 27 | val p = CompletableFuture[ExitCase]() 28 | val r = Resource[Int](throw CustomEx(n), (_, ex) => require(p.complete(ex))) 29 | val result = 30 | try 31 | r.use(_ + 1) 32 | "unexpected" 33 | catch case e: CompletionException => e.getCause.asInstanceOf[CustomEx].token 34 | result == n 35 | } 36 | 37 | end ResourcesTests 38 | 39 | /* 40 | val p = CompletableDeferred() 41 | val r = Resource({ throw e }, { _, ex -> require(p.complete(ex)) }) 42 | 43 | Either.catch { 44 | r.use { it + 1 } 45 | } should leftException(e) 46 | */ 47 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/RuntimeTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Properties 4 | import org.scalacheck.Prop.forAll 5 | 6 | object RuntimeTests extends Properties("Runtime Tests"): 7 | property("Either binding toOption") = forAll { (a: Int, b: Int) => 8 | val effect: Int = Right(a).bind + Right(b).bind 9 | toOption(effect).contains(a + b) 10 | } 11 | 12 | property("Option binding toEither") = forAll { (a: Int, b: Int) => 13 | val effect: Int = Some(a).bind + Some(b).bind 14 | toEither(effect) == Right(a + b) 15 | } 16 | 17 | property("Binding two values of different types toEither") = forAll { (a: Int, b: Int) => 18 | val effect: Int = Right(a).bind + Some(b).bind 19 | toEither(effect) == Right(a + b) 20 | } 21 | 22 | property("Short-circuiting with Either.Left toEither") = forAll { (n: Int, s: String) => 23 | val effect: Control[String | None.type] ?=> Int = 24 | Left[String, Int](s).bind + Some(n).bind 25 | toEither(effect) == Left[String, Int](s) 26 | } 27 | 28 | property("Short-circuiting with Option.None toOption") = forAll { (n: Int) => 29 | val effect: Control[None.type] ?=> Int = 30 | Right(n).bind + Option.empty[Int].bind 31 | toOption(effect).isEmpty 32 | } 33 | 34 | end RuntimeTests 35 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/StreamsTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Properties 4 | import org.scalacheck.Prop._ 5 | 6 | object StreamsTests extends Properties("Streams tests"): 7 | 8 | property("send/receive") = forAll { (n: Int) => streamOf(n).toList == List(n) } 9 | 10 | property("toList") = forAll { (list: List[Int]) => streamOf(list*).toList == list } 11 | 12 | property("zipWithIndex") = forAll { (list: List[Int]) => 13 | streamOf(list*).zipWithIndex.toList == list.zipWithIndex 14 | } 15 | 16 | property("streamOf(list).grouped.toList.flatten should result in the original list") = 17 | forAll { (v: List[Int], size: Int) => 18 | (size > 0) ==> { 19 | val list = streamOf(v*).grouped(size).toList.flatten 20 | list == v 21 | } 22 | } 23 | 24 | property( 25 | "streamOf(list).grouped.toList should result in lists of size size, except for the final item, which contains all remaining items") = 26 | forAll { (v: List[Long]) => 27 | (v.size > 10) ==> { 28 | val list = streamOf(v*).grouped(10).toList.map(_.size == 10) 29 | list.headOption.exists(identity) && list.lastOption.nonEmpty 30 | } 31 | 32 | } 33 | 34 | property("flatten: identity") = forAll { (n: Int) => 35 | streamOf(n).map(i => streamOf(i)).flatten.toList == List(n) 36 | } 37 | 38 | property("flatten") = forAll { (list: List[Int]) => 39 | streamOf(list*).map(i => streamOf(i, i + 1)).flatten.toList == list.flatMap(i => 40 | List(i, i + 1)) 41 | } 42 | 43 | property("fold") = forAll { (initial: Int, list: List[Int], operation: (Int, Int) => Int) => 44 | streamOf(list*).fold(initial, operation).toList == List(list.fold(initial)(operation)) 45 | } 46 | 47 | property("map") = forAll { (n: Int) => streamOf(n).map(_ + 1).toList == List(n + 1) } 48 | 49 | property("flatMap") = forAll { (n: Int) => 50 | streamOf(n).flatMap(n => streamOf(n + 1)).toList == List(n + 1) 51 | } 52 | 53 | property("comprehensions") = forAll { (n: Int) => 54 | val r = for { 55 | a <- streamOf(n) 56 | b <- streamOf(n) 57 | } yield a + b 58 | r.toList == List(n + n) 59 | } 60 | 61 | end StreamsTests 62 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/StructuredTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Prop.forAll 4 | import org.scalacheck.Properties 5 | import org.scalacheck.Test.Parameters 6 | 7 | import java.time.Duration 8 | import java.util.concurrent.CompletableFuture 9 | import java.util.concurrent.Semaphore 10 | import java.util.concurrent.atomic.AtomicReference 11 | import scala.util.control.NonFatal 12 | 13 | object StructuredTests extends Properties("Structured Concurrency Tests"): 14 | 15 | override def overrideParameters(p: Parameters) = 16 | p.withMinSuccessfulTests(10000) 17 | 18 | property("parallel runs in parallel") = forAll { (a: Int, b: Int) => 19 | val r = AtomicReference("") 20 | val modifyGate = CompletableFuture[Int]() 21 | structured( 22 | parallel( 23 | () => 24 | modifyGate.join 25 | r.updateAndGet { i => s"$i$a" } 26 | , 27 | () => 28 | r.set(s"$b") 29 | modifyGate.complete(0) 30 | ) 31 | ) 32 | r.get() == s"$b$a" 33 | } 34 | 35 | property("concurrent shift on fork join propagates") = forAll { (a: Int, b: Int) => 36 | val x: Control[Int] ?=> Structured ?=> String = 37 | val fa = fork[String](() => a.shift) 38 | val fb = fork[String](() => b.shift) 39 | fa.join + fb.join 40 | 41 | val value: String | Int = run(structured(x)) 42 | 43 | List(a, b).contains(value) 44 | } 45 | 46 | property("concurrent shift on fork that doesn't join does not propagate") = forAll { 47 | (a: Int, b: Int, c: String) => 48 | val x: Control[Int] ?=> Structured ?=> String = 49 | val fa = fork[Nothing](() => a.shift) 50 | val fb = fork[Nothing](() => b.shift) 51 | c 52 | 53 | c == run(structured(x)) 54 | } 55 | 56 | end StructuredTests 57 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/UseTests.scala: -------------------------------------------------------------------------------- 1 | package unwrapped 2 | 3 | import org.scalacheck.Properties 4 | import org.scalacheck.Prop.forAll 5 | import java.util.concurrent.atomic.AtomicReference 6 | import java.util.concurrent.CompletableFuture 7 | import java.util.concurrent.Future 8 | import org.scalacheck.Test.Parameters 9 | import java.util.concurrent.Semaphore 10 | import java.util.concurrent.CancellationException 11 | import scala.util.control.NonFatal 12 | import java.time.Duration 13 | import java.time.temporal.TemporalUnit 14 | 15 | object UseTests extends Properties("Use Tests"): 16 | 17 | property("finalizer is invoked on success") = forAll { (i: Int, s: String) => 18 | val p = CompletableFuture[String]() 19 | val res = guarantee( 20 | fa = () => i, 21 | finalizer = () => p.complete(s) 22 | ) 23 | p.join == s && res == i 24 | } 25 | 26 | property("finalizer is invoked on exception") = forAll { (i: Int, s: String) => 27 | val p = CompletableFuture[String]() 28 | val res = 29 | try 30 | guarantee( 31 | fa = () => throw RuntimeException("boom"), 32 | finalizer = () => p.complete(s) 33 | ) 34 | catch case e: RuntimeException => i 35 | p.join == s && res == i 36 | } 37 | 38 | end UseTests 39 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Bind.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import java.util.concurrent.{Callable, Executors, TimeUnit} 4 | import scala.concurrent.duration.Duration 5 | import scala.concurrent.{Await, Future} 6 | 7 | extension [A](f: Fiber[A])(using Structured) 8 | def bind: A = 9 | f.join // or Await.result(f, scala.concurrent.duration.Duration.Inf) 10 | // otherwise we could have written something like 11 | // continuation[A] { cont: Continuation[A] => { 12 | // f.whenComplete { (result, exception) => 13 | // if (exception == null) // the future has been completed normally 14 | // cont.resume(result) 15 | // else // the future has completed with an exception 16 | // cont.resumeWithException(exception) 17 | // } 18 | // } 19 | // } 20 | def apply(): A = bind 21 | 22 | extension [R, A](fa: Either[R, A])(using Control[R]) 23 | def bind: A = fa.fold(_.shift, identity) 24 | def apply(): A = bind 25 | 26 | extension [A](fa: Option[A])(using Control[None.type]) 27 | def bind: A = fa.fold(None.shift)(identity) 28 | def apply(): A = bind 29 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Continuation.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import java.util.UUID 4 | import java.util.concurrent.ExecutionException 5 | import scala.annotation.tailrec 6 | import scala.util.control.ControlThrowable 7 | 8 | object Continuation: 9 | inline def fold[R, A, B]( 10 | inline program: Control[R] ?=> A 11 | )(inline recover: R => B, inline transform: A => B): B = { 12 | var result: Any | Null = null 13 | implicit val control = new Control[R] { 14 | val token: String = UUID.randomUUID.toString 15 | 16 | extension (r: R) 17 | def shift[A]: A = 18 | throw ControlToken(token, r, recover.asInstanceOf[Any => Any]) 19 | } 20 | try { 21 | result = transform(program(using control)) 22 | } catch { 23 | case e: Throwable => 24 | result = handleControl(control, e) 25 | } 26 | result.asInstanceOf[B] 27 | } 28 | 29 | @tailrec def handleControl(control: Control[_], e: Throwable): Any = 30 | e match 31 | case e: ExecutionException => 32 | handleControl(control, e.getCause) 33 | case e @ ControlToken(token, shifted, recover) => 34 | if (control.token == token) 35 | recover(shifted) 36 | else 37 | throw e 38 | case _ => throw e 39 | 40 | private case class ControlToken( 41 | token: String, 42 | shifted: Any, 43 | recover: (Any) => Any 44 | ) extends ControlThrowable 45 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Control.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | /** 6 | * [[Control]] describes the ability to short-circuit an abilities function with a value of 7 | * [[R]] 8 | */ 9 | @implicitNotFound( 10 | "this function may shift control to ${R} and requires:\n Control[${R}]" 11 | ) 12 | trait Control[-R]: 13 | val token: String 14 | 15 | /** 16 | * Short-circuits the computation of [[A]] with a value of [[R]] 17 | */ 18 | extension (r: R) def shift[A]: A 19 | 20 | /** 21 | * Terminal operators for programs that require control 22 | */ 23 | extension [R, A](c: Control[R] ?=> A) 24 | 25 | def toEither: Either[R, A] = 26 | Continuation.fold(c)(Left(_), Right(_)) 27 | 28 | def toOption: Option[A] = 29 | Continuation.fold(c)(_ => None, Some(_)) 30 | 31 | def run: (R | A) = Continuation.fold(c)(identity, identity) 32 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Fiber.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import java.util.concurrent.{CompletableFuture, Future} 4 | 5 | opaque type Fiber[A] = Future[A] 6 | 7 | extension [A](fiber: Fiber[A]) 8 | def join: A = fiber.get 9 | def cancel(mayInterrupt: Boolean = true): Boolean = 10 | fiber.cancel(mayInterrupt) 11 | 12 | def fork[B](f: () => B)(using structured: Structured): Fiber[B] = 13 | structured.forked(callableOf(f)) 14 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Model.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import scala.concurrent.Future 4 | 5 | object NotFound 6 | 7 | case class Country(code: Option[String]) 8 | 9 | case class Address(country: Option[Country]) 10 | 11 | case class Person(name: String, address: Either[NotFound.type, Address]) 12 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Program.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import scala.concurrent.Future 4 | import concurrent.ExecutionContext.Implicits.global 5 | 6 | val effects: Structured ?=> Control[NotFound.type | None.type] ?=> String = 7 | val jane = fork(() => Person("Jane", Right(Address(Some(Country(Some("ES"))))))) 8 | val joe = fork(() => Person("Joe", Left(NotFound))) 9 | val janeEffect = getCountryCodeDirect(jane) 10 | val joeEffect = getCountryCodeDirect(joe) 11 | run(s"$janeEffect, $joeEffect") 12 | 13 | @main def program = 14 | println(run(structured(effects))) // NotFound 15 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/Structured.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import jdk.incubator.concurrent.StructuredTaskScope 4 | 5 | import java.util.concurrent.* 6 | import scala.annotation.implicitNotFound 7 | import scala.jdk.FutureConverters.* 8 | 9 | @implicitNotFound( 10 | "Structured concurrency requires capability:\n% Structured" 11 | ) 12 | opaque type Structured = StructuredTaskScope[Any] 13 | 14 | extension (s: Structured) 15 | private[unwrapped] def forked[A](callable: Callable[A]): Future[A] = 16 | s.fork(callable) 17 | 18 | inline def structured[B](f: Structured ?=> B): B = 19 | val scope = new StructuredTaskScope[Any]() 20 | given Structured = scope 21 | try f 22 | finally 23 | scope.join 24 | scope.close() 25 | 26 | def joinAll(using structured: Structured): Unit = 27 | structured.join 28 | 29 | private[unwrapped] inline def callableOf[A](f: () => A): Callable[A] = 30 | new Callable[A] { def call(): A = f() } 31 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/StyleDirect.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import scala.concurrent.Future 4 | 5 | def getCountryCodeDirect( 6 | futurePerson: Fiber[Person])(using Structured, Control[NotFound.type | None.type]): String = 7 | val person = futurePerson.bind 8 | val address = person.address.bind 9 | val country = address.country.bind 10 | country.code.bind 11 | 12 | // or if bind is defined as apply()... 13 | 14 | def getCountryDirect2( 15 | futurePerson: Fiber[Person])(using Structured, Control[NotFound.type | None.type]): String = 16 | futurePerson.bind.address().country().code() 17 | -------------------------------------------------------------------------------- /unwrapped/src/test/scala/unwrapped/sip/StyleIndirect.scala: -------------------------------------------------------------------------------- 1 | package unwrapped.sip 2 | 3 | import scala.concurrent.Future 4 | import concurrent.ExecutionContext.Implicits.global 5 | 6 | def getCountryCodeIndirect(futurePerson: Future[Person]): Future[Option[String]] = 7 | futurePerson.map { person => 8 | person.address match 9 | case Right(address) => 10 | address.country.flatMap(_.code) 11 | case Left(_) => 12 | None 13 | } 14 | --------------------------------------------------------------------------------