├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .jvmopts ├── .scalafmt.conf ├── LICENSE ├── NOTICE ├── README.md ├── akka-http-argonaut └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpargonaut │ │ └── ArgonautSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpargonaut │ ├── ArgonautSupportSpec.scala │ └── ExampleApp.scala ├── akka-http-avro4s └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpavro4s │ │ └── AvroSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpavro4s │ ├── AvroSupportSpec.scala │ └── ExampleApp.scala ├── akka-http-circe └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpcirce │ │ └── CirceSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpcirce │ ├── CirceSupportSpec.scala │ └── ExampleApp.scala ├── akka-http-jackson └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpjackson │ │ └── JacksonSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpjackson │ ├── ExampleApp.scala │ └── JacksonSupportSpec.scala ├── akka-http-json4s └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpjson4s │ │ └── Json4sSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpjson4s │ ├── ExampleApp.scala │ └── Json4sSupportSpec.scala ├── akka-http-jsoniter-scala └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpjsoniterscala │ │ └── JsoniterScalaSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpjsoniterscala │ ├── ExampleApp.scala │ └── JsoniterScalaSupportSpec.scala ├── akka-http-ninny └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpninny │ │ └── NinnySupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpninny │ ├── ExampleApp.scala │ └── NinnySupportSpec.scala ├── akka-http-play-json └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpplayjson │ │ └── PlayJsonSupport.scala │ └── test │ ├── resources │ └── application.conf │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpplayjson │ ├── ExampleApp.scala │ └── PlayJsonSupportSpec.scala ├── akka-http-upickle └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpupickle │ │ ├── UpickleCustomizationSupport.scala │ │ └── UpickleSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpupickle │ ├── ExampleApp.scala │ ├── UpickleCustomizationSupportSpec.scala │ └── UpickleSupportSpec.scala ├── akka-http-zio-json └── src │ ├── main │ └── scala │ │ └── de │ │ └── heikoseeberger │ │ └── akkahttpziojson │ │ └── ZioJsonSupport.scala │ └── test │ └── scala │ └── de │ └── heikoseeberger │ └── akkahttpziojson │ ├── ExampleApp.scala │ └── ZioJsonSupportSpec.scala ├── build.sbt └── project ├── build.properties └── plugins.sbt /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: ["*"] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: olafurpg/setup-scala@v13 15 | - run: sbt ci-release 16 | env: 17 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 18 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 19 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 20 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up JDK 11 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 11 19 | - name: Cache sbt 20 | uses: actions/cache@v2 21 | with: 22 | path: | 23 | ~/.ivy2/cache 24 | ~/.sbt 25 | key: ${{ runner.os }}-sbt-${{ hashFiles('build.sbt', 'project/plugins.sbt') }} 26 | - name: Run tests 27 | run: sbt +test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # sbt 2 | lib_managed 3 | project/project 4 | target 5 | .bsp 6 | 7 | # Worksheets (Eclipse or IntelliJ) 8 | *.sc 9 | 10 | # Eclipse 11 | .cache* 12 | .classpath 13 | .project 14 | .scala_dependencies 15 | .settings 16 | .target 17 | .worksheet 18 | 19 | # IntelliJ 20 | .idea 21 | 22 | # ENSIME 23 | .ensime 24 | .ensime_cache 25 | .ensime_lucene 26 | 27 | # Mac 28 | .DS_Store 29 | 30 | # Akka Persistence 31 | journal 32 | snapshots 33 | 34 | # Log files 35 | *.log 36 | 37 | # jenv 38 | .java-version 39 | 40 | # VSCode Metals 41 | metals.sbt 42 | .bloop 43 | .metals 44 | .vscode -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Xmx2g 2 | -XX:MaxMetaspaceSize=1g 3 | -XX:ReservedCodeCacheSize=256m 4 | -XX:+UseParallelGC 5 | -Dfile.encoding=UTF8 6 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.4.3" 2 | 3 | preset = "defaultWithAlign" 4 | runner.dialect = "scala213" 5 | 6 | maxColumn = 100 7 | indentOperator.preset = "spray" 8 | indentOperator.exemptScope = "all" 9 | spaces.inImportCurlyBraces = true 10 | rewrite.rules = ["AsciiSortImports", "RedundantBraces", "RedundantParens"] 11 | docstrings.blankFirstLine = true 12 | trailingCommas = "preserve" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Heiko Seeberger 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # akka-http-json # 2 | 3 | Attention: This project is no longer actively maintained. Feel free to fork it or use [Pekko](https://pekko.apache.org/docs/pekko-http/current/common/json-support.html). 4 | 5 | [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 6 | [![Maven Central](https://img.shields.io/maven-central/v/de.heikoseeberger/akka-http-circe_2.13)](https://search.maven.org/artifact/de.heikoseeberger/akka-http-circe_2.13) 7 | [![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) 8 | 9 | akka-http-json provides JSON (un)marshalling support for [Akka HTTP](https://github.com/akka/akka-http) via the following JSON libraries: 10 | - [Argonaut](http://argonaut.io) 11 | - [avro4s](https://github.com/sksamuel/avro4s) 12 | - [AVSystem GenCodec](https://github.com/AVSystem/scala-commons/blob/master/docs/GenCodec.md) 13 | - [circe](https://circe.github.io/circe/) 14 | - [Jackson](https://github.com/FasterXML/jackson) via [Scala Module](https://github.com/FasterXML/jackson-module-scala) by default 15 | - [Json4s](https://github.com/json4s/json4s) 16 | - [jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) 17 | - [ninny](https://nrktkt.github.io/ninny-json/USERGUIDE) 18 | - [Play JSON](https://www.playframework.com/documentation/2.6.x/ScalaJson) 19 | - [uPickle](https://github.com/lihaoyi/upickle-pprint) 20 | - [zio-json](https://github.com/zio/zio-json) 21 | 22 | ## Installation 23 | 24 | The artifacts are published to Maven Central. 25 | 26 | ``` scala 27 | libraryDependencies ++= Seq( 28 | "de.heikoseeberger" %% "akka-http-circe" % AkkaHttpJsonVersion, 29 | ... 30 | ) 31 | ``` 32 | 33 | ## Usage 34 | 35 | Use respective support trait or object, e.g. `ArgonautSupport`, `FailFastCirceSupport`, etc. into your Akka HTTP code which is supposed to (un)marshal from/to JSON. Don't forget to provide the type class instances for the respective JSON libraries, if needed. 36 | 37 | ## Contribution policy ## 38 | 39 | Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so. 40 | 41 | ## License ## 42 | 43 | This code is open source software licensed under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.html). 44 | 45 | [![YourKit](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com) 46 | 47 | YourKit supports open source projects with its full-featured Java Profiler. YourKit, LLC is the creator of [YourKit Java Profiler](https://www.yourkit.com/java/profiler). 48 | -------------------------------------------------------------------------------- /akka-http-argonaut/src/main/scala/de/heikoseeberger/akkahttpargonaut/ArgonautSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpargonaut 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } 23 | import akka.http.scaladsl.model.MediaTypes.`application/json` 24 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 25 | import akka.http.scaladsl.util.FastFuture 26 | import akka.stream.scaladsl.{ Flow, Source } 27 | import akka.util.ByteString 28 | import argonaut.{ DecodeJson, EncodeJson, Json, Parse, PrettyParams } 29 | import scala.collection.immutable.Seq 30 | import scala.concurrent.Future 31 | import scala.util.control.NonFatal 32 | 33 | /** 34 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope *Argonaut* protocol. 35 | * 36 | * To use automatic codec derivation, user needs to import `argonaut.Shapeless._`. 37 | */ 38 | object ArgonautSupport extends ArgonautSupport 39 | 40 | /** 41 | * JSON marshalling/unmarshalling using an in-scope *Argonaut* protocol. 42 | * 43 | * To use automatic codec derivation, user needs to import `argonaut.Shapeless._` 44 | */ 45 | trait ArgonautSupport { 46 | type SourceOf[A] = Source[A, _] 47 | 48 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 49 | mediaTypes.map(ContentTypeRange.apply) 50 | 51 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 52 | List(`application/json`) 53 | 54 | private val jsonStringUnmarshaller = 55 | Unmarshaller.byteStringUnmarshaller 56 | .forContentTypes(unmarshallerContentTypes: _*) 57 | .mapWithCharset { 58 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 59 | case (data, charset) => data.decodeString(charset.nioCharset.name) 60 | } 61 | 62 | private val jsonStringMarshaller = 63 | Marshaller.oneOf(mediaTypes: _*)(Marshaller.stringMarshaller) 64 | 65 | private def sourceByteStringMarshaller( 66 | mediaType: MediaType.WithFixedCharset 67 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 68 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 69 | try 70 | FastFuture.successful { 71 | Marshalling.WithFixedContentType( 72 | mediaType, 73 | () => HttpEntity(contentType = mediaType, data = value) 74 | ) :: Nil 75 | } 76 | catch { 77 | case NonFatal(e) => FastFuture.failed(e) 78 | } 79 | } 80 | 81 | private val jsonSourceStringMarshaller = 82 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 83 | 84 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 85 | e: EncodeJson[A], 86 | support: JsonEntityStreamingSupport 87 | ): SourceOf[ByteString] = 88 | entitySource 89 | .map(e.apply) 90 | .map(PrettyParams.nospace.pretty) 91 | .map(ByteString(_)) 92 | .via(support.framingRenderer) 93 | 94 | private def parse(s: String) = 95 | Parse.parse(s) match { 96 | case Right(json) => json 97 | case Left(message) => sys.error(message) 98 | } 99 | 100 | private def decode[A: DecodeJson](json: Json) = 101 | DecodeJson.of[A].decodeJson(json).result match { 102 | case Right(entity) => entity 103 | case Left((m, h)) => sys.error(m + " - " + h) 104 | } 105 | 106 | /** 107 | * HTTP entity => `A` 108 | * 109 | * @tparam A 110 | * type to decode 111 | * @return 112 | * unmarshaller for `A` 113 | */ 114 | implicit def unmarshaller[A: DecodeJson]: FromEntityUnmarshaller[A] = 115 | jsonStringUnmarshaller.map(parse).map(decode[A]) 116 | 117 | /** 118 | * `A` => HTTP entity 119 | * 120 | * @tparam A 121 | * type to encode 122 | * @return 123 | * marshaller for any `A` value 124 | */ 125 | implicit def marshaller[A: EncodeJson]: ToEntityMarshaller[A] = 126 | jsonStringMarshaller 127 | .compose(PrettyParams.nospace.pretty) 128 | .compose(EncodeJson.of[A].apply) 129 | 130 | /** 131 | * `ByteString` => `A` 132 | * 133 | * @tparam A 134 | * type to decode 135 | * @return 136 | * unmarshaller for any `A` value 137 | */ 138 | implicit def fromByteStringUnmarshaller[A: DecodeJson]: Unmarshaller[ByteString, A] = 139 | Unmarshaller(_ => bs => Future.successful(decode(parse(bs.utf8String)))) 140 | 141 | /** 142 | * HTTP entity => `Source[A, _]` 143 | * 144 | * @tparam A 145 | * type to decode 146 | * @return 147 | * unmarshaller for `Source[A, _]` 148 | */ 149 | implicit def sourceUnmarshaller[A: DecodeJson](implicit 150 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 151 | ): FromEntityUnmarshaller[SourceOf[A]] = 152 | Unmarshaller 153 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 154 | def asyncParse(bs: ByteString) = 155 | Unmarshal(bs).to[A] 156 | 157 | def ordered = 158 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 159 | 160 | def unordered = 161 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 162 | 163 | Future.successful { 164 | entity.dataBytes 165 | .via(support.framingDecoder) 166 | .via(if (support.unordered) unordered else ordered) 167 | } 168 | } 169 | .forContentTypes(unmarshallerContentTypes: _*) 170 | 171 | /** 172 | * `SourceOf[A]` => HTTP entity 173 | * 174 | * @tparam A 175 | * type to encode 176 | * @return 177 | * marshaller for any `SourceOf[A]` value 178 | */ 179 | implicit def sourceMarshaller[A](implicit 180 | e: EncodeJson[A], 181 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 182 | ): ToEntityMarshaller[SourceOf[A]] = 183 | jsonSourceStringMarshaller.compose(jsonSource[A]) 184 | } 185 | -------------------------------------------------------------------------------- /akka-http-argonaut/src/test/scala/de/heikoseeberger/akkahttpargonaut/ArgonautSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpargonaut 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import argonaut.Argonaut._ 27 | import argonaut.CodecJson 28 | import org.scalatest.BeforeAndAfterAll 29 | import org.scalatest.matchers.should.Matchers 30 | import org.scalatest.wordspec.AsyncWordSpec 31 | 32 | import scala.concurrent.Await 33 | import scala.concurrent.duration.DurationInt 34 | 35 | object ArgonautSupportSpec { 36 | 37 | final case class Foo(bar: String) { 38 | require(bar startsWith "bar", "bar must start with 'bar'!") 39 | } 40 | } 41 | 42 | final class ArgonautSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 43 | 44 | import ArgonautSupportSpec._ 45 | 46 | private implicit val system: ActorSystem = ActorSystem() 47 | private implicit def fooCodec: CodecJson[Foo] = 48 | casecodec1(Foo.apply, (f: Foo) => Option(f.bar))("bar") 49 | 50 | "ArgonautSupport" should { 51 | "enable marshalling and unmarshalling objects for generic derivation" in { 52 | import ArgonautSupport._ 53 | 54 | val foo = Foo("bar") 55 | Marshal(foo) 56 | .to[RequestEntity] 57 | .flatMap(Unmarshal(_).to[Foo]) 58 | .map(_ shouldBe foo) 59 | } 60 | 61 | "enable streamed marshalling and unmarshalling for json arrays" in { 62 | import ArgonautSupport._ 63 | 64 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 65 | 66 | Marshal(Source(foos)) 67 | .to[RequestEntity] 68 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 69 | .flatMap(_.runWith(Sink.seq)) 70 | .map(_ shouldBe foos) 71 | } 72 | 73 | "provide proper error messages for requirement errors" in { 74 | import ArgonautSupport._ 75 | 76 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 77 | Unmarshal(entity) 78 | .to[Foo] 79 | .failed 80 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 81 | } 82 | 83 | "fail with NoContentException when unmarshalling empty entities" in { 84 | import ArgonautSupport._ 85 | 86 | val entity = HttpEntity.empty(`application/json`) 87 | Unmarshal(entity) 88 | .to[Foo] 89 | .failed 90 | .map(_ shouldBe Unmarshaller.NoContentException) 91 | } 92 | 93 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 94 | import ArgonautSupport._ 95 | 96 | val entity = HttpEntity("""{ "bar": "bar" }""") 97 | Unmarshal(entity) 98 | .to[Foo] 99 | .failed 100 | .map( 101 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 102 | ) 103 | } 104 | 105 | "allow unmarshalling with passed in Content-Types" in { 106 | val foo = Foo("bar") 107 | val `application/json-home` = 108 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 109 | 110 | final object CustomArgonautSupport extends ArgonautSupport { 111 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 112 | } 113 | import CustomArgonautSupport._ 114 | 115 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 116 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 117 | } 118 | } 119 | 120 | override protected def afterAll() = { 121 | Await.ready(system.terminate(), 42.seconds) 122 | super.afterAll() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /akka-http-argonaut/src/test/scala/de/heikoseeberger/akkahttpargonaut/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpargonaut 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.marshalling.Marshal 22 | import akka.http.scaladsl.model.{ HttpRequest, RequestEntity } 23 | import akka.http.scaladsl.server.Directives 24 | import akka.http.scaladsl.unmarshalling.Unmarshal 25 | import akka.stream.scaladsl.Source 26 | import argonaut.Argonaut.casecodec1 27 | import argonaut.CodecJson 28 | 29 | import scala.concurrent.Await 30 | import scala.concurrent.duration._ 31 | import scala.io.StdIn 32 | 33 | object ExampleApp { 34 | 35 | final object Foo { 36 | implicit val fooCodec: CodecJson[Foo] = 37 | casecodec1(Foo.apply, (f: Foo) => Option(f.bar))("bar") 38 | } 39 | final case class Foo(bar: String) 40 | 41 | def main(args: Array[String]): Unit = { 42 | implicit val system: ActorSystem = ActorSystem() 43 | 44 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 45 | 46 | StdIn.readLine("Hit ENTER to exit") 47 | Await.ready(system.terminate(), Duration.Inf) 48 | } 49 | 50 | def route(implicit sys: ActorSystem) = { 51 | import ArgonautSupport._ 52 | import Directives._ 53 | 54 | pathSingleSlash { 55 | post { 56 | entity(as[Foo]) { foo => 57 | complete { 58 | foo 59 | } 60 | } 61 | } 62 | } ~ pathPrefix("stream") { 63 | post { 64 | entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => 65 | import sys._ 66 | 67 | Marshal(Source.single(Foo("a"))).to[RequestEntity] 68 | 69 | complete(fooSource.throttle(1, 2.seconds)) 70 | } 71 | } ~ get { 72 | pathEndOrSingleSlash { 73 | complete( 74 | Source(0 to 5) 75 | .throttle(1, 1.seconds) 76 | .map(i => Foo(s"bar-$i")) 77 | ) 78 | } ~ pathPrefix("remote") { 79 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 80 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /akka-http-avro4s/src/main/scala/de/heikoseeberger/akkahttpavro4s/AvroSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpavro4s 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model._ 23 | import akka.http.scaladsl.model.MediaTypes.`application/json` 24 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 25 | import akka.http.scaladsl.util.FastFuture 26 | import akka.stream.scaladsl.{ Flow, Source } 27 | import akka.util.ByteString 28 | import com.sksamuel.avro4s.{ 29 | AvroInputStream, 30 | AvroOutputStream, 31 | AvroSchema, 32 | Decoder, 33 | Encoder, 34 | SchemaFor 35 | } 36 | import java.io.ByteArrayOutputStream 37 | import scala.collection.immutable.Seq 38 | import scala.concurrent.Future 39 | import scala.util.Try 40 | import scala.util.control.NonFatal 41 | 42 | /** 43 | * Automatic to and from JSON marshalling/unmarshalling using *avro4s* protocol. 44 | */ 45 | object AvroSupport extends AvroSupport 46 | 47 | /** 48 | * Automatic to and from JSON marshalling/unmarshalling using *avro4s* protocol. 49 | */ 50 | trait AvroSupport { 51 | type SourceOf[A] = Source[A, _] 52 | 53 | private val defaultMediaTypes: Seq[MediaType.WithFixedCharset] = List(`application/json`) 54 | private val defaultContentTypes: Seq[ContentTypeRange] = 55 | defaultMediaTypes.map(ContentTypeRange.apply) 56 | private val byteArrayUnmarshaller: FromEntityUnmarshaller[Array[Byte]] = 57 | Unmarshaller.byteArrayUnmarshaller.forContentTypes(unmarshallerContentTypes: _*) 58 | 59 | private def sourceByteStringMarshaller( 60 | mediaType: MediaType.WithFixedCharset 61 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 62 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 63 | try 64 | FastFuture.successful { 65 | Marshalling.WithFixedContentType( 66 | mediaType, 67 | () => HttpEntity(contentType = mediaType, data = value) 68 | ) :: Nil 69 | } 70 | catch { 71 | case NonFatal(e) => FastFuture.failed(e) 72 | } 73 | } 74 | 75 | private val jsonSourceStringMarshaller = 76 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 77 | 78 | private def jsonSource[A: SchemaFor: Encoder](entitySource: SourceOf[A])(implicit 79 | support: JsonEntityStreamingSupport 80 | ): SourceOf[ByteString] = 81 | entitySource 82 | .map { obj => 83 | val baos = new ByteArrayOutputStream() 84 | val stream = AvroOutputStream.json[A].to(baos).build() 85 | stream.write(obj) 86 | stream.close() 87 | baos.toByteArray 88 | } 89 | .map(ByteString(_)) 90 | .via(support.framingRenderer) 91 | 92 | def unmarshallerContentTypes: Seq[ContentTypeRange] = defaultContentTypes 93 | 94 | def mediaTypes: Seq[MediaType.WithFixedCharset] = defaultMediaTypes 95 | 96 | /** 97 | * `ByteString` => `A` 98 | * 99 | * @tparam A 100 | * type to decode 101 | * @return 102 | * unmarshaller for any `A` value 103 | */ 104 | implicit def fromByteStringUnmarshaller[A: SchemaFor: Decoder]: Unmarshaller[ByteString, A] = 105 | Unmarshaller { _ => bs => 106 | Future.fromTry { 107 | val schema = AvroSchema[A] 108 | 109 | Try { 110 | val bytes = bs.toArray 111 | if (bytes.length == 0) throw Unmarshaller.NoContentException 112 | AvroInputStream.json[A].from(bytes).build(schema).iterator.next() 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * HTTP entity => `A` 119 | */ 120 | implicit def unmarshaller[A: SchemaFor: Decoder]: FromEntityUnmarshaller[A] = { 121 | val schema = AvroSchema[A] 122 | byteArrayUnmarshaller.map { bytes => 123 | if (bytes.length == 0) throw Unmarshaller.NoContentException 124 | AvroInputStream.json[A].from(bytes).build(schema).iterator.next() 125 | } 126 | } 127 | 128 | /** 129 | * `A` => HTTP entity 130 | */ 131 | implicit def marshaller[A: SchemaFor: Encoder]: ToEntityMarshaller[A] = { 132 | val mediaType = mediaTypes.head 133 | val contentType = ContentType.WithFixedCharset(mediaType) 134 | Marshaller.withFixedContentType(contentType) { obj => 135 | HttpEntity.Strict( 136 | contentType, 137 | ByteString.fromArrayUnsafe { 138 | val baos = new ByteArrayOutputStream() 139 | val stream = AvroOutputStream.json[A].to(baos).build() 140 | stream.write(obj) 141 | stream.close() 142 | baos.toByteArray 143 | } 144 | ) 145 | } 146 | } 147 | 148 | /** 149 | * HTTP entity => `Source[A, _]` 150 | * 151 | * @tparam A 152 | * type to decode 153 | * @return 154 | * unmarshaller for `Source[A, _]` 155 | */ 156 | implicit def sourceUnmarshaller[A: SchemaFor: Decoder](implicit 157 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 158 | ): FromEntityUnmarshaller[SourceOf[A]] = 159 | Unmarshaller 160 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 161 | def asyncParse(bs: ByteString) = 162 | Unmarshal(bs).to[A] 163 | 164 | def ordered = 165 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 166 | 167 | def unordered = 168 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 169 | 170 | Future.successful { 171 | entity.dataBytes 172 | .via(support.framingDecoder) 173 | .via(if (support.unordered) unordered else ordered) 174 | } 175 | } 176 | .forContentTypes(unmarshallerContentTypes: _*) 177 | 178 | /** 179 | * `SourceOf[A]` => HTTP entity 180 | * 181 | * @tparam A 182 | * type to encode 183 | * @return 184 | * marshaller for any `SourceOf[A]` value 185 | */ 186 | implicit def sourceMarshaller[A: SchemaFor: Encoder](implicit 187 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 188 | ): ToEntityMarshaller[SourceOf[A]] = 189 | jsonSourceStringMarshaller.compose(jsonSource[A]) 190 | } 191 | -------------------------------------------------------------------------------- /akka-http-avro4s/src/test/scala/de/heikoseeberger/akkahttpavro4s/AvroSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpavro4s 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import com.sksamuel.avro4s.{ Decoder, Encoder, SchemaFor } 27 | import org.scalatest.BeforeAndAfterAll 28 | import org.scalatest.matchers.should.Matchers 29 | import org.scalatest.wordspec.AsyncWordSpec 30 | import scala.concurrent.Await 31 | import scala.concurrent.duration.DurationInt 32 | 33 | object AvroSupportSpec { 34 | 35 | final case class Foo(bar: String) { 36 | require(bar startsWith "bar", "bar must start with 'bar'!") 37 | } 38 | } 39 | 40 | final class AvroSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 41 | 42 | import AvroSupportSpec._ 43 | 44 | private implicit val system = ActorSystem() 45 | private implicit val schemaFor = SchemaFor[Foo] 46 | private implicit val encoder = Encoder[Foo] 47 | private implicit val decoder = Decoder[Foo] 48 | 49 | "AvroSupport" should { 50 | "enable marshalling and unmarshalling objects for generic derivation" in { 51 | import AvroSupport._ 52 | 53 | val foo = Foo("bar") 54 | Marshal(foo) 55 | .to[RequestEntity] 56 | .flatMap(Unmarshal(_).to[Foo]) 57 | .map(_ shouldBe foo) 58 | } 59 | 60 | "enable streamed marshalling and unmarshalling for json arrays" in { 61 | import AvroSupport._ 62 | 63 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 64 | 65 | Marshal(Source(foos)) 66 | .to[RequestEntity] 67 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 68 | .flatMap(_.runWith(Sink.seq)) 69 | .map(_ shouldBe foos) 70 | } 71 | 72 | "provide proper error messages for requirement errors" in { 73 | import AvroSupport._ 74 | 75 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 76 | Unmarshal(entity) 77 | .to[Foo] 78 | .failed 79 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 80 | } 81 | 82 | "fail with NoContentException when unmarshalling empty entities" in { 83 | import AvroSupport._ 84 | 85 | val entity = HttpEntity.empty(`application/json`) 86 | Unmarshal(entity) 87 | .to[Foo] 88 | .failed 89 | .map(_ shouldBe Unmarshaller.NoContentException) 90 | } 91 | 92 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 93 | import AvroSupport._ 94 | 95 | val entity = HttpEntity("""{ "bar": "bar" }""") 96 | Unmarshal(entity) 97 | .to[Foo] 98 | .failed 99 | .map( 100 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 101 | ) 102 | } 103 | 104 | "allow unmarshalling with passed in Content-Types" in { 105 | val foo = Foo("bar") 106 | val `application/json-home` = 107 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 108 | 109 | final object CustomAvroSupport extends AvroSupport { 110 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 111 | } 112 | import CustomAvroSupport._ 113 | 114 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 115 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 116 | } 117 | } 118 | 119 | override protected def afterAll(): Unit = { 120 | Await.ready(system.terminate(), 42.seconds) 121 | super.afterAll() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /akka-http-avro4s/src/test/scala/de/heikoseeberger/akkahttpavro4s/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpavro4s 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.marshalling.Marshal 22 | import akka.http.scaladsl.model.{ HttpRequest, RequestEntity } 23 | import akka.http.scaladsl.server.Directives 24 | import akka.http.scaladsl.unmarshalling.Unmarshal 25 | import akka.stream.scaladsl.Source 26 | import com.sksamuel.avro4s.{ FromRecord, SchemaFor, ToRecord } 27 | import scala.concurrent.Await 28 | import scala.concurrent.duration._ 29 | import scala.io.StdIn 30 | 31 | object ExampleApp { 32 | 33 | final object Foo { 34 | implicit val schemaFor = SchemaFor[Foo] 35 | implicit val toRecord = ToRecord[Foo] 36 | implicit val fromRecord = FromRecord[Foo] 37 | } 38 | final case class Foo(bar: String) 39 | 40 | def main(args: Array[String]): Unit = { 41 | implicit val system = ActorSystem() 42 | 43 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 44 | 45 | StdIn.readLine("Hit ENTER to exit") 46 | Await.ready(system.terminate(), Duration.Inf) 47 | } 48 | 49 | def route(implicit sys: ActorSystem) = { 50 | import AvroSupport._ 51 | import Directives._ 52 | 53 | pathSingleSlash { 54 | post { 55 | entity(as[Foo]) { foo => 56 | complete { 57 | foo 58 | } 59 | } 60 | } 61 | } ~ pathPrefix("stream") { 62 | post { 63 | entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => 64 | import sys._ 65 | 66 | Marshal(Source.single(Foo("a"))).to[RequestEntity] 67 | 68 | complete(fooSource.throttle(1, 2.seconds)) 69 | } 70 | } ~ get { 71 | pathEndOrSingleSlash { 72 | complete( 73 | Source(0 to 5) 74 | .throttle(1, 1.seconds) 75 | .map(i => Foo(s"bar-$i")) 76 | ) 77 | } ~ pathPrefix("remote") { 78 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 79 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /akka-http-circe/src/main/scala/de/heikoseeberger/akkahttpcirce/CirceSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpcirce 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model.{ 23 | ContentType, 24 | ContentTypeRange, 25 | HttpEntity, 26 | MediaType, 27 | MessageEntity 28 | } 29 | import akka.http.scaladsl.model.MediaTypes.`application/json` 30 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 31 | import akka.http.scaladsl.util.FastFuture 32 | import akka.stream.scaladsl.{ Flow, Source } 33 | import akka.util.ByteString 34 | import cats.data.{ NonEmptyList, ValidatedNel } 35 | import cats.syntax.either.catsSyntaxEither 36 | import cats.syntax.show.toShow 37 | import io.circe.{ Decoder, DecodingFailure, Encoder, Json, Printer, jawn } 38 | import io.circe.parser.parse 39 | import scala.collection.immutable.Seq 40 | import scala.concurrent.Future 41 | import scala.util.control.NonFatal 42 | 43 | /** 44 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol. The 45 | * unmarshaller fails fast, throwing the first `Error` encountered. 46 | * 47 | * To use automatic codec derivation, user needs to import `io.circe.generic.auto._`. 48 | */ 49 | object FailFastCirceSupport extends FailFastCirceSupport 50 | 51 | /** 52 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol. The 53 | * unmarshaller fails fast, throwing the first `Error` encountered. 54 | * 55 | * To use automatic codec derivation import `io.circe.generic.auto._`. 56 | */ 57 | trait FailFastCirceSupport extends BaseCirceSupport with FailFastUnmarshaller 58 | 59 | /** 60 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol. The 61 | * unmarshaller accumulates all errors in the exception `Errors`. 62 | * 63 | * To use automatic codec derivation, user needs to import `io.circe.generic.auto._`. 64 | */ 65 | object ErrorAccumulatingCirceSupport extends ErrorAccumulatingCirceSupport { 66 | final case class DecodingFailures(failures: NonEmptyList[DecodingFailure]) extends Exception { 67 | override def getMessage = failures.toList.map(_.show).mkString("\n") 68 | } 69 | } 70 | 71 | /** 72 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol. The 73 | * unmarshaller accumulates all errors in the exception `Errors`. 74 | * 75 | * To use automatic codec derivation import `io.circe.generic.auto._`. 76 | */ 77 | trait ErrorAccumulatingCirceSupport extends BaseCirceSupport with ErrorAccumulatingUnmarshaller 78 | 79 | /** 80 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol. 81 | */ 82 | trait BaseCirceSupport { 83 | type SourceOf[A] = Source[A, _] 84 | 85 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 86 | mediaTypes.map(ContentTypeRange.apply) 87 | 88 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 89 | List(`application/json`) 90 | 91 | private def sourceByteStringMarshaller( 92 | mediaType: MediaType.WithFixedCharset 93 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 94 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 95 | try 96 | FastFuture.successful { 97 | Marshalling.WithFixedContentType( 98 | mediaType, 99 | () => HttpEntity(contentType = mediaType, data = value) 100 | ) :: Nil 101 | } 102 | catch { 103 | case NonFatal(e) => FastFuture.failed(e) 104 | } 105 | } 106 | 107 | private val jsonSourceStringMarshaller = 108 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 109 | 110 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 111 | encoder: Encoder[A], 112 | printer: Printer, 113 | support: JsonEntityStreamingSupport 114 | ): SourceOf[ByteString] = 115 | entitySource 116 | .map(encoder.apply) 117 | .map(printer.printToByteBuffer) 118 | .map(ByteString(_)) 119 | .via(support.framingRenderer) 120 | 121 | /** 122 | * `ByteString` => `A` 123 | * 124 | * @tparam A 125 | * type to decode 126 | * @return 127 | * unmarshaller for any `A` value 128 | */ 129 | implicit def fromByteStringUnmarshaller[A: Decoder]: Unmarshaller[ByteString, A] 130 | 131 | /** 132 | * `Json` => HTTP entity 133 | * 134 | * @return 135 | * marshaller for JSON value 136 | */ 137 | implicit final def jsonMarshaller(implicit 138 | printer: Printer = Printer.noSpaces 139 | ): ToEntityMarshaller[Json] = 140 | Marshaller.oneOf(mediaTypes: _*) { mediaType => 141 | Marshaller.withFixedContentType(ContentType(mediaType)) { json => 142 | HttpEntity( 143 | mediaType, 144 | ByteString(printer.printToByteBuffer(json, mediaType.charset.nioCharset())) 145 | ) 146 | } 147 | } 148 | 149 | /** 150 | * `A` => HTTP entity 151 | * 152 | * @tparam A 153 | * type to encode 154 | * @return 155 | * marshaller for any `A` value 156 | */ 157 | implicit final def marshaller[A: Encoder](implicit 158 | printer: Printer = Printer.noSpaces 159 | ): ToEntityMarshaller[A] = 160 | jsonMarshaller(printer).compose(Encoder[A].apply) 161 | 162 | /** 163 | * HTTP entity => `Json` 164 | * 165 | * @return 166 | * unmarshaller for `Json` 167 | */ 168 | implicit final val jsonUnmarshaller: FromEntityUnmarshaller[Json] = 169 | Unmarshaller.byteStringUnmarshaller 170 | .forContentTypes(unmarshallerContentTypes: _*) 171 | .map { 172 | case ByteString.empty => throw Unmarshaller.NoContentException 173 | case data => jawn.parseByteBuffer(data.asByteBuffer).fold(throw _, identity) 174 | } 175 | 176 | /** 177 | * HTTP entity => `Either[io.circe.ParsingFailure, Json]` 178 | * 179 | * @return 180 | * unmarshaller for `Either[io.circe.ParsingFailure, Json]` 181 | */ 182 | implicit final val safeJsonUnmarshaller 183 | : FromEntityUnmarshaller[Either[io.circe.ParsingFailure, Json]] = 184 | Unmarshaller.stringUnmarshaller 185 | .forContentTypes(unmarshallerContentTypes: _*) 186 | .map(parse) 187 | 188 | /** 189 | * HTTP entity => `A` 190 | * 191 | * @tparam A 192 | * type to decode 193 | * @return 194 | * unmarshaller for `A` 195 | */ 196 | implicit def unmarshaller[A: Decoder]: FromEntityUnmarshaller[A] 197 | 198 | def byteStringJsonUnmarshaller: Unmarshaller[ByteString, Json] = 199 | Unmarshaller(_ => bs => Future.fromTry(jawn.parseByteBuffer(bs.asByteBuffer).toTry)) 200 | 201 | /** 202 | * HTTP entity => `Source[A, _]` 203 | * 204 | * @tparam A 205 | * type to decode 206 | * @return 207 | * unmarshaller for `Source[A, _]` 208 | */ 209 | implicit def sourceUnmarshaller[A: Decoder](implicit 210 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 211 | ): FromEntityUnmarshaller[SourceOf[A]] = 212 | Unmarshaller 213 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 214 | def asyncParse(bs: ByteString) = 215 | Unmarshal(bs).to[A] 216 | 217 | def ordered = 218 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 219 | 220 | def unordered = 221 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 222 | 223 | Future.successful { 224 | entity.dataBytes 225 | .via(support.framingDecoder) 226 | .via(if (support.unordered) unordered else ordered) 227 | } 228 | } 229 | .forContentTypes(unmarshallerContentTypes: _*) 230 | 231 | /** 232 | * `SourceOf[A]` => HTTP entity 233 | * 234 | * @tparam A 235 | * type to encode 236 | * @return 237 | * marshaller for any `SourceOf[A]` value 238 | */ 239 | implicit def sourceMarshaller[A](implicit 240 | writes: Encoder[A], 241 | printer: Printer = Printer.noSpaces, 242 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 243 | ): ToEntityMarshaller[SourceOf[A]] = 244 | jsonSourceStringMarshaller.compose(jsonSource[A]) 245 | } 246 | 247 | /** 248 | * Mix-in this trait to fail on the first error during unmarshalling. 249 | */ 250 | trait FailFastUnmarshaller { this: BaseCirceSupport => 251 | override implicit final def fromByteStringUnmarshaller[A: Decoder]: Unmarshaller[ByteString, A] = 252 | byteStringJsonUnmarshaller 253 | .map(Decoder[A].decodeJson) 254 | .map(_.fold(throw _, identity)) 255 | 256 | override implicit final def unmarshaller[A: Decoder]: FromEntityUnmarshaller[A] = 257 | jsonUnmarshaller 258 | .map(Decoder[A].decodeJson) 259 | .map(_.fold(throw _, identity)) 260 | 261 | implicit final def safeUnmarshaller[A: Decoder] 262 | : FromEntityUnmarshaller[Either[io.circe.Error, A]] = 263 | safeJsonUnmarshaller.map(_.flatMap(Decoder[A].decodeJson)) 264 | } 265 | 266 | /** 267 | * Mix-in this trait to accumulate all errors during unmarshalling. 268 | */ 269 | trait ErrorAccumulatingUnmarshaller { this: BaseCirceSupport => 270 | private def decode[A: Decoder](json: Json) = 271 | Decoder[A].decodeAccumulating(json.hcursor) 272 | 273 | override implicit final def fromByteStringUnmarshaller[A: Decoder]: Unmarshaller[ByteString, A] = 274 | byteStringJsonUnmarshaller 275 | .map(decode[A]) 276 | .map( 277 | _.fold(failures => throw ErrorAccumulatingCirceSupport.DecodingFailures(failures), identity) 278 | ) 279 | 280 | override implicit final def unmarshaller[A: Decoder]: FromEntityUnmarshaller[A] = 281 | jsonUnmarshaller 282 | .map(decode[A]) 283 | .map( 284 | _.fold(failures => throw ErrorAccumulatingCirceSupport.DecodingFailures(failures), identity) 285 | ) 286 | 287 | implicit final def safeUnmarshaller[A: Decoder] 288 | : FromEntityUnmarshaller[ValidatedNel[io.circe.Error, A]] = 289 | safeJsonUnmarshaller.map(_.toValidatedNel andThen decode[A]) 290 | } 291 | -------------------------------------------------------------------------------- /akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpcirce 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model.{ 22 | ContentTypeRange, 23 | HttpCharsets, 24 | HttpEntity, 25 | MediaType, 26 | RequestEntity, 27 | ResponseEntity 28 | } 29 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 30 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 31 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 32 | import akka.stream.scaladsl.{ Sink, Source } 33 | import cats.data.{ NonEmptyList, ValidatedNel } 34 | import io.circe.{ DecodingFailure, Encoder, ParsingFailure, Printer } 35 | import io.circe.CursorOp.DownField 36 | import org.scalatest.{ BeforeAndAfterAll, EitherValues } 37 | import org.scalatest.concurrent.ScalaFutures 38 | import org.scalatest.matchers.should.Matchers 39 | import org.scalatest.wordspec.AsyncWordSpec 40 | import scala.concurrent.Await 41 | import scala.concurrent.duration.DurationInt 42 | 43 | object CirceSupportSpec { 44 | 45 | final case class Foo(bar: String) { 46 | require(bar startsWith "bar", "bar must start with 'bar'!") 47 | } 48 | 49 | final case class MultiFoo(a: String, b: String) 50 | 51 | final case class OptionFoo(a: Option[String]) 52 | } 53 | 54 | final class CirceSupportSpec 55 | extends AsyncWordSpec 56 | with Matchers 57 | with BeforeAndAfterAll 58 | with ScalaFutures 59 | with EitherValues { 60 | import CirceSupportSpec._ 61 | 62 | private implicit val system: ActorSystem = ActorSystem() 63 | 64 | private val `application/json-home` = 65 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 66 | 67 | /** 68 | * Specs common to both [[FailFastCirceSupport]] and [[ErrorAccumulatingCirceSupport]] 69 | */ 70 | private def commonCirceSupport(support: BaseCirceSupport) = { 71 | import io.circe.generic.auto._ 72 | import support._ 73 | 74 | "enable marshalling and unmarshalling objects for generic derivation" in { 75 | val foo = Foo("bar") 76 | Marshal(foo) 77 | .to[RequestEntity] 78 | .flatMap(Unmarshal(_).to[Foo]) 79 | .map(_ shouldBe foo) 80 | } 81 | 82 | "enable streamed marshalling and unmarshalling for json arrays" in { 83 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 84 | 85 | // Don't know why, the encoder is not resolving alongside the marshaller 86 | // this only happens if we use the implicits from BaseCirceSupport 87 | // so, tried to create it before and guess what? it worked. 88 | // not sure if this is a bug, but, the error is this: 89 | // diverging implicit expansion for type io.circe.Encoder[A] 90 | // [error] starting with lazy value encodeZoneOffset in object Encoder 91 | implicit val e = implicitly[Encoder[Foo]] 92 | 93 | Marshal(Source(foos)) 94 | .to[ResponseEntity] 95 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 96 | .flatMap(_.runWith(Sink.seq)) 97 | .map(_ shouldBe foos) 98 | } 99 | 100 | "provide proper error messages for requirement errors" in { 101 | val entity = HttpEntity(`application/json`, """{ "bar": "baz" }""") 102 | Unmarshal(entity) 103 | .to[Foo] 104 | .failed 105 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 106 | } 107 | 108 | "fail with NoContentException when unmarshalling empty entities" in { 109 | val entity = HttpEntity.empty(`application/json`) 110 | Unmarshal(entity) 111 | .to[Foo] 112 | .failed 113 | .map(_ shouldBe Unmarshaller.NoContentException) 114 | } 115 | 116 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 117 | val entity = HttpEntity("""{ "bar": "bar" }""") 118 | Unmarshal(entity) 119 | .to[Foo] 120 | .failed 121 | .map( 122 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 123 | ) 124 | } 125 | 126 | "write None as null by default" in { 127 | val optionFoo = OptionFoo(None) 128 | Marshal(optionFoo) 129 | .to[RequestEntity] 130 | .map(_.asInstanceOf[HttpEntity.Strict].data.decodeString("UTF-8") shouldBe "{\"a\":null}") 131 | } 132 | 133 | "not write None" in { 134 | implicit val printer: Printer = Printer.noSpaces.copy(dropNullValues = true) 135 | val optionFoo = OptionFoo(None) 136 | Marshal(optionFoo) 137 | .to[RequestEntity] 138 | .map(_.asInstanceOf[HttpEntity.Strict].data.decodeString("UTF-8") shouldBe "{}") 139 | } 140 | } 141 | 142 | "FailFastCirceSupport" should { 143 | import FailFastCirceSupport._ 144 | import io.circe.generic.auto._ 145 | 146 | behave like commonCirceSupport(FailFastCirceSupport) 147 | 148 | "fail with a ParsingFailure when unmarshalling empty entities with safeUnmarshaller" in { 149 | val entity = HttpEntity.empty(`application/json`) 150 | Unmarshal(entity) 151 | .to[Either[io.circe.Error, Foo]] 152 | .futureValue 153 | .left 154 | .value shouldBe a[ParsingFailure] 155 | } 156 | 157 | "fail-fast and return only the first unmarshalling error" in { 158 | val entity = HttpEntity(`application/json`, """{ "a": 1, "b": 2 }""") 159 | val error = DecodingFailure("String", List(DownField("a"))) 160 | Unmarshal(entity) 161 | .to[MultiFoo] 162 | .failed 163 | .map(_ shouldBe error) 164 | } 165 | 166 | "fail-fast and return only the first unmarshalling error with safeUnmarshaller" in { 167 | val entity = HttpEntity(`application/json`, """{ "a": 1, "b": 2 }""") 168 | val error = DecodingFailure("String", List(DownField("a"))) 169 | Unmarshal(entity) 170 | .to[Either[io.circe.Error, MultiFoo]] 171 | .futureValue 172 | .left 173 | .value shouldBe error 174 | } 175 | 176 | "allow unmarshalling with passed in Content-Types" in { 177 | val foo = Foo("bar") 178 | 179 | final object CustomCirceSupport extends FailFastCirceSupport { 180 | override def unmarshallerContentTypes: List[ContentTypeRange] = 181 | List(`application/json`, `application/json-home`) 182 | } 183 | import CustomCirceSupport._ 184 | 185 | val entity = HttpEntity(`application/json`, """{ "bar": "bar" }""") 186 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 187 | } 188 | } 189 | 190 | "ErrorAccumulatingCirceSupport" should { 191 | import ErrorAccumulatingCirceSupport._ 192 | import io.circe.generic.auto._ 193 | 194 | behave like commonCirceSupport(ErrorAccumulatingCirceSupport) 195 | 196 | "fail with a NonEmptyList of Errors when unmarshalling empty entities with safeUnmarshaller" in { 197 | val entity = HttpEntity.empty(`application/json`) 198 | Unmarshal(entity) 199 | .to[ValidatedNel[io.circe.Error, Foo]] 200 | .futureValue 201 | .toEither 202 | .left 203 | .value shouldBe a[NonEmptyList[_]] 204 | } 205 | 206 | "accumulate and return all unmarshalling errors" in { 207 | val entity = HttpEntity(`application/json`, """{ "a": 1, "b": 2 }""") 208 | val errors = 209 | NonEmptyList.of( 210 | DecodingFailure("String", List(DownField("a"))), 211 | DecodingFailure("String", List(DownField("b"))) 212 | ) 213 | val errorMessage = ErrorAccumulatingCirceSupport.DecodingFailures(errors).getMessage 214 | Unmarshal(entity) 215 | .to[MultiFoo] 216 | .failed 217 | .map(_.getMessage shouldBe errorMessage) 218 | } 219 | 220 | "accumulate and return all unmarshalling errors with safeUnmarshaller" in { 221 | val entity = HttpEntity(`application/json`, """{ "a": 1, "b": 2 }""") 222 | val errors = 223 | NonEmptyList.of( 224 | DecodingFailure("String", List(DownField("a"))), 225 | DecodingFailure("String", List(DownField("b"))) 226 | ) 227 | val errorMessage = ErrorAccumulatingCirceSupport.DecodingFailures(errors).getMessage 228 | Unmarshal(entity) 229 | .to[ValidatedNel[io.circe.Error, MultiFoo]] 230 | .futureValue 231 | .toEither 232 | .left 233 | .value shouldBe errors 234 | } 235 | 236 | "allow unmarshalling with passed in Content-Types" in { 237 | val foo = Foo("bar") 238 | 239 | final object CustomCirceSupport extends ErrorAccumulatingCirceSupport { 240 | override def unmarshallerContentTypes: List[ContentTypeRange] = 241 | List(`application/json`, `application/json-home`) 242 | } 243 | import CustomCirceSupport._ 244 | 245 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 246 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 247 | } 248 | } 249 | 250 | override protected def afterAll(): Unit = { 251 | Await.ready(system.terminate(), 42.seconds) 252 | super.afterAll() 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpcirce 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.marshalling.Marshal 22 | import akka.http.scaladsl.model.{ HttpRequest, RequestEntity } 23 | import akka.http.scaladsl.server.Directives 24 | import akka.http.scaladsl.unmarshalling.Unmarshal 25 | import akka.stream.scaladsl.Source 26 | import scala.concurrent.duration._ 27 | import scala.io.StdIn 28 | 29 | object ExampleApp { 30 | 31 | private final case class Foo(bar: String) 32 | 33 | def main(args: Array[String]): Unit = { 34 | implicit val system = ActorSystem() 35 | 36 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 37 | 38 | StdIn.readLine("Hit ENTER to exit") 39 | system.terminate() 40 | } 41 | 42 | private def route(implicit sys: ActorSystem) = { 43 | import Directives._ 44 | import FailFastCirceSupport._ 45 | import io.circe.generic.auto._ 46 | 47 | pathSingleSlash { 48 | post { 49 | entity(as[Foo]) { foo => 50 | complete { 51 | foo 52 | } 53 | } 54 | } 55 | } ~ pathPrefix("stream") { 56 | post { 57 | entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => 58 | import sys._ 59 | 60 | Marshal(Source.single(Foo("a"))).to[RequestEntity] 61 | 62 | complete(fooSource.throttle(1, 2.seconds)) 63 | } 64 | } ~ get { 65 | pathEndOrSingleSlash { 66 | complete( 67 | Source(0 to 5) 68 | .throttle(1, 1.seconds) 69 | .map(i => Foo(s"bar-$i")) 70 | ) 71 | } ~ pathPrefix("remote") { 72 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 73 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /akka-http-jackson/src/main/scala/de/heikoseeberger/akkahttpjackson/JacksonSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjackson 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.javadsl.marshallers.jackson.Jackson 21 | import akka.http.scaladsl.common.EntityStreamingSupport 22 | import akka.http.scaladsl.marshalling._ 23 | import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } 24 | import akka.http.scaladsl.model.MediaTypes.`application/json` 25 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 26 | import akka.http.scaladsl.util.FastFuture 27 | import akka.stream.scaladsl.{ Flow, Source } 28 | import akka.util.ByteString 29 | import com.fasterxml.jackson.core.`type`.TypeReference 30 | import com.fasterxml.jackson.databind.ObjectMapper 31 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 32 | import java.lang.reflect.{ ParameterizedType, Type => JType } 33 | import scala.collection.immutable.Seq 34 | import scala.concurrent.Future 35 | import scala.reflect.runtime.universe._ 36 | import scala.util.Try 37 | import scala.util.control.NonFatal 38 | 39 | /** 40 | * Automatic to and from JSON marshalling/unmarshalling usung an in-scope Jackon's ObjectMapper 41 | */ 42 | object JacksonSupport extends JacksonSupport { 43 | 44 | val defaultObjectMapper: ObjectMapper = 45 | new ObjectMapper().registerModule(DefaultScalaModule) 46 | } 47 | 48 | /** 49 | * JSON marshalling/unmarshalling using an in-scope Jackson's ObjectMapper 50 | */ 51 | trait JacksonSupport { 52 | type SourceOf[A] = Source[A, _] 53 | 54 | import JacksonSupport._ 55 | 56 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 57 | mediaTypes.map(ContentTypeRange.apply) 58 | 59 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 60 | List(`application/json`) 61 | 62 | private val jsonStringUnmarshaller = 63 | Unmarshaller.byteStringUnmarshaller 64 | .forContentTypes(unmarshallerContentTypes: _*) 65 | .mapWithCharset { 66 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 67 | case (data, charset) => data.decodeString(charset.nioCharset.name) 68 | } 69 | 70 | private def typeReference[T: TypeTag] = { 71 | val t = typeTag[T] 72 | val mirror = t.mirror 73 | def mapType(t: Type): JType = 74 | if (t.typeArgs.isEmpty) 75 | mirror.runtimeClass(t) 76 | else 77 | new ParameterizedType { 78 | def getRawType() = mirror.runtimeClass(t) 79 | def getActualTypeArguments() = t.typeArgs.map(mapType).toArray 80 | def getOwnerType() = null 81 | } 82 | 83 | new TypeReference[T] { 84 | override def getType = mapType(t.tpe) 85 | } 86 | } 87 | 88 | private def sourceByteStringMarshaller( 89 | mediaType: MediaType.WithFixedCharset 90 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 91 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 92 | try 93 | FastFuture.successful { 94 | Marshalling.WithFixedContentType( 95 | mediaType, 96 | () => HttpEntity(contentType = mediaType, data = value) 97 | ) :: Nil 98 | } 99 | catch { 100 | case NonFatal(e) => FastFuture.failed(e) 101 | } 102 | } 103 | 104 | private val jsonSourceStringMarshaller = 105 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 106 | 107 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 108 | objectMapper: ObjectMapper = defaultObjectMapper, 109 | support: JsonEntityStreamingSupport 110 | ): SourceOf[ByteString] = 111 | entitySource 112 | .map(objectMapper.writeValueAsBytes) 113 | .map(ByteString(_)) 114 | .via(support.framingRenderer) 115 | 116 | /** 117 | * HTTP entity => `A` 118 | */ 119 | implicit def unmarshaller[A](implicit 120 | ct: TypeTag[A], 121 | objectMapper: ObjectMapper = defaultObjectMapper 122 | ): FromEntityUnmarshaller[A] = 123 | jsonStringUnmarshaller.map(data => objectMapper.readValue(data, typeReference[A])) 124 | 125 | /** 126 | * `A` => HTTP entity 127 | */ 128 | implicit def marshaller[Object](implicit 129 | objectMapper: ObjectMapper = defaultObjectMapper 130 | ): ToEntityMarshaller[Object] = 131 | Jackson.marshaller[Object](objectMapper) 132 | 133 | /** 134 | * `ByteString` => `A` 135 | * 136 | * @tparam A 137 | * type to decode 138 | * @return 139 | * unmarshaller for any `A` value 140 | */ 141 | implicit def fromByteStringUnmarshaller[A](implicit 142 | ct: TypeTag[A], 143 | objectMapper: ObjectMapper = defaultObjectMapper 144 | ): Unmarshaller[ByteString, A] = 145 | Unmarshaller { _ => bs => 146 | Future.fromTry(Try(objectMapper.readValue(bs.toArray, typeReference[A]))) 147 | } 148 | 149 | /** 150 | * HTTP entity => `Source[A, _]` 151 | * 152 | * @tparam A 153 | * type to decode 154 | * @return 155 | * unmarshaller for `Source[A, _]` 156 | */ 157 | implicit def sourceUnmarshaller[A](implicit 158 | ct: TypeTag[A], 159 | objectMapper: ObjectMapper = defaultObjectMapper, 160 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 161 | ): FromEntityUnmarshaller[SourceOf[A]] = 162 | Unmarshaller 163 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 164 | def asyncParse(bs: ByteString) = 165 | Unmarshal(bs).to[A] 166 | 167 | def ordered = 168 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 169 | 170 | def unordered = 171 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 172 | 173 | Future.successful { 174 | entity.dataBytes 175 | .via(support.framingDecoder) 176 | .via(if (support.unordered) unordered else ordered) 177 | } 178 | } 179 | .forContentTypes(unmarshallerContentTypes: _*) 180 | 181 | /** 182 | * `SourceOf[A]` => HTTP entity 183 | * 184 | * @tparam A 185 | * type to encode 186 | * @return 187 | * marshaller for any `SourceOf[A]` value 188 | */ 189 | implicit def sourceMarshaller[A](implicit 190 | ct: TypeTag[A], 191 | objectMapper: ObjectMapper = defaultObjectMapper, 192 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 193 | ): ToEntityMarshaller[SourceOf[A]] = 194 | jsonSourceStringMarshaller.compose(jsonSource[A]) 195 | } 196 | -------------------------------------------------------------------------------- /akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjackson 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.HttpRequest 22 | import akka.http.scaladsl.server.Directives 23 | import akka.http.scaladsl.unmarshalling.Unmarshal 24 | import akka.stream.scaladsl.Source 25 | import scala.concurrent.Await 26 | import scala.concurrent.duration._ 27 | import scala.io.StdIn 28 | 29 | object ExampleApp { 30 | 31 | final case class Foo(bar: String) 32 | 33 | def main(args: Array[String]): Unit = { 34 | implicit val system = ActorSystem() 35 | 36 | // provide an implicit ObjectMapper if you want serialization/deserialization to use it 37 | // instead of a default ObjectMapper configured only with DefaultScalaModule provided 38 | // by JacksonSupport 39 | // 40 | // for example: 41 | // 42 | // implicit val objectMapper = new ObjectMapper() 43 | // .registerModule(DefaultScalaModule) 44 | // .registerModule(new GuavaModule()) 45 | 46 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 47 | 48 | StdIn.readLine("Hit ENTER to exit") 49 | Await.ready(system.terminate(), Duration.Inf) 50 | } 51 | 52 | def route(implicit sys: ActorSystem) = { 53 | import Directives._ 54 | import JacksonSupport._ 55 | 56 | pathSingleSlash { 57 | post { 58 | 59 | entity(as[Foo]) { foo => 60 | complete { 61 | foo 62 | } 63 | } 64 | } 65 | } ~ pathPrefix("stream") { 66 | post { 67 | entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => 68 | complete(fooSource.throttle(1, 2.seconds)) 69 | } 70 | } ~ get { 71 | pathEndOrSingleSlash { 72 | complete( 73 | Source(0 to 5) 74 | .throttle(1, 1.seconds) 75 | .map(i => Foo(s"bar-$i")) 76 | ) 77 | } ~ pathPrefix("remote") { 78 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 79 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/JacksonSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjackson 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import org.scalatest.BeforeAndAfterAll 27 | import org.scalatest.matchers.should.Matchers 28 | import org.scalatest.wordspec.AsyncWordSpec 29 | import scala.concurrent.Await 30 | import scala.concurrent.duration.DurationInt 31 | 32 | object JacksonSupportSpec { 33 | 34 | final case class Foo(bar: String) { 35 | require(bar startsWith "bar", "bar must start with 'bar'!") 36 | } 37 | } 38 | 39 | final class JacksonSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 40 | import JacksonSupport._ 41 | import JacksonSupportSpec._ 42 | 43 | private implicit val system = ActorSystem() 44 | 45 | "JacksonSupport" should { 46 | "should enable marshalling and unmarshalling of case classes" in { 47 | val foo = Foo("bar") 48 | Marshal(foo) 49 | .to[RequestEntity] 50 | .flatMap(Unmarshal(_).to[Foo]) 51 | .map(_ shouldBe foo) 52 | } 53 | 54 | "enable streamed marshalling and unmarshalling for json arrays" in { 55 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 56 | 57 | Marshal(Source(foos)) 58 | .to[RequestEntity] 59 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 60 | .flatMap(_.runWith(Sink.seq)) 61 | .map(_ shouldBe foos) 62 | } 63 | 64 | "should enable marshalling and unmarshalling of arrays of values" in { 65 | val foo = Seq(Foo("bar")) 66 | Marshal(foo) 67 | .to[RequestEntity] 68 | .flatMap(Unmarshal(_).to[Seq[Foo]]) 69 | .map(_ shouldBe foo) 70 | } 71 | 72 | "provide proper error messages for requirement errors" in { 73 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 74 | Unmarshal(entity) 75 | .to[Foo] 76 | .failed 77 | .map(_.getMessage should include("requirement failed: bar must start with 'bar'!")) 78 | } 79 | 80 | "fail with NoContentException when unmarshalling empty entities" in { 81 | val entity = HttpEntity.empty(`application/json`) 82 | Unmarshal(entity) 83 | .to[Foo] 84 | .failed 85 | .map(_ shouldBe Unmarshaller.NoContentException) 86 | } 87 | 88 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 89 | val entity = HttpEntity("""{ "bar": "bar" }""") 90 | Unmarshal(entity) 91 | .to[Foo] 92 | .failed 93 | .map( 94 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 95 | ) 96 | } 97 | 98 | "allow unmarshalling with passed in Content-Types" in { 99 | val foo = Foo("bar") 100 | val `application/json-home` = 101 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 102 | 103 | final object CustomJacksonSupport extends JacksonSupport { 104 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 105 | } 106 | import CustomJacksonSupport._ 107 | 108 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 109 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 110 | } 111 | } 112 | 113 | override protected def afterAll() = { 114 | Await.ready(system.terminate(), 42.seconds) 115 | super.afterAll() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /akka-http-json4s/src/main/scala/de/heikoseeberger/akkahttpjson4s/Json4sSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjson4s 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } 23 | import akka.http.scaladsl.model.MediaTypes.`application/json` 24 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 25 | import akka.http.scaladsl.util.FastFuture 26 | import akka.stream.Materializer 27 | import akka.stream.scaladsl.{ Flow, Source } 28 | import akka.util.ByteString 29 | import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty.False 30 | import java.lang.reflect.InvocationTargetException 31 | import org.json4s.{ Formats, MappingException, Serialization } 32 | import scala.collection.immutable.Seq 33 | import scala.concurrent.{ ExecutionContext, Future } 34 | import scala.util.Try 35 | import scala.util.control.NonFatal 36 | 37 | /** 38 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope *Json4s* protocol. 39 | * 40 | * Pretty printing is enabled if an implicit [[Json4sSupport.ShouldWritePretty.True]] is in scope. 41 | */ 42 | object Json4sSupport extends Json4sSupport { 43 | 44 | sealed abstract class ShouldWritePretty 45 | 46 | final object ShouldWritePretty { 47 | final object True extends ShouldWritePretty 48 | final object False extends ShouldWritePretty 49 | } 50 | } 51 | 52 | /** 53 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope *Json4s* protocol. 54 | * 55 | * Pretty printing is enabled if an implicit [[Json4sSupport.ShouldWritePretty.True]] is in scope. 56 | */ 57 | trait Json4sSupport { 58 | type SourceOf[A] = Source[A, _] 59 | 60 | import Json4sSupport._ 61 | 62 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 63 | mediaTypes.map(ContentTypeRange.apply) 64 | 65 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 66 | List(`application/json`) 67 | 68 | private val jsonStringUnmarshaller = 69 | Unmarshaller.byteStringUnmarshaller 70 | .forContentTypes(unmarshallerContentTypes: _*) 71 | .mapWithCharset { 72 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 73 | case (data, charset) => data.decodeString(charset.nioCharset.name) 74 | } 75 | 76 | private val jsonStringMarshaller = 77 | Marshaller.oneOf(mediaTypes: _*)(Marshaller.stringMarshaller) 78 | 79 | private def sourceByteStringMarshaller( 80 | mediaType: MediaType.WithFixedCharset 81 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 82 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 83 | try 84 | FastFuture.successful { 85 | Marshalling.WithFixedContentType( 86 | mediaType, 87 | () => HttpEntity(contentType = mediaType, data = value) 88 | ) :: Nil 89 | } 90 | catch { 91 | case NonFatal(e) => FastFuture.failed(e) 92 | } 93 | } 94 | 95 | private val jsonSourceStringMarshaller = 96 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 97 | 98 | private def jsonSource[A <: AnyRef](entitySource: SourceOf[A])(implicit 99 | f: Formats, 100 | s: Serialization, 101 | isPretty: ShouldWritePretty, 102 | support: JsonEntityStreamingSupport 103 | ): SourceOf[ByteString] = 104 | entitySource 105 | .map(e => 106 | if (isPretty == False) s.write[A](e) 107 | else s.writePretty[A](e) 108 | ) 109 | .map(ByteString(_)) 110 | .via(support.framingRenderer) 111 | 112 | /** 113 | * HTTP entity => `A` 114 | * 115 | * @tparam A 116 | * type to decode 117 | * @return 118 | * unmarshaller for `A` 119 | */ 120 | implicit def unmarshaller[A: Manifest](implicit 121 | serialization: Serialization, 122 | formats: Formats 123 | ): FromEntityUnmarshaller[A] = 124 | jsonStringUnmarshaller 125 | .map(s => serialization.read(s)) 126 | .recover(throwCause) 127 | 128 | /** 129 | * `A` => HTTP entity 130 | * 131 | * @tparam A 132 | * type to encode, must be upper bounded by `AnyRef` 133 | * @return 134 | * marshaller for any `A` value 135 | */ 136 | implicit def marshaller[A <: AnyRef](implicit 137 | serialization: Serialization, 138 | formats: Formats, 139 | shouldWritePretty: ShouldWritePretty = ShouldWritePretty.False 140 | ): ToEntityMarshaller[A] = 141 | shouldWritePretty match { 142 | case ShouldWritePretty.False => 143 | jsonStringMarshaller.compose(serialization.write[A]) 144 | case ShouldWritePretty.True => 145 | jsonStringMarshaller.compose(serialization.writePretty[A]) 146 | } 147 | 148 | /** 149 | * `ByteString` => `A` 150 | * 151 | * @tparam A 152 | * type to decode 153 | * @return 154 | * unmarshaller for any `A` value 155 | */ 156 | implicit def fromByteStringUnmarshaller[A: Manifest](implicit 157 | s: Serialization, 158 | formats: Formats 159 | ): Unmarshaller[ByteString, A] = { 160 | val result: Unmarshaller[ByteString, A] = 161 | Unmarshaller(_ => bs => Future.fromTry(Try(s.read(bs.utf8String)))) 162 | 163 | result.recover(throwCause) 164 | } 165 | 166 | /** 167 | * HTTP entity => `Source[A, _]` 168 | * 169 | * @tparam A 170 | * type to decode 171 | * @return 172 | * unmarshaller for `Source[A, _]` 173 | */ 174 | implicit def sourceUnmarshaller[A: Manifest](implicit 175 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json(), 176 | serialization: Serialization, 177 | formats: Formats 178 | ): FromEntityUnmarshaller[SourceOf[A]] = 179 | Unmarshaller 180 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 181 | def asyncParse(bs: ByteString) = 182 | Unmarshal(bs).to[A] 183 | 184 | def ordered = 185 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 186 | 187 | def unordered = 188 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 189 | 190 | Future.successful { 191 | entity.dataBytes 192 | .via(support.framingDecoder) 193 | .via(if (support.unordered) unordered else ordered) 194 | } 195 | } 196 | .forContentTypes(unmarshallerContentTypes: _*) 197 | 198 | /** 199 | * `SourceOf[A]` => HTTP entity 200 | * 201 | * @tparam A 202 | * type to encode 203 | * @return 204 | * marshaller for any `SourceOf[A]` value 205 | */ 206 | implicit def sourceMarshaller[A <: AnyRef](implicit 207 | serialization: Serialization, 208 | formats: Formats, 209 | shouldWritePretty: ShouldWritePretty = False, 210 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 211 | ): ToEntityMarshaller[SourceOf[A]] = 212 | jsonSourceStringMarshaller.compose(jsonSource[A]) 213 | 214 | private def throwCause[A]( 215 | ec: ExecutionContext 216 | )(mat: Materializer): PartialFunction[Throwable, A] = { 217 | case e: MappingException if e.cause.isInstanceOf[InvocationTargetException] => 218 | throw e.cause.getCause 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /akka-http-json4s/src/test/scala/de/heikoseeberger/akkahttpjson4s/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjson4s 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.HttpRequest 22 | import akka.http.scaladsl.server.Directives 23 | import akka.http.scaladsl.unmarshalling.Unmarshal 24 | import akka.stream.scaladsl.Source 25 | import org.json4s.jackson.Serialization 26 | import org.json4s.{ DefaultFormats, jackson } 27 | 28 | import scala.concurrent.Await 29 | import scala.concurrent.duration._ 30 | import scala.io.StdIn 31 | 32 | object ExampleApp { 33 | 34 | final case class Foo(bar: String) 35 | 36 | def main(args: Array[String]): Unit = { 37 | implicit val system: ActorSystem = ActorSystem() 38 | 39 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 40 | 41 | StdIn.readLine("Hit ENTER to exit") 42 | Await.ready(system.terminate(), Duration.Inf) 43 | } 44 | 45 | def route(implicit sys: ActorSystem) = { 46 | import Directives._ 47 | import Json4sSupport._ 48 | 49 | implicit val serialization: Serialization.type = 50 | jackson.Serialization // or native.Serialization 51 | implicit val formats: DefaultFormats.type = DefaultFormats 52 | 53 | pathSingleSlash { 54 | post { 55 | entity(as[Foo]) { foo => 56 | complete { 57 | foo 58 | } 59 | } 60 | } 61 | } ~ pathPrefix("stream") { 62 | post { 63 | entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => 64 | complete(fooSource.throttle(1, 2.seconds)) 65 | } 66 | } ~ get { 67 | pathEndOrSingleSlash { 68 | complete( 69 | Source(0 to 5) 70 | .throttle(1, 1.seconds) 71 | .map(i => Foo(s"bar-$i")) 72 | ) 73 | } ~ pathPrefix("remote") { 74 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 75 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /akka-http-json4s/src/test/scala/de/heikoseeberger/akkahttpjson4s/Json4sSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjson4s 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import org.json4s.jackson.Serialization 27 | import org.json4s.{ DefaultFormats, jackson, native } 28 | import org.scalatest.BeforeAndAfterAll 29 | import org.scalatest.matchers.should.Matchers 30 | import org.scalatest.wordspec.AsyncWordSpec 31 | 32 | import scala.concurrent.Await 33 | import scala.concurrent.duration.DurationInt 34 | 35 | object Json4sSupportSpec { 36 | 37 | final case class Foo(bar: String) { 38 | require(bar startsWith "bar", "bar must start with 'bar'!") 39 | } 40 | } 41 | 42 | final class Json4sSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 43 | import Json4sSupport._ 44 | import Json4sSupportSpec._ 45 | 46 | private implicit val system: ActorSystem = ActorSystem() 47 | private implicit val formats: DefaultFormats.type = DefaultFormats 48 | 49 | private val foo = Foo("bar") 50 | 51 | "Json4sSupport" should { 52 | "enable marshalling and unmarshalling objects for `DefaultFormats` and `jackson.Serialization`" in { 53 | implicit val serialization: Serialization.type = jackson.Serialization 54 | Marshal(foo) 55 | .to[RequestEntity] 56 | .flatMap(Unmarshal(_).to[Foo]) 57 | .map(_ shouldBe foo) 58 | } 59 | 60 | "enable streamed marshalling and unmarshalling for json arrays" in { 61 | implicit val serialization: Serialization.type = jackson.Serialization 62 | 63 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 64 | 65 | Marshal(Source(foos)) 66 | .to[RequestEntity] 67 | .flatMap { entity => 68 | Unmarshal(entity).to[String].onComplete(println) 69 | 70 | Unmarshal(entity).to[SourceOf[Foo]] 71 | } 72 | .flatMap(_.runWith(Sink.seq)) 73 | .map(_ shouldBe foos) 74 | } 75 | 76 | "enable marshalling and unmarshalling objects for `DefaultFormats` and `native.Serialization`" in { 77 | implicit val serialization: native.Serialization.type = native.Serialization 78 | Marshal(foo) 79 | .to[RequestEntity] 80 | .flatMap(Unmarshal(_).to[Foo]) 81 | .map(_ shouldBe foo) 82 | } 83 | 84 | "provide proper error messages for requirement errors" in { 85 | implicit val serialization: native.Serialization.type = native.Serialization 86 | val entity = 87 | HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 88 | Unmarshal(entity) 89 | .to[Foo] 90 | .failed 91 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 92 | } 93 | 94 | "fail with NoContentException when unmarshalling empty entities" in { 95 | implicit val serialization: native.Serialization.type = native.Serialization 96 | val entity = HttpEntity.empty(`application/json`) 97 | Unmarshal(entity) 98 | .to[Foo] 99 | .failed 100 | .map(_ shouldBe Unmarshaller.NoContentException) 101 | } 102 | 103 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 104 | implicit val serialization: native.Serialization.type = native.Serialization 105 | val entity = HttpEntity("""{ "bar": "bar" }""") 106 | Unmarshal(entity) 107 | .to[Foo] 108 | .failed 109 | .map( 110 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 111 | ) 112 | } 113 | 114 | "allow unmarshalling with passed in Content-Types" in { 115 | implicit val serialization: native.Serialization.type = native.Serialization 116 | val foo = Foo("bar") 117 | val `application/json-home` = 118 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 119 | 120 | final object CustomJson4sSupport extends Json4sSupport { 121 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 122 | } 123 | import CustomJson4sSupport._ 124 | 125 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 126 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 127 | } 128 | } 129 | 130 | override protected def afterAll() = { 131 | Await.ready(system.terminate(), 42.seconds) 132 | super.afterAll() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /akka-http-jsoniter-scala/src/main/scala/de/heikoseeberger/akkahttpjsoniterscala/JsoniterScalaSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjsoniterscala 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling._ 22 | import akka.http.scaladsl.model.{ 23 | ContentType, 24 | ContentTypeRange, 25 | HttpEntity, 26 | MediaType, 27 | MessageEntity 28 | } 29 | import akka.http.scaladsl.model.MediaTypes.`application/json` 30 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 31 | import akka.http.scaladsl.util.FastFuture 32 | import akka.stream.scaladsl.{ Flow, Source } 33 | import akka.util.ByteString 34 | import com.github.plokhotnyuk.jsoniter_scala.core._ 35 | import scala.collection.immutable.Seq 36 | import scala.concurrent.Future 37 | import scala.util.Try 38 | import scala.util.control.NonFatal 39 | 40 | /** 41 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope instance of 42 | * JsonValueCodec 43 | */ 44 | object JsoniterScalaSupport extends JsoniterScalaSupport { 45 | val defaultReaderConfig: ReaderConfig = 46 | ReaderConfig.withPreferredBufSize(100 * 1024).withPreferredCharBufSize(10 * 1024) 47 | val defaultWriterConfig: WriterConfig = WriterConfig.withPreferredBufSize(100 * 1024) 48 | } 49 | 50 | /** 51 | * JSON marshalling/unmarshalling using an in-scope instance of JsonValueCodec 52 | */ 53 | trait JsoniterScalaSupport { 54 | type SourceOf[A] = Source[A, _] 55 | 56 | import JsoniterScalaSupport._ 57 | 58 | private val defaultMediaTypes: Seq[MediaType.WithFixedCharset] = List(`application/json`) 59 | private val defaultContentTypes: Seq[ContentTypeRange] = 60 | defaultMediaTypes.map(ContentTypeRange.apply) 61 | private val byteArrayUnmarshaller: FromEntityUnmarshaller[Array[Byte]] = 62 | Unmarshaller.byteArrayUnmarshaller.forContentTypes(unmarshallerContentTypes: _*) 63 | 64 | private def sourceByteStringMarshaller( 65 | mediaType: MediaType.WithFixedCharset 66 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 67 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 68 | try 69 | FastFuture.successful { 70 | Marshalling.WithFixedContentType( 71 | mediaType, 72 | () => HttpEntity(contentType = mediaType, data = value) 73 | ) :: Nil 74 | } 75 | catch { 76 | case NonFatal(e) => FastFuture.failed(e) 77 | } 78 | } 79 | 80 | private val jsonSourceStringMarshaller = 81 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 82 | 83 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 84 | codec: JsonValueCodec[A], 85 | config: WriterConfig = defaultWriterConfig, 86 | support: JsonEntityStreamingSupport 87 | ): SourceOf[ByteString] = 88 | entitySource 89 | .map(writeToArray(_, config)) 90 | .map(ByteString(_)) 91 | .via(support.framingRenderer) 92 | 93 | def unmarshallerContentTypes: Seq[ContentTypeRange] = defaultContentTypes 94 | 95 | def mediaTypes: Seq[MediaType.WithFixedCharset] = defaultMediaTypes 96 | 97 | /** 98 | * HTTP entity => `A` 99 | */ 100 | implicit def unmarshaller[A](implicit 101 | codec: JsonValueCodec[A], 102 | config: ReaderConfig = defaultReaderConfig 103 | ): FromEntityUnmarshaller[A] = 104 | byteArrayUnmarshaller.map { bytes => 105 | if (bytes.length == 0) throw Unmarshaller.NoContentException 106 | readFromArray[A](bytes, config) 107 | } 108 | 109 | /** 110 | * `A` => HTTP entity 111 | */ 112 | implicit def marshaller[A](implicit 113 | codec: JsonValueCodec[A], 114 | config: WriterConfig = defaultWriterConfig 115 | ): ToEntityMarshaller[A] = { 116 | val mediaType = mediaTypes.head 117 | val contentType = ContentType.WithFixedCharset(mediaType) 118 | Marshaller.withFixedContentType(contentType) { obj => 119 | HttpEntity.Strict(contentType, ByteString.fromArrayUnsafe(writeToArray(obj, config))) 120 | } 121 | } 122 | 123 | /** 124 | * `ByteString` => `A` 125 | * 126 | * @tparam A 127 | * type to decode 128 | * @return 129 | * unmarshaller for any `A` value 130 | */ 131 | implicit def fromByteStringUnmarshaller[A](implicit 132 | codec: JsonValueCodec[A], 133 | config: ReaderConfig = defaultReaderConfig 134 | ): Unmarshaller[ByteString, A] = 135 | Unmarshaller(_ => bs => Future.fromTry(Try(readFromArray(bs.toArray, config)))) 136 | 137 | /** 138 | * HTTP entity => `Source[A, _]` 139 | * 140 | * @tparam A 141 | * type to decode 142 | * @return 143 | * unmarshaller for `Source[A, _]` 144 | */ 145 | implicit def sourceUnmarshaller[A: JsonValueCodec](implicit 146 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json(), 147 | config: ReaderConfig = defaultReaderConfig 148 | ): FromEntityUnmarshaller[SourceOf[A]] = 149 | Unmarshaller 150 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 151 | def asyncParse(bs: ByteString) = 152 | Unmarshal(bs).to[A] 153 | 154 | def ordered = 155 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 156 | 157 | def unordered = 158 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 159 | 160 | Future.successful { 161 | entity.dataBytes 162 | .via(support.framingDecoder) 163 | .via(if (support.unordered) unordered else ordered) 164 | } 165 | } 166 | .forContentTypes(unmarshallerContentTypes: _*) 167 | 168 | /** 169 | * `SourceOf[A]` => HTTP entity 170 | * 171 | * @tparam A 172 | * type to encode 173 | * @return 174 | * marshaller for any `SourceOf[A]` value 175 | */ 176 | implicit def sourceMarshaller[A](implicit 177 | codec: JsonValueCodec[A], 178 | config: WriterConfig = defaultWriterConfig, 179 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 180 | ): ToEntityMarshaller[SourceOf[A]] = 181 | jsonSourceStringMarshaller.compose(jsonSource[A]) 182 | } 183 | -------------------------------------------------------------------------------- /akka-http-jsoniter-scala/src/test/scala/de/heikoseeberger/akkahttpjsoniterscala/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjsoniterscala 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.HttpRequest 22 | import akka.http.scaladsl.server.{ Directives, Route } 23 | import akka.http.scaladsl.unmarshalling.Unmarshal 24 | import akka.stream.scaladsl.Source 25 | import scala.concurrent.Await 26 | import scala.concurrent.duration._ 27 | import scala.io.StdIn 28 | 29 | object ExampleApp { 30 | 31 | final case class Foo(bar: String) 32 | 33 | def main(args: Array[String]): Unit = { 34 | implicit val system: ActorSystem = ActorSystem() 35 | 36 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 37 | 38 | StdIn.readLine("Hit ENTER to exit") 39 | Await.ready(system.terminate(), Duration.Inf) 40 | } 41 | 42 | def route(implicit sys: ActorSystem): Route = { 43 | import Directives._ 44 | import JsoniterScalaSupport._ 45 | import com.github.plokhotnyuk.jsoniter_scala.core._ 46 | import com.github.plokhotnyuk.jsoniter_scala.macros._ 47 | 48 | // here you should provide implicit codecs for in/out messages of all routes 49 | 50 | implicit val codec: JsonValueCodec[Foo] = JsonCodecMaker.make[Foo](CodecMakerConfig) 51 | 52 | // also, you can provide an implicit reader/writer configs to override defaults: 53 | // 54 | // implicit val readerConfig = ReaderConfig.withThrowReaderExceptionWithStackTrace(true) 55 | // implicit val writerConfig = WriterConfig.withIndentionStep(2) 56 | 57 | pathSingleSlash { 58 | post { 59 | entity(as[Foo]) { foo => 60 | complete { 61 | foo 62 | } 63 | } 64 | } 65 | } ~ pathPrefix("stream") { 66 | post { 67 | entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => 68 | complete(fooSource.throttle(1, 2.seconds)) 69 | } 70 | } ~ get { 71 | pathEndOrSingleSlash { 72 | complete( 73 | Source(0 to 5) 74 | .throttle(1, 1.seconds) 75 | .map(i => Foo(s"bar-$i")) 76 | ) 77 | } ~ pathPrefix("remote") { 78 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 79 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /akka-http-jsoniter-scala/src/test/scala/de/heikoseeberger/akkahttpjsoniterscala/JsoniterScalaSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpjsoniterscala 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec 27 | import com.github.plokhotnyuk.jsoniter_scala.macros._ 28 | import org.scalatest.BeforeAndAfterAll 29 | import org.scalatest.matchers.should.Matchers 30 | import org.scalatest.wordspec.AsyncWordSpec 31 | import scala.concurrent.Await 32 | import scala.concurrent.duration.DurationInt 33 | 34 | object JsoniterScalaSupportSpec { 35 | 36 | final case class Foo(bar: String) { 37 | require(bar startsWith "bar", "bar must start with 'bar'!") 38 | } 39 | } 40 | 41 | final class JsoniterScalaSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 42 | import JsoniterScalaSupport._ 43 | import JsoniterScalaSupportSpec._ 44 | 45 | private implicit val system: ActorSystem = ActorSystem() 46 | private implicit val codec: JsonValueCodec[Foo] = JsonCodecMaker.make[Foo](CodecMakerConfig) 47 | 48 | "JsoniterScalaSupport" should { 49 | "should enable marshalling and unmarshalling" in { 50 | val foo = Foo("bar") 51 | Marshal(foo) 52 | .to[RequestEntity] 53 | .flatMap(Unmarshal(_).to[Foo]) 54 | .map(_ shouldBe foo) 55 | } 56 | 57 | "enable streamed marshalling and unmarshalling for json arrays" in { 58 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 59 | 60 | Marshal(Source(foos)) 61 | .to[RequestEntity] 62 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 63 | .flatMap(_.runWith(Sink.seq)) 64 | .map(_ shouldBe foos) 65 | } 66 | 67 | "provide proper error messages for requirement errors" in { 68 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 69 | Unmarshal(entity) 70 | .to[Foo] 71 | .failed 72 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 73 | } 74 | 75 | "fail with NoContentException when unmarshalling empty entities" in { 76 | val entity = HttpEntity.empty(`application/json`) 77 | Unmarshal(entity) 78 | .to[Foo] 79 | .failed 80 | .map(_ shouldBe Unmarshaller.NoContentException) 81 | } 82 | 83 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 84 | val entity = HttpEntity("""{ "bar": "bar" }""") 85 | Unmarshal(entity) 86 | .to[Foo] 87 | .failed 88 | .map( 89 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 90 | ) 91 | } 92 | 93 | "allow unmarshalling with passed in Content-Types" in { 94 | val foo = Foo("bar") 95 | val `application/json-home` = 96 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 97 | 98 | final object CustomJsoniterScalaSupport extends JsoniterScalaSupport { 99 | override def unmarshallerContentTypes: List[ContentTypeRange] = 100 | List(`application/json`, `application/json-home`) 101 | } 102 | import CustomJsoniterScalaSupport._ 103 | 104 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 105 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 106 | } 107 | } 108 | 109 | override protected def afterAll(): Unit = { 110 | Await.ready(system.terminate(), 42.seconds) 111 | super.afterAll() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /akka-http-ninny/src/main/scala/de/heikoseeberger/akkahttpninny/NinnySupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpninny 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } 23 | import akka.http.scaladsl.model.MediaTypes.`application/json` 24 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 25 | import akka.http.scaladsl.util.FastFuture 26 | import akka.stream.scaladsl.{ Flow, Source } 27 | import akka.util.ByteString 28 | import nrktkt.ninny._ 29 | import java.nio.charset.StandardCharsets 30 | import scala.collection.immutable.Seq 31 | import scala.concurrent.Future 32 | import scala.util.control.NonFatal 33 | 34 | trait NinnySupport { 35 | type SourceOf[A] = Source[A, _] 36 | 37 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 38 | mediaTypes.map(ContentTypeRange.apply) 39 | 40 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 41 | List(`application/json`) 42 | 43 | /** 44 | * HTTP entity => `A` 45 | * 46 | * @tparam A 47 | * type to decode 48 | * @return 49 | * unmarshaller for `A` 50 | */ 51 | implicit def unmarshaller[A: FromJson]: FromEntityUnmarshaller[A] = 52 | Unmarshaller.byteStringUnmarshaller 53 | .forContentTypes(unmarshallerContentTypes: _*) 54 | .mapWithCharset { 55 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 56 | case (data, charset) => data.decodeString(charset.nioCharset.name) 57 | } 58 | .map(Json.parse(_).to[A].get) 59 | 60 | /** 61 | * `A` => HTTP entity 62 | * 63 | * @tparam A 64 | * type to encode 65 | * @return 66 | * marshaller for any `A` value 67 | */ 68 | implicit def marshaller[A: ToSomeJson]: ToEntityMarshaller[A] = 69 | Marshaller 70 | .oneOf(mediaTypes: _*)(Marshaller.stringMarshaller) 71 | .compose(Json.render) 72 | .compose(_.toSomeJson) 73 | 74 | /** 75 | * `ByteString` => `A` 76 | * 77 | * @tparam A 78 | * type to decode 79 | * @return 80 | * unmarshaller for any `A` value 81 | */ 82 | implicit def fromByteStringUnmarshaller[A: FromJson]: Unmarshaller[ByteString, A] = 83 | Unmarshaller(_ => 84 | bs => Future.fromTry(Json.parse(new String(bs.toArray, StandardCharsets.UTF_8)).to[A]) 85 | ) 86 | 87 | /** 88 | * HTTP entity => `Source[A, _]` 89 | * 90 | * @tparam A 91 | * type to decode 92 | * @return 93 | * unmarshaller for `Source[A, _]` 94 | */ 95 | implicit def sourceUnmarshaller[A: FromJson](implicit 96 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 97 | ): FromEntityUnmarshaller[SourceOf[A]] = 98 | Unmarshaller 99 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 100 | def asyncParse(bs: ByteString) = 101 | Unmarshal(bs).to[A] 102 | 103 | def ordered = 104 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 105 | 106 | def unordered = 107 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 108 | 109 | Future.successful { 110 | entity.dataBytes 111 | .via(support.framingDecoder) 112 | .via(if (support.unordered) unordered else ordered) 113 | } 114 | } 115 | .forContentTypes(unmarshallerContentTypes: _*) 116 | 117 | /** 118 | * `SourceOf[A]` => HTTP entity 119 | * 120 | * @tparam A 121 | * type to encode 122 | * @return 123 | * marshaller for any `SourceOf[A]` value 124 | */ 125 | implicit def sourceMarshaller[A](implicit 126 | toJson: ToSomeJson[A], 127 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 128 | ): ToEntityMarshaller[SourceOf[A]] = 129 | jsonSourceStringMarshaller.compose(jsonSource[A]) 130 | 131 | private def sourceByteStringMarshaller( 132 | mediaType: MediaType.WithFixedCharset 133 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 134 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 135 | try 136 | FastFuture.successful { 137 | Marshalling.WithFixedContentType( 138 | mediaType, 139 | () => HttpEntity(contentType = mediaType, data = value) 140 | ) :: Nil 141 | } 142 | catch { 143 | case NonFatal(e) => FastFuture.failed(e) 144 | } 145 | } 146 | 147 | private lazy val jsonSourceStringMarshaller = 148 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 149 | 150 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 151 | toJson: ToSomeJson[A], 152 | support: JsonEntityStreamingSupport 153 | ): SourceOf[ByteString] = 154 | entitySource 155 | .map(_.toSomeJson) 156 | .map(Json.render) 157 | .map(ByteString(_)) 158 | .via(support.framingRenderer) 159 | 160 | } 161 | 162 | object NinnySupport extends NinnySupport 163 | -------------------------------------------------------------------------------- /akka-http-ninny/src/test/scala/de/heikoseeberger/akkahttpninny/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpninny 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.marshalling.Marshal 22 | import akka.http.scaladsl.model.{ HttpRequest, RequestEntity } 23 | import akka.http.scaladsl.server.Directives 24 | import akka.http.scaladsl.unmarshalling.Unmarshal 25 | import akka.stream.scaladsl.Source 26 | 27 | import scala.concurrent.duration._ 28 | import scala.io.StdIn 29 | import nrktkt.ninny._ 30 | 31 | object ExampleApp { 32 | 33 | private final case class Foo(bar: String) 34 | private object Foo { 35 | implicit val toJson: ToSomeJson[Foo] = foo => obj("bar" --> foo.bar) 36 | implicit val fromJson: FromJson[Foo] = FromJson.fromSome(_.bar.to[String].map(Foo(_))) 37 | } 38 | 39 | def main(args: Array[String]): Unit = { 40 | implicit val system = ActorSystem() 41 | 42 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 43 | 44 | StdIn.readLine("Hit ENTER to exit") 45 | system.terminate() 46 | } 47 | 48 | private def route(implicit sys: ActorSystem) = { 49 | import Directives._ 50 | import NinnySupport._ 51 | 52 | pathSingleSlash { 53 | post { 54 | entity(as[Foo]) { foo => 55 | complete { 56 | foo 57 | } 58 | } 59 | } 60 | } ~ pathPrefix("stream") { 61 | post { 62 | entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => 63 | import sys._ 64 | 65 | Marshal(Source.single(Foo("a"))).to[RequestEntity] 66 | 67 | complete(fooSource.throttle(1, 2.seconds)) 68 | } 69 | } ~ get { 70 | pathEndOrSingleSlash { 71 | complete( 72 | Source(0 to 5) 73 | .throttle(1, 1.seconds) 74 | .map(i => Foo(s"bar-$i")) 75 | ) 76 | } ~ pathPrefix("remote") { 77 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 78 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /akka-http-ninny/src/test/scala/de/heikoseeberger/akkahttpninny/NinnySupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpninny 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import nrktkt.ninny._ 27 | import org.scalatest.BeforeAndAfterAll 28 | import org.scalatest.matchers.should.Matchers 29 | import org.scalatest.wordspec.AsyncWordSpec 30 | import scala.concurrent.Await 31 | import scala.concurrent.duration.DurationInt 32 | 33 | object NinnySupportSpec { 34 | 35 | final case class Foo(bar: String) { 36 | require(bar startsWith "bar", "bar must start with 'bar'!") 37 | } 38 | private object Foo { 39 | implicit val toJson: ToSomeJson[Foo] = foo => obj("bar" --> foo.bar) 40 | implicit val fromJson: FromJson[Foo] = FromJson.fromSome(_.bar.to[String].map(Foo(_))) 41 | } 42 | } 43 | 44 | final class NinnySupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 45 | import NinnySupport._ 46 | import NinnySupportSpec._ 47 | 48 | private implicit val system: ActorSystem = ActorSystem() 49 | 50 | "NinnySupport" should { 51 | "enable marshalling and unmarshalling objects for which `ToJson` and `FromSomeJson` exist" in { 52 | val foo = Foo("bar") 53 | Marshal(foo) 54 | .to[RequestEntity] 55 | .flatMap(Unmarshal(_).to[Foo]) 56 | .map(_ shouldBe foo) 57 | } 58 | 59 | "enable streamed marshalling and unmarshalling for json arrays" in { 60 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 61 | 62 | Marshal(Source(foos)) 63 | .to[RequestEntity] 64 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 65 | .flatMap(_.runWith(Sink.seq)) 66 | .map(_ shouldBe foos) 67 | } 68 | 69 | "provide proper error messages for requirement errors" in { 70 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 71 | Unmarshal(entity) 72 | .to[Foo] 73 | .failed 74 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 75 | } 76 | 77 | "provide stringified error representation for parsing errors" in { 78 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": 5 }""") 79 | Unmarshal(entity) 80 | .to[Foo] 81 | .failed 82 | .map { err => 83 | err shouldBe a[JsonException] 84 | err should have message "Expected string, got 5" 85 | } 86 | } 87 | 88 | "fail with NoContentException when unmarshalling empty entities" in { 89 | val entity = HttpEntity.empty(`application/json`) 90 | Unmarshal(entity) 91 | .to[Foo] 92 | .failed 93 | .map(_ shouldBe Unmarshaller.NoContentException) 94 | } 95 | 96 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 97 | val entity = HttpEntity("""{ "bar": "bar" }""") 98 | Unmarshal(entity) 99 | .to[Foo] 100 | .failed 101 | .map( 102 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 103 | ) 104 | } 105 | 106 | "allow unmarshalling with passed in Content-Types" in { 107 | val foo = Foo("bar") 108 | val `application/json-home` = 109 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 110 | 111 | object CustomNinnySupport extends NinnySupport { 112 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 113 | } 114 | import CustomNinnySupport._ 115 | 116 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 117 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 118 | } 119 | } 120 | 121 | override protected def afterAll() = { 122 | Await.ready(system.terminate(), 42.seconds) 123 | super.afterAll() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /akka-http-play-json/src/main/scala/de/heikoseeberger/akkahttpplayjson/PlayJsonSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpplayjson 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling._ 22 | import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } 23 | import akka.http.scaladsl.model.MediaTypes.`application/json` 24 | import akka.http.scaladsl.unmarshalling._ 25 | import akka.http.scaladsl.util.FastFuture 26 | import akka.stream.scaladsl.{ Flow, Source } 27 | import akka.util.ByteString 28 | import play.api.libs.json.{ JsError, JsResultException, JsValue, Json, Reads, Writes } 29 | import scala.collection.immutable.Seq 30 | import scala.concurrent.Future 31 | import scala.util.Try 32 | import scala.util.control.NonFatal 33 | 34 | /** 35 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope *play-json* protocol. 36 | */ 37 | object PlayJsonSupport extends PlayJsonSupport { 38 | final case class PlayJsonError(error: JsError) extends IllegalArgumentException { 39 | override def getMessage: String = 40 | JsError.toJson(error).toString() 41 | } 42 | } 43 | 44 | /** 45 | * Automatic to and from JSON marshalling/unmarshalling using an in-scope *play-json* protocol. 46 | */ 47 | trait PlayJsonSupport { 48 | type SourceOf[A] = Source[A, _] 49 | 50 | import PlayJsonSupport._ 51 | 52 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 53 | mediaTypes.map(ContentTypeRange.apply) 54 | 55 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 56 | List(`application/json`) 57 | 58 | private val jsonStringUnmarshaller = 59 | Unmarshaller.byteStringUnmarshaller 60 | .forContentTypes(unmarshallerContentTypes: _*) 61 | .mapWithCharset { 62 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 63 | case (data, charset) => data.decodeString(charset.nioCharset.name) 64 | } 65 | 66 | private def sourceByteStringMarshaller( 67 | mediaType: MediaType.WithFixedCharset 68 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 69 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 70 | try 71 | FastFuture.successful { 72 | Marshalling.WithFixedContentType( 73 | mediaType, 74 | () => HttpEntity(contentType = mediaType, data = value) 75 | ) :: Nil 76 | } 77 | catch { 78 | case NonFatal(e) => FastFuture.failed(e) 79 | } 80 | } 81 | 82 | private val jsonSourceStringMarshaller = 83 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 84 | 85 | private val jsonStringMarshaller = 86 | Marshaller.oneOf(mediaTypes: _*)(Marshaller.stringMarshaller) 87 | 88 | private def read[A: Reads](json: JsValue): A = 89 | implicitly[Reads[A]] 90 | .reads(json) 91 | .recoverTotal(e => throw PlayJsonError(e)) 92 | 93 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 94 | writes: Writes[A], 95 | printer: JsValue => String, 96 | support: JsonEntityStreamingSupport 97 | ): SourceOf[ByteString] = 98 | entitySource 99 | .map(writes.writes) 100 | .map(printer) 101 | .map(ByteString(_)) 102 | .via(support.framingRenderer) 103 | 104 | /** 105 | * HTTP entity => `A` 106 | * 107 | * @tparam A 108 | * type to decode 109 | * @return 110 | * unmarshaller for `A` 111 | */ 112 | implicit def unmarshaller[A: Reads]: FromEntityUnmarshaller[A] = 113 | jsonStringUnmarshaller.map(data => read(Json.parse(data))) 114 | 115 | /** 116 | * `A` => HTTP entity 117 | * 118 | * @tparam A 119 | * type to encode 120 | * @return 121 | * marshaller for any `A` value 122 | */ 123 | implicit def marshaller[A](implicit 124 | writes: Writes[A], 125 | printer: JsValue => String = Json.prettyPrint 126 | ): ToEntityMarshaller[A] = 127 | jsonStringMarshaller.compose(printer).compose(writes.writes) 128 | 129 | /** 130 | * `ByteString` => `A` 131 | * 132 | * @tparam A 133 | * type to decode 134 | * @return 135 | * unmarshaller for any `A` value 136 | */ 137 | implicit def fromByteStringUnmarshaller[A: Reads]: Unmarshaller[ByteString, A] = 138 | Unmarshaller(_ => bs => Future.fromTry(Try(Json.parse(bs.toArray).as[A]))) 139 | 140 | /** 141 | * HTTP entity => `Source[A, _]` 142 | * 143 | * @tparam A 144 | * type to decode 145 | * @return 146 | * unmarshaller for `Source[A, _]` 147 | */ 148 | implicit def sourceUnmarshaller[A: Reads](implicit 149 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 150 | ): FromEntityUnmarshaller[SourceOf[A]] = 151 | Unmarshaller 152 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 153 | def asyncParse(bs: ByteString) = 154 | Unmarshal(bs).to[A] 155 | 156 | def ordered = 157 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 158 | 159 | def unordered = 160 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 161 | 162 | Future.successful { 163 | entity.dataBytes 164 | .via(support.framingDecoder) 165 | .via(if (support.unordered) unordered else ordered) 166 | .recoverWithRetries( 167 | 1, 168 | { case a: JsResultException => 169 | Source.failed(PlayJsonError(JsError(a.errors))) 170 | } 171 | ) 172 | } 173 | } 174 | .forContentTypes(unmarshallerContentTypes: _*) 175 | 176 | /** 177 | * `SourceOf[A]` => HTTP entity 178 | * 179 | * @tparam A 180 | * type to encode 181 | * @return 182 | * marshaller for any `SourceOf[A]` value 183 | */ 184 | implicit def sourceMarshaller[A](implicit 185 | writes: Writes[A], 186 | printer: JsValue => String = Json.stringify, 187 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 188 | ): ToEntityMarshaller[SourceOf[A]] = 189 | jsonSourceStringMarshaller.compose(jsonSource[A]) 190 | } 191 | -------------------------------------------------------------------------------- /akka-http-play-json/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.http.server.parsing.max-content-length=11138888914 -------------------------------------------------------------------------------- /akka-http-play-json/src/test/scala/de/heikoseeberger/akkahttpplayjson/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpplayjson 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.HttpRequest 22 | import akka.http.scaladsl.server.Directives 23 | import akka.http.scaladsl.unmarshalling.Unmarshal 24 | import akka.stream.scaladsl.Source 25 | import play.api.libs.json.{ Format, JsValue, Json } 26 | import scala.concurrent.Await 27 | import scala.concurrent.duration._ 28 | import scala.io.StdIn 29 | 30 | object ExampleApp { 31 | 32 | final object Foo { 33 | implicit val fooFormat: Format[Foo] = Json.format[Foo] 34 | } 35 | final case class Foo(bar: String) 36 | 37 | def main(args: Array[String]): Unit = { 38 | implicit val system = ActorSystem() 39 | 40 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 41 | 42 | StdIn.readLine("Hit ENTER to exit") 43 | Await.ready(system.terminate(), Duration.Inf) 44 | } 45 | 46 | def route(implicit sys: ActorSystem) = { 47 | import Directives._ 48 | import PlayJsonSupport._ 49 | 50 | implicit val prettyPrint: JsValue => String = Json.prettyPrint 51 | 52 | pathSingleSlash { 53 | post { 54 | entity(as[Foo]) { foo => 55 | complete { 56 | foo 57 | } 58 | } 59 | } 60 | } ~ pathPrefix("stream") { 61 | post { 62 | entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => 63 | complete(fooSource.throttle(1, 2.seconds)) 64 | } 65 | } ~ get { 66 | pathEndOrSingleSlash { 67 | complete( 68 | Source(0 to 5) 69 | .throttle(1, 1.seconds) 70 | .map(i => Foo(s"bar-$i")) 71 | ) 72 | } ~ pathPrefix("remote") { 73 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 74 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /akka-http-play-json/src/test/scala/de/heikoseeberger/akkahttpplayjson/PlayJsonSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpplayjson 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import org.scalatest.BeforeAndAfterAll 27 | import org.scalatest.matchers.should.Matchers 28 | import org.scalatest.wordspec.AsyncWordSpec 29 | import play.api.libs.json.{ Format, Json } 30 | import scala.collection.immutable.Seq 31 | import scala.concurrent.Await 32 | import scala.concurrent.duration.DurationInt 33 | 34 | object PlayJsonSupportSpec { 35 | 36 | final case class Foo(bar: String) { 37 | require(bar startsWith "bar", "bar must start with 'bar'!") 38 | } 39 | 40 | implicit val fooFormat: Format[Foo] = 41 | Json.format[Foo] 42 | } 43 | 44 | final class PlayJsonSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 45 | import PlayJsonSupport._ 46 | import PlayJsonSupportSpec._ 47 | 48 | private implicit val system = ActorSystem() 49 | 50 | "PlayJsonSupport" should { 51 | "enable marshalling and unmarshalling objects for which `Writes` and `Reads` exist" in { 52 | val foo = Foo("bar") 53 | Marshal(foo) 54 | .to[RequestEntity] 55 | .flatMap(Unmarshal(_).to[Foo]) 56 | .map(_ shouldBe foo) 57 | } 58 | 59 | "enable streamed marshalling and unmarshalling for json arrays" in { 60 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 61 | 62 | Marshal(Source(foos)) 63 | .to[RequestEntity] 64 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 65 | .flatMap(_.runWith(Sink.seq)) 66 | .map(_ shouldBe foos) 67 | } 68 | 69 | "provide proper error messages for requirement errors" in { 70 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 71 | Unmarshal(entity) 72 | .to[Foo] 73 | .failed 74 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 75 | } 76 | 77 | "provide stringified error representation for parsing errors" in { 78 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": 5 }""") 79 | Unmarshal(entity) 80 | .to[Foo] 81 | .failed 82 | .map { err => 83 | err shouldBe a[PlayJsonError] 84 | err should have message """{"obj.bar":[{"msg":["error.expected.jsstring"],"args":[]}]}""" 85 | val errors = err.asInstanceOf[PlayJsonError].error.errors 86 | errors should have length 1 87 | errors.head._1.toString should be("/bar") 88 | errors.head._2.flatMap(_.messages) should be(Seq("error.expected.jsstring")) 89 | } 90 | } 91 | 92 | "fail with NoContentException when unmarshalling empty entities" in { 93 | val entity = HttpEntity.empty(`application/json`) 94 | Unmarshal(entity) 95 | .to[Foo] 96 | .failed 97 | .map(_ shouldBe Unmarshaller.NoContentException) 98 | } 99 | 100 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 101 | val entity = HttpEntity("""{ "bar": "bar" }""") 102 | Unmarshal(entity) 103 | .to[Foo] 104 | .failed 105 | .map( 106 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 107 | ) 108 | } 109 | 110 | "allow unmarshalling with passed in Content-Types" in { 111 | val foo = Foo("bar") 112 | val `application/json-home` = 113 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 114 | 115 | final object CustomPlayJsonSupport extends PlayJsonSupport { 116 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 117 | } 118 | import CustomPlayJsonSupport._ 119 | 120 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 121 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 122 | } 123 | } 124 | 125 | override protected def afterAll() = { 126 | Await.ready(system.terminate(), 42.seconds) 127 | super.afterAll() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpupickle 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } 23 | import akka.http.scaladsl.model.MediaTypes.`application/json` 24 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 25 | import akka.http.scaladsl.util.FastFuture 26 | import akka.stream.scaladsl.{ Flow, Source } 27 | import akka.util.ByteString 28 | import de.heikoseeberger.akkahttpupickle.UpickleCustomizationSupport._ 29 | import scala.collection.immutable.Seq 30 | import scala.concurrent.Future 31 | import scala.util.Try 32 | import scala.util.control.NonFatal 33 | 34 | // This companion object only exists for binary compatibility as adding methods with default implementations 35 | // (including val's as they create synthetic methods) is not compatible. 36 | private object UpickleCustomizationSupport { 37 | 38 | private def jsonStringUnmarshaller(support: UpickleCustomizationSupport) = 39 | Unmarshaller.byteStringUnmarshaller 40 | .forContentTypes(support.unmarshallerContentTypes: _*) 41 | .mapWithCharset { 42 | case (ByteString.empty, _) => throw Unmarshaller.NoContentException 43 | case (data, charset) => data.decodeString(charset.nioCharset.name) 44 | } 45 | 46 | private def jsonSourceStringMarshaller(support: UpickleCustomizationSupport) = 47 | Marshaller.oneOf(support.mediaTypes: _*)(support.sourceByteStringMarshaller) 48 | 49 | private def jsonStringMarshaller(support: UpickleCustomizationSupport) = 50 | Marshaller.oneOf(support.mediaTypes: _*)(Marshaller.stringMarshaller) 51 | } 52 | 53 | /** 54 | * Automatic to and from JSON marshalling/unmarshalling using *upickle* protocol. 55 | */ 56 | trait UpickleCustomizationSupport { 57 | type SourceOf[A] = Source[A, _] 58 | 59 | type Api <: upickle.Api 60 | 61 | def api: Api 62 | 63 | private lazy val apiInstance: Api = api 64 | 65 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 66 | mediaTypes.map(ContentTypeRange.apply) 67 | 68 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 69 | List(`application/json`) 70 | 71 | private def sourceByteStringMarshaller( 72 | mediaType: MediaType.WithFixedCharset 73 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 74 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 75 | try 76 | FastFuture.successful { 77 | Marshalling.WithFixedContentType( 78 | mediaType, 79 | () => HttpEntity(contentType = mediaType, data = value) 80 | ) :: Nil 81 | } 82 | catch { 83 | case NonFatal(e) => FastFuture.failed(e) 84 | } 85 | } 86 | 87 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 88 | writes: apiInstance.Writer[A], 89 | support: JsonEntityStreamingSupport 90 | ): SourceOf[ByteString] = 91 | entitySource 92 | .map(apiInstance.write(_)) 93 | .map(ByteString(_)) 94 | .via(support.framingRenderer) 95 | 96 | /** 97 | * `ByteString` => `A` 98 | * 99 | * @tparam A 100 | * type to decode 101 | * @return 102 | * unmarshaller for any `A` value 103 | */ 104 | implicit def fromByteStringUnmarshaller[A: apiInstance.Reader]: Unmarshaller[ByteString, A] = 105 | Unmarshaller(_ => bs => Future.fromTry(Try(apiInstance.read(bs.toArray)))) 106 | 107 | /** 108 | * HTTP entity => `A` 109 | * 110 | * @tparam A 111 | * type to decode 112 | * @return 113 | * unmarshaller for `A` 114 | */ 115 | implicit def unmarshaller[A: apiInstance.Reader]: FromEntityUnmarshaller[A] = 116 | jsonStringUnmarshaller(this).map(apiInstance.read(_)) 117 | 118 | /** 119 | * `A` => HTTP entity 120 | * 121 | * @tparam A 122 | * type to encode 123 | * @return 124 | * marshaller for any `A` value 125 | */ 126 | implicit def marshaller[A: apiInstance.Writer]: ToEntityMarshaller[A] = 127 | jsonStringMarshaller(this).compose(apiInstance.write(_)) 128 | 129 | /** 130 | * HTTP entity => `Source[A, _]` 131 | * 132 | * @tparam A 133 | * type to decode 134 | * @return 135 | * unmarshaller for `Source[A, _]` 136 | */ 137 | implicit def sourceUnmarshaller[A: apiInstance.Reader](implicit 138 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 139 | ): FromEntityUnmarshaller[SourceOf[A]] = 140 | Unmarshaller 141 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 142 | def asyncParse(bs: ByteString) = 143 | Unmarshal(bs).to[A] 144 | 145 | def ordered = 146 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 147 | 148 | def unordered = 149 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 150 | 151 | Future.successful { 152 | entity.dataBytes 153 | .via(support.framingDecoder) 154 | .via(if (support.unordered) unordered else ordered) 155 | } 156 | } 157 | .forContentTypes(unmarshallerContentTypes: _*) 158 | 159 | /** 160 | * `SourceOf[A]` => HTTP entity 161 | * 162 | * @tparam A 163 | * type to encode 164 | * @return 165 | * marshaller for any `SourceOf[A]` value 166 | */ 167 | implicit def sourceMarshaller[A](implicit 168 | writes: apiInstance.Writer[A], 169 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 170 | ): ToEntityMarshaller[SourceOf[A]] = 171 | jsonSourceStringMarshaller(this).compose(jsonSource[A]) 172 | } 173 | -------------------------------------------------------------------------------- /akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpupickle 18 | 19 | /** 20 | * Automatic to and from JSON marshalling/unmarshalling using *upickle* protocol. 21 | */ 22 | object UpickleSupport extends UpickleSupport 23 | 24 | /** 25 | * Automatic to and from JSON marshalling/unmarshalling using *upickle* protocol. 26 | */ 27 | trait UpickleSupport extends UpickleCustomizationSupport { 28 | 29 | override type Api = upickle.default.type 30 | 31 | override def api: Api = upickle.default 32 | } 33 | -------------------------------------------------------------------------------- /akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpupickle 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.HttpRequest 22 | import akka.http.scaladsl.server.Directives 23 | import akka.http.scaladsl.unmarshalling.Unmarshal 24 | import akka.stream.scaladsl.Source 25 | import scala.concurrent.Await 26 | import scala.concurrent.duration._ 27 | import scala.io.StdIn 28 | import upickle.default.{ ReadWriter, macroRW } 29 | 30 | object ExampleApp { 31 | 32 | final object Foo { 33 | implicit val rw: ReadWriter[Foo] = macroRW 34 | } 35 | 36 | final case class Foo(bar: String) 37 | 38 | def main(args: Array[String]): Unit = { 39 | implicit val system = ActorSystem() 40 | 41 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 42 | 43 | StdIn.readLine("Hit ENTER to exit") 44 | Await.ready(system.terminate(), Duration.Inf) 45 | } 46 | 47 | def route(implicit system: ActorSystem) = { 48 | import Directives._ 49 | import UpickleSupport._ 50 | 51 | pathSingleSlash { 52 | post { 53 | entity(as[Foo]) { foo => 54 | complete { 55 | foo 56 | } 57 | } 58 | } 59 | } ~ pathPrefix("stream") { 60 | post { 61 | entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => 62 | complete(fooSource.throttle(1, 2.seconds)) 63 | } 64 | } ~ get { 65 | pathEndOrSingleSlash { 66 | complete( 67 | Source(0 to 5) 68 | .throttle(1, 1.seconds) 69 | .map(i => Foo(s"bar-$i")) 70 | ) 71 | } ~ pathPrefix("remote") { 72 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 73 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpupickle 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.unmarshalling.Unmarshal 23 | import org.scalatest.BeforeAndAfterAll 24 | import org.scalatest.matchers.should.Matchers 25 | import org.scalatest.wordspec.AsyncWordSpec 26 | import scala.concurrent.Await 27 | import scala.concurrent.duration.DurationInt 28 | import upickle.AttributeTagged 29 | import upickle.core.Visitor 30 | 31 | final class UpickleCustomizationSupportSpec 32 | extends AsyncWordSpec 33 | with Matchers 34 | with BeforeAndAfterAll { 35 | 36 | private implicit val system = ActorSystem() 37 | 38 | object FooApi extends AttributeTagged { 39 | override implicit val IntWriter: FooApi.Writer[Int] = new Writer[Int] { 40 | override def write0[V](out: Visitor[_, V], v: Int): V = out.visitString("foo", -1) 41 | } 42 | } 43 | object UpickleFoo extends UpickleCustomizationSupport { 44 | override type Api = FooApi.type 45 | override def api: FooApi.type = FooApi 46 | } 47 | 48 | import UpickleFoo._ 49 | 50 | "UpickleCustomizationSupport" should { 51 | "support custom configuration" in { 52 | Marshal(123) 53 | .to[RequestEntity] 54 | .flatMap(Unmarshal(_).to[String]) 55 | .map(_ shouldBe "foo") 56 | } 57 | } 58 | 59 | override protected def afterAll() = { 60 | Await.ready(system.terminate(), 42.seconds) 61 | super.afterAll() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpupickle 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 22 | import akka.http.scaladsl.model._ 23 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 24 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import org.scalatest.BeforeAndAfterAll 27 | 28 | import scala.concurrent.Await 29 | import scala.concurrent.duration.DurationInt 30 | import upickle.default.{ ReadWriter, macroRW } 31 | import org.scalatest.matchers.should.Matchers 32 | import org.scalatest.wordspec.AsyncWordSpec 33 | 34 | object UpickleSupportSpec { 35 | 36 | final object Foo { 37 | implicit val rw: ReadWriter[Foo] = macroRW 38 | } 39 | 40 | final case class Foo(bar: String) { 41 | require(bar startsWith "bar", "bar must start with 'bar'!") 42 | } 43 | } 44 | 45 | final class UpickleSupportSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 46 | import UpickleSupport._ 47 | import UpickleSupportSpec._ 48 | 49 | private implicit val system = ActorSystem() 50 | 51 | "UpickleSupport" should { 52 | "enable marshalling and unmarshalling of case classes" in { 53 | val foo = Foo("bar") 54 | Marshal(foo) 55 | .to[RequestEntity] 56 | .flatMap(Unmarshal(_).to[Foo]) 57 | .map(_ shouldBe foo) 58 | } 59 | 60 | "enable streamed marshalling and unmarshalling for json arrays" in { 61 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 62 | 63 | Marshal(Source(foos)) 64 | .to[RequestEntity] 65 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 66 | .flatMap(_.runWith(Sink.seq)) 67 | .map(_ shouldBe foos) 68 | } 69 | 70 | "provide proper error messages for requirement errors" in { 71 | val entity = HttpEntity(MediaTypes.`application/json`, """{ "bar": "baz" }""") 72 | Unmarshal(entity) 73 | .to[Foo] 74 | .failed 75 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 76 | } 77 | 78 | "fail with NoContentException when unmarshalling empty entities" in { 79 | val entity = HttpEntity.empty(`application/json`) 80 | Unmarshal(entity) 81 | .to[Foo] 82 | .failed 83 | .map(_ shouldBe Unmarshaller.NoContentException) 84 | } 85 | 86 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 87 | val entity = HttpEntity("""{ "bar": "bar" }""") 88 | Unmarshal(entity) 89 | .to[Foo] 90 | .failed 91 | .map( 92 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 93 | ) 94 | } 95 | 96 | "allow unmarshalling with passed in Content-Types" in { 97 | val foo = Foo("bar") 98 | val `application/json-home` = 99 | MediaType.applicationWithFixedCharset("json-home", HttpCharsets.`UTF-8`, "json-home") 100 | 101 | final object CustomUpickleSupport extends UpickleSupport { 102 | override def unmarshallerContentTypes = List(`application/json`, `application/json-home`) 103 | } 104 | import CustomUpickleSupport._ 105 | 106 | val entity = HttpEntity(`application/json-home`, """{ "bar": "bar" }""") 107 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 108 | } 109 | } 110 | 111 | override protected def afterAll() = { 112 | Await.ready(system.terminate(), 42.seconds) 113 | super.afterAll() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /akka-http-zio-json/src/main/scala/de/heikoseeberger/akkahttpziojson/ZioJsonSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpziojson 18 | 19 | import akka.http.javadsl.common.JsonEntityStreamingSupport 20 | import akka.http.scaladsl.common.EntityStreamingSupport 21 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling, ToEntityMarshaller } 22 | import akka.http.scaladsl.model.{ 23 | ContentType, 24 | ContentTypeRange, 25 | HttpEntity, 26 | MediaType, 27 | MessageEntity 28 | } 29 | import akka.http.scaladsl.model.MediaTypes.`application/json` 30 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } 31 | import akka.http.scaladsl.util.FastFuture 32 | import akka.stream.scaladsl.{ Flow, Source } 33 | import akka.util.ByteString 34 | import scala.collection.immutable.Seq 35 | import scala.concurrent.Future 36 | import scala.util.control.NonFatal 37 | import zio.json._ 38 | import zio.stream.ZStream 39 | import zio.Unsafe 40 | 41 | object ZioJsonSupport extends ZioJsonSupport 42 | 43 | /** 44 | * JSON marshalling/unmarshalling using zio-json codec implicits. 45 | * 46 | * The marshaller writes `A` to JSON `HTTPEntity`. 47 | * 48 | * The unmarshaller follows zio-json's early exit strategy, attempting to reading JSON to an `A`. 49 | * 50 | * A safe unmarshaller is provided to attempt reading JSON to an `Either[String, A]` instead. 51 | * 52 | * No intermediate JSON representation as per zio-json's design. 53 | */ 54 | trait ZioJsonSupport { 55 | type SourceOf[A] = Source[A, _] 56 | 57 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 58 | mediaTypes.map(ContentTypeRange.apply) 59 | 60 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 61 | List(`application/json`) 62 | 63 | private def sourceByteStringMarshaller( 64 | mediaType: MediaType.WithFixedCharset 65 | ): Marshaller[SourceOf[ByteString], MessageEntity] = 66 | Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value => 67 | try 68 | FastFuture.successful { 69 | Marshalling.WithFixedContentType( 70 | mediaType, 71 | () => HttpEntity(contentType = mediaType, data = value) 72 | ) :: Nil 73 | } 74 | catch { 75 | case NonFatal(e) => FastFuture.failed(e) 76 | } 77 | } 78 | 79 | private val jsonSourceStringMarshaller = 80 | Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller) 81 | 82 | private def jsonSource[A](entitySource: SourceOf[A])(implicit 83 | encoder: JsonEncoder[A], 84 | support: JsonEntityStreamingSupport 85 | ): SourceOf[ByteString] = 86 | entitySource 87 | .map(_.toJson) 88 | .map(ByteString(_)) 89 | .via(support.framingRenderer) 90 | 91 | /** 92 | * `ByteString` => `A` 93 | * 94 | * @tparam A 95 | * type to decode 96 | * @return 97 | */ 98 | implicit final def fromByteStringUnmarshaller[A](implicit 99 | jd: JsonDecoder[A], 100 | rt: zio.Runtime[Any] 101 | ): Unmarshaller[ByteString, A] = 102 | Unmarshaller(_ => 103 | bs => { 104 | val decoded = jd.decodeJsonStreamInput(ZStream.fromIterable(bs)) 105 | Unsafe.unsafeCompat(implicit u => rt.unsafe.runToFuture(decoded)) 106 | } 107 | ) 108 | 109 | /** 110 | * `A` => HTTP entity 111 | * 112 | * @tparam A 113 | * type to encode 114 | * @return 115 | * marshaller for any `A` value 116 | */ 117 | implicit final def marshaller[A: JsonEncoder]: ToEntityMarshaller[A] = 118 | Marshaller.oneOf(mediaTypes: _*) { mediaType => 119 | Marshaller.withFixedContentType(ContentType(mediaType)) { a => 120 | HttpEntity(mediaType, ByteString(a.toJson)) 121 | } 122 | } 123 | 124 | /** 125 | * HTTPEntity => `A` 126 | * 127 | * @tparam A 128 | * type to decode 129 | * @return 130 | * unmarshaller for `A` 131 | */ 132 | implicit final def unmarshaller[A: JsonDecoder, RT: zio.Runtime]: FromEntityUnmarshaller[A] = 133 | Unmarshaller.byteStringUnmarshaller 134 | .forContentTypes(unmarshallerContentTypes: _*) 135 | .flatMap { implicit ec => implicit m => 136 | { 137 | case ByteString.empty => throw Unmarshaller.NoContentException 138 | case data => 139 | val marshaller = fromByteStringUnmarshaller 140 | marshaller(data) 141 | } 142 | } 143 | 144 | /** 145 | * HTTP entity => `Source[A, _]` 146 | * 147 | * @tparam A 148 | * type to decode 149 | * @return 150 | * unmarshaller from `Source[A, _]` 151 | */ 152 | implicit final def sourceUnmarshaller[A: JsonDecoder, RT: zio.Runtime](implicit 153 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 154 | ): FromEntityUnmarshaller[SourceOf[A]] = 155 | Unmarshaller 156 | .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity => 157 | def asyncParse(bs: ByteString) = 158 | Unmarshal(bs).to[A] 159 | 160 | def ordered = 161 | Flow[ByteString].mapAsync(support.parallelism)(asyncParse) 162 | 163 | def unordered = 164 | Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse) 165 | 166 | Future.successful { 167 | entity.dataBytes 168 | .via(support.framingDecoder) 169 | .via(if (support.unordered) unordered else ordered) 170 | } 171 | } 172 | .forContentTypes(unmarshallerContentTypes: _*) 173 | 174 | /** 175 | * `SourceOf[A]` => HTTP entity 176 | * 177 | * @tparam A 178 | * type to encode 179 | * @return 180 | * marshaller for any `SourceOf[A]` value 181 | */ 182 | implicit final def sourceMarshaller[A](implicit 183 | writes: JsonEncoder[A], 184 | support: JsonEntityStreamingSupport = EntityStreamingSupport.json() 185 | ): ToEntityMarshaller[SourceOf[A]] = 186 | jsonSourceStringMarshaller.compose(jsonSource[A]) 187 | 188 | implicit final def safeUnmarshaller[A: JsonDecoder]: FromEntityUnmarshaller[Either[String, A]] = 189 | Unmarshaller.stringUnmarshaller 190 | .forContentTypes(unmarshallerContentTypes: _*) 191 | .map(_.fromJson) 192 | } 193 | -------------------------------------------------------------------------------- /akka-http-zio-json/src/test/scala/de/heikoseeberger/akkahttpziojson/ExampleApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpziojson 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.marshalling.Marshal 22 | import akka.http.scaladsl.model.{ HttpRequest, RequestEntity } 23 | import akka.http.scaladsl.server.Directives 24 | import akka.http.scaladsl.unmarshalling.Unmarshal 25 | import akka.stream.scaladsl.Source 26 | import scala.concurrent.duration._ 27 | import scala.io.StdIn 28 | import zio.json._ 29 | 30 | object ExampleApp { 31 | private final case class Foo(bar: String) 32 | private object Foo { 33 | implicit val fooEncoder: JsonEncoder[Foo] = DeriveJsonEncoder.gen 34 | implicit val fooDecoder: JsonDecoder[Foo] = DeriveJsonDecoder.gen 35 | } 36 | 37 | private implicit val rt: zio.Runtime[Any] = zio.Runtime.default 38 | 39 | def main(args: Array[String]): Unit = { 40 | implicit val system = ActorSystem() 41 | 42 | Http().newServerAt("127.0.0.1", 8000).bindFlow(route) 43 | 44 | StdIn.readLine("Hit ENTER to exit") 45 | system.terminate() 46 | } 47 | 48 | private def route(implicit sys: ActorSystem) = { 49 | import Directives._ 50 | import ZioJsonSupport._ 51 | 52 | pathSingleSlash { 53 | post { 54 | entity(as[Foo]) { foo => 55 | complete { 56 | foo 57 | } 58 | } 59 | } 60 | } ~ pathPrefix("stream") { 61 | post { 62 | entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => 63 | import sys._ 64 | 65 | Marshal(Source.single(Foo("a"))).to[RequestEntity] 66 | 67 | complete(fooSource.throttle(1, 2.seconds)) 68 | } 69 | } ~ get { 70 | pathEndOrSingleSlash { 71 | complete( 72 | Source(0 to 5) 73 | .throttle(1, 1.seconds) 74 | .map(i => Foo(s"bar-$i")) 75 | ) 76 | } ~ pathPrefix("remote") { 77 | onSuccess(Http().singleRequest(HttpRequest(uri = "http://localhost:8000/stream"))) { 78 | response => complete(Unmarshal(response).to[SourceOf[Foo]]) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /akka-http-zio-json/src/test/scala/de/heikoseeberger/akkahttpziojson/ZioJsonSupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Heiko Seeberger 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.heikoseeberger.akkahttpziojson 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.marshalling.Marshal 21 | import akka.http.scaladsl.model.{ HttpEntity, RequestEntity, ResponseEntity } 22 | import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } 23 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 24 | import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException 25 | import akka.stream.scaladsl.{ Sink, Source } 26 | import org.scalatest.{ BeforeAndAfterAll, EitherValues } 27 | import org.scalatest.concurrent.ScalaFutures 28 | import org.scalatest.matchers.should.Matchers 29 | import org.scalatest.wordspec.AsyncWordSpec 30 | import scala.concurrent.Await 31 | import scala.concurrent.duration.DurationInt 32 | import zio.json._ 33 | 34 | object ZioJsonSupportSpec { 35 | 36 | final case class Foo(bar: String) { 37 | require(bar startsWith "bar", "bar must start with 'bar'!") 38 | } 39 | 40 | final case class MultiFoo(a: String, b: String) 41 | 42 | final case class OptionFoo(a: Option[String]) 43 | 44 | implicit val fooEncoder: JsonEncoder[Foo] = DeriveJsonEncoder.gen 45 | implicit val multiFooEncoder: JsonEncoder[MultiFoo] = DeriveJsonEncoder.gen 46 | implicit val optionFooEncoder: JsonEncoder[OptionFoo] = DeriveJsonEncoder.gen 47 | 48 | implicit val fooDecoder: JsonDecoder[Foo] = DeriveJsonDecoder.gen 49 | implicit val multiFooDecoder: JsonDecoder[MultiFoo] = DeriveJsonDecoder.gen 50 | implicit val optionFooDecoder: JsonDecoder[OptionFoo] = DeriveJsonDecoder.gen 51 | 52 | implicit val rt: zio.Runtime[Any] = zio.Runtime.default 53 | } 54 | 55 | final class ZioJsonSupportSpec 56 | extends AsyncWordSpec 57 | with Matchers 58 | with BeforeAndAfterAll 59 | with ScalaFutures 60 | with EitherValues { 61 | import ZioJsonSupportSpec._ 62 | 63 | private implicit val system: ActorSystem = ActorSystem() 64 | 65 | "ZioJsonSupport" should { 66 | import ZioJsonSupport._ 67 | 68 | "enable marshalling and unmarshalling objects for generic derivation" in { 69 | val foo = Foo("bar") 70 | Marshal(foo) 71 | .to[RequestEntity] 72 | .flatMap(Unmarshal(_).to[Foo]) 73 | .map(_ shouldBe foo) 74 | } 75 | 76 | "enable streamed marshalling and unmarshalling for json arrays" in { 77 | val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList 78 | 79 | Marshal(Source(foos)) 80 | .to[ResponseEntity] 81 | .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) 82 | .flatMap(_.runWith(Sink.seq)) 83 | .map(_ shouldBe foos) 84 | } 85 | 86 | "provide proper error messages for requirement errors" in { 87 | val entity = HttpEntity(`application/json`, """{ "bar": "baz" }""") 88 | Unmarshal(entity) 89 | .to[Foo] 90 | .failed 91 | .map(_ should have message "requirement failed: bar must start with 'bar'!") 92 | } 93 | 94 | "fail with NoContentException when unmarshalling empty entities" in { 95 | val entity = HttpEntity.empty(`application/json`) 96 | Unmarshal(entity) 97 | .to[Foo] 98 | .failed 99 | .map(_ shouldBe Unmarshaller.NoContentException) 100 | } 101 | 102 | "fail with UnsupportedContentTypeException when Content-Type is not `application/json`" in { 103 | val entity = HttpEntity("""{ "bar": "bar" }""") 104 | Unmarshal(entity) 105 | .to[Foo] 106 | .failed 107 | .map( 108 | _ shouldBe UnsupportedContentTypeException(Some(`text/plain(UTF-8)`), `application/json`) 109 | ) 110 | } 111 | 112 | "not write None" in { 113 | val optionFoo = OptionFoo(None) 114 | Marshal(optionFoo) 115 | .to[RequestEntity] 116 | .map(_.asInstanceOf[HttpEntity.Strict].data.decodeString("UTF-8") shouldBe "{}") 117 | } 118 | 119 | "fail when unmarshalling empty entities with safeUnmarshaller" in { 120 | val entity = HttpEntity.empty(`application/json`) 121 | Unmarshal(entity) 122 | .to[Either[String, Foo]] 123 | .futureValue 124 | .left 125 | .value shouldBe a[String] 126 | } 127 | 128 | val errorMessage = """.a(expected '"' got '1')""" 129 | 130 | "fail-fast and return only the first unmarshalling error" in { 131 | val entity = HttpEntity(`application/json`, """{ "a": 1, "b": 2 }""") 132 | Unmarshal(entity) 133 | .to[MultiFoo] 134 | .failed 135 | .map(_.getMessage()) 136 | .map(_ shouldBe errorMessage) 137 | } 138 | 139 | "fail-fast and return only the first unmarshalling error with safeUnmarshaller" in { 140 | val entity = HttpEntity(`application/json`, """{ "a": 1, "b": 2 }""") 141 | Unmarshal(entity) 142 | .to[Either[String, MultiFoo]] 143 | .futureValue 144 | .left 145 | .value shouldBe errorMessage 146 | } 147 | 148 | "allow unmarshalling with passed in Content-Types" in { 149 | val foo = Foo("bar") 150 | 151 | val entity = HttpEntity(`application/json`, """{ "bar": "bar" }""") 152 | Unmarshal(entity).to[Foo].map(_ shouldBe foo) 153 | } 154 | } 155 | 156 | override protected def afterAll(): Unit = { 157 | Await.ready(system.terminate(), 42.seconds) 158 | super.afterAll() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | // ***************************************************************************** 2 | // Build settings 3 | // ***************************************************************************** 4 | 5 | inThisBuild( 6 | Seq( 7 | organization := "de.heikoseeberger", 8 | organizationName := "Heiko Seeberger", 9 | startYear := Some(2015), 10 | licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")), 11 | homepage := Some(url("https://github.com/hseeberger/akka-http-json")), 12 | scmInfo := Some( 13 | ScmInfo( 14 | url("https://github.com/hseeberger/akka-http-json"), 15 | "git@github.com:hseeberger/akka-http-json.git" 16 | ) 17 | ), 18 | developers := List( 19 | Developer( 20 | "hseeberger", 21 | "Heiko Seeberger", 22 | "mail@heikoseeberger.de", 23 | url("https://github.com/hseeberger") 24 | ) 25 | ), 26 | scalaVersion := "2.13.10", 27 | crossScalaVersions := Seq(scalaVersion.value, "2.12.17"), 28 | scalacOptions ++= Seq( 29 | "-unchecked", 30 | "-deprecation", 31 | "-language:_", 32 | "-encoding", 33 | "UTF-8", 34 | "-Ywarn-unused:imports", 35 | "-target:jvm-1.8" 36 | ), 37 | scalafmtOnCompile := true, 38 | dynverSeparator := "_" // the default `+` is not compatible with docker tags, 39 | ) 40 | ) 41 | 42 | val withScala3 = Seq( 43 | crossScalaVersions += "3.2.1", 44 | ) 45 | 46 | // ***************************************************************************** 47 | // Projects 48 | // ***************************************************************************** 49 | 50 | lazy val `akka-http-json` = 51 | project 52 | .in(file(".")) 53 | .disablePlugins(MimaPlugin) 54 | .aggregate( 55 | `akka-http-argonaut`, 56 | `akka-http-avro4s`, 57 | `akka-http-circe`, 58 | `akka-http-jackson`, 59 | `akka-http-json4s`, 60 | `akka-http-jsoniter-scala`, 61 | `akka-http-ninny`, 62 | `akka-http-play-json`, 63 | `akka-http-upickle`, 64 | `akka-http-zio-json`, 65 | ) 66 | .settings(commonSettings) 67 | .settings( 68 | Compile / unmanagedSourceDirectories := Seq.empty, 69 | Test / unmanagedSourceDirectories := Seq.empty, 70 | publishArtifact := false, 71 | ) 72 | 73 | lazy val `akka-http-argonaut` = 74 | project 75 | .enablePlugins(AutomateHeaderPlugin) 76 | .settings(commonSettings, withScala3) 77 | .settings( 78 | libraryDependencies ++= Seq( 79 | library.akkaHttp, 80 | library.argonaut, 81 | library.akkaStream % Provided, 82 | library.scalaTest % Test, 83 | ) 84 | ) 85 | 86 | lazy val `akka-http-circe` = 87 | project 88 | .enablePlugins(AutomateHeaderPlugin) 89 | .settings(commonSettings) 90 | .settings( 91 | libraryDependencies ++= Seq( 92 | library.akkaHttp, 93 | library.circe, 94 | library.circeParser, 95 | library.akkaStream % Provided, 96 | library.circeGeneric % Test, 97 | library.scalaTest % Test, 98 | ) 99 | ) 100 | 101 | lazy val `akka-http-jackson` = 102 | project 103 | .enablePlugins(AutomateHeaderPlugin) 104 | .settings(commonSettings) 105 | .settings( 106 | libraryDependencies ++= Seq( 107 | library.akkaHttp, 108 | library.akkaHttpJacksonJava, 109 | library.jacksonModuleScala, 110 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 111 | library.akkaStream % Provided, 112 | library.scalaTest % Test, 113 | ) 114 | ) 115 | 116 | lazy val `akka-http-json4s` = 117 | project 118 | .enablePlugins(AutomateHeaderPlugin) 119 | .settings(commonSettings, withScala3) 120 | .settings( 121 | libraryDependencies ++= Seq( 122 | library.akkaHttp, 123 | library.json4sCore, 124 | library.akkaStream % Provided, 125 | library.json4sJackson % Test, 126 | library.json4sNative % Test, 127 | library.scalaTest % Test, 128 | ) 129 | ) 130 | 131 | lazy val `akka-http-jsoniter-scala` = 132 | project 133 | .enablePlugins(AutomateHeaderPlugin) 134 | .settings(commonSettings, withScala3) 135 | .settings( 136 | libraryDependencies ++= Seq( 137 | library.akkaHttp, 138 | library.jsoniterScalaCore, 139 | library.akkaStream % Provided, 140 | library.jsoniterScalaMacros % Test, 141 | library.scalaTest % Test, 142 | ) 143 | ) 144 | 145 | lazy val `akka-http-ninny` = 146 | project 147 | .enablePlugins(AutomateHeaderPlugin) 148 | .settings(commonSettings, withScala3) 149 | .settings( 150 | libraryDependencies ++= Seq( 151 | library.akkaHttp, 152 | library.ninny, 153 | library.akkaStream % Provided, 154 | library.scalaTest % Test, 155 | ) 156 | ) 157 | 158 | lazy val `akka-http-play-json` = 159 | project 160 | .enablePlugins(AutomateHeaderPlugin) 161 | .settings(commonSettings) 162 | .settings( 163 | libraryDependencies ++= Seq( 164 | library.akkaHttp, 165 | library.playJson, 166 | library.akkaStream % Provided, 167 | library.scalaTest % Test, 168 | ) 169 | ) 170 | 171 | lazy val `akka-http-upickle` = 172 | project 173 | .enablePlugins(AutomateHeaderPlugin) 174 | .settings(commonSettings) 175 | .settings( 176 | libraryDependencies ++= Seq( 177 | library.akkaHttp, 178 | library.upickle, 179 | library.akkaStream % Provided, 180 | library.scalaTest % Test, 181 | ) 182 | ) 183 | 184 | lazy val `akka-http-avro4s` = 185 | project 186 | .enablePlugins(AutomateHeaderPlugin) 187 | .settings(commonSettings) 188 | .settings( 189 | libraryDependencies ++= Seq( 190 | library.akkaHttp, 191 | library.avro4sJson, 192 | library.akkaStream % Provided, 193 | library.scalaTest % Test, 194 | ) 195 | ) 196 | 197 | lazy val `akka-http-zio-json` = 198 | project 199 | .enablePlugins(AutomateHeaderPlugin) 200 | .settings(commonSettings, withScala3) 201 | .settings( 202 | libraryDependencies ++= Seq( 203 | library.akkaHttp, 204 | library.zioJson, 205 | library.akkaStream % Provided, 206 | library.scalaTest % Test 207 | ) 208 | ) 209 | 210 | // ***************************************************************************** 211 | // Project settings 212 | // ***************************************************************************** 213 | 214 | lazy val commonSettings = 215 | Seq( 216 | // Also (automatically) format build definition together with sources 217 | Compile / scalafmt := { 218 | val _ = (Compile / scalafmtSbt).value 219 | (Compile / scalafmt).value 220 | } 221 | ) 222 | 223 | // ***************************************************************************** 224 | // Library dependencies 225 | // ***************************************************************************** 226 | 227 | lazy val library = 228 | new { 229 | object Version { 230 | val akka = "2.6.20" 231 | val akkaHttp = "10.2.10" 232 | val argonaut = "6.3.8" 233 | val avro4s = "4.0.12" 234 | val circe = "0.14.1" 235 | val jacksonModuleScala = "2.13.1" 236 | val json4s = "4.0.6" 237 | val jsoniterScala = "2.17.9" 238 | val ninny = "0.7.0" 239 | val play = "2.9.2" 240 | val scalaTest = "3.2.11" 241 | val upickle = "1.5.0" 242 | val zioJson = "0.3.0" 243 | } 244 | // format: off 245 | val akkaHttp = ("com.typesafe.akka" %% "akka-http" % Version.akkaHttp).cross(CrossVersion.for3Use2_13) 246 | val akkaHttpJacksonJava = ("com.typesafe.akka" %% "akka-http-jackson" % Version.akkaHttp).cross(CrossVersion.for3Use2_13) 247 | val akkaStream = "com.typesafe.akka" %% "akka-stream" % Version.akka 248 | val argonaut = "io.argonaut" %% "argonaut" % Version.argonaut 249 | val avro4sJson = "com.sksamuel.avro4s" %% "avro4s-json" % Version.avro4s 250 | val circe = "io.circe" %% "circe-core" % Version.circe 251 | val circeGeneric = "io.circe" %% "circe-generic" % Version.circe 252 | val circeParser = "io.circe" %% "circe-parser" % Version.circe 253 | val jacksonModuleScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % Version.jacksonModuleScala 254 | val json4sCore = "org.json4s" %% "json4s-core" % Version.json4s 255 | val json4sJackson = "org.json4s" %% "json4s-jackson" % Version.json4s 256 | val json4sNative = "org.json4s" %% "json4s-native" % Version.json4s 257 | val jsoniterScalaCore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % Version.jsoniterScala 258 | val jsoniterScalaMacros = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % Version.jsoniterScala 259 | val ninny = "tk.nrktkt" %% "ninny" % Version.ninny 260 | val playJson = "com.typesafe.play" %% "play-json" % Version.play 261 | val scalaTest = "org.scalatest" %% "scalatest" % Version.scalaTest 262 | val upickle = "com.lihaoyi" %% "upickle" % Version.upickle 263 | val zioJson = "dev.zio" %% "zio-json" % Version.zioJson 264 | // format: on 265 | } 266 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.8.0 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.bintrayIvyRepo("sbt", "sbt-plugin-releases") 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") 4 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.0.1") 5 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.5") 6 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 7 | --------------------------------------------------------------------------------