├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── ErrorHandler.scala ├── Filters.scala ├── Module.scala └── com │ └── github │ └── dnvriend │ ├── component │ ├── helloworld │ │ ├── controller │ │ │ ├── HelloWorldController.scala │ │ │ └── dto │ │ │ │ └── HelloWorldDto.scala │ │ └── repository │ │ │ ├── HelloWorldRepository.scala │ │ │ └── entity │ │ │ └── HelloWorld.scala │ ├── highlevelserver │ │ ├── HighLevelServer.scala │ │ ├── dto │ │ │ ├── Person.scala │ │ │ └── PersonWithId.scala │ │ ├── marshaller │ │ │ └── Marshaller.scala │ │ ├── repository │ │ │ └── PersonRepository.scala │ │ └── route │ │ │ └── RestRoute.scala │ ├── lowlevelserver │ │ ├── LowLevelServer.scala │ │ ├── dto │ │ │ ├── Person.scala │ │ │ └── PersonWithId.scala │ │ ├── marshaller │ │ │ └── Marshaller.scala │ │ └── repository │ │ │ └── PersonRepository.scala │ ├── repository │ │ └── PersonRepository.scala │ ├── simpleserver │ │ ├── SimpleServer.scala │ │ ├── dto │ │ │ ├── http │ │ │ │ ├── OrderDto.scala │ │ │ │ ├── Person.scala │ │ │ │ ├── PersonV1.scala │ │ │ │ ├── PersonV2.scala │ │ │ │ └── Ping.scala │ │ │ └── play │ │ │ │ ├── OrderDto.scala │ │ │ │ └── Person.scala │ │ ├── marshaller │ │ │ ├── DisjunctionMarshaller.scala │ │ │ └── Marshallers.scala │ │ └── route │ │ │ ├── CsvStreamingRoute.scala │ │ │ ├── JsonStreamingRoute.scala │ │ │ ├── SimpleDisjunctionRoute.scala │ │ │ ├── SimpleServerRestRoutes.scala │ │ │ └── TryRoute.scala │ └── webservices │ │ ├── common │ │ └── Domain.scala │ │ ├── eetnu │ │ └── EetNuClient.scala │ │ ├── generic │ │ └── HttpClient.scala │ │ ├── iens │ │ └── IensClient.scala │ │ ├── postcode │ │ └── PostcodeClient.scala │ │ └── weather │ │ └── WeatherClient.scala │ └── util │ └── TimeUtil.scala ├── build.sbt ├── conf ├── application.conf ├── logback.xml └── routes ├── people.json ├── project ├── build.properties └── plugins.sbt ├── sandbox-run.sh ├── sandbox-stop.sh ├── stream-persons.sh └── test └── com └── github └── dnvriend ├── TestSpec.scala └── marshaller └── XmlMarshallerTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | project/project/ 2 | target/ 3 | .idea/ 4 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: false 3 | scala: 4 | - "2.11.8" 5 | jdk: 6 | - oraclejdk8 7 | branches: 8 | only: 9 | - master 10 | notifications: 11 | email: 12 | - dnvriend@gmail.com 13 | script: sbt clean test 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # akka-http-test # 2 | 3 | [![Join the chat at https://gitter.im/dnvriend/akka-http-test](https://badges.gitter.im/dnvriend/akka-http-test.svg)](https://gitter.im/dnvriend/akka-http-test?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/dnvriend/akka-http-test.svg?branch=master)](https://travis-ci.org/dnvriend/akka-http-test) 5 | [![License](http://img.shields.io/:license-Apache%202-red.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt) 6 | 7 | A study project how akka-http works. The code below is a bit compacted, so please use it for reference only how 8 | the (new) API must be used. It will not compile/work correctly when you just copy/paste it. Check out the working 9 | source code for correct usage. 10 | 11 | ## Mastering Akka by Chris Baxter 12 | A great book ETA October 2016 by Packt Publishing, written by Chris Baxter and reviewed by me is [Mastering Akka](https://www.packtpub.com/application-development/mastering-akka) which will contain everything you ever wanted to know about akka-actors, akka-persistence, akka-streams, akka-http and akka-cluster, ConductR, Domain Driven Design and Event Sourcing. Its a book you just should have. 13 | 14 | ## Contribution policy ## 15 | 16 | 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. 17 | 18 | ## License ## 19 | 20 | This code is open source software licensed under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.html). 21 | 22 | # akka http documentation 23 | Akka http now has its own release scheme and is not tied to that of akka anymore. 24 | As such, you should update your build file accordingly. 25 | 26 | - [Akka-HTTP documentation](http://doc.akka.io/docs/akka-http/current/scala.html) 27 | - [Akka-HTTP migration guide](http://doc.akka.io/docs/akka-http/current/java/http/migration-guide/migration-guide-2.4.x-3.0.x.html) 28 | 29 | # Source Streaming 30 | As of [Akka v2.4.9-RC1](http://doc.akka.io/docs/akka/2.4/scala/http/routing-dsl/source-streaming-support.html), 31 | akka-http supports completing a request with an Akka Source[T, _], which makes it possible 32 | to easily build and consume streaming end-to-end APIs which apply back-pressure throughout the entire stack! 33 | 34 | # Web Service Clients (RPC) 35 | Akka-Http has a client API and as such RPC's can be created. Take a look at the package `com.github.dnvriend.webservices`, I have created 36 | some example RPC style web service clients for `eetnu`, `iens`, `postcode`, `openWeatherApi`, based on the generic `com.github.dnvriend.webservices.generic.HttpClient` 37 | client that supports Http and SSL connections with basic authentication or one legged OAuth1 with consumerKey and consumerSecret configuration 38 | from `application.conf`. The RPC clients also support for single RPC without cached connections and the streaming cached connection style where 39 | you can stream data to your clients. For usage please see the tests for the RPC clients. Good stuff :) 40 | 41 | # Web Server 42 | A new HTTP server can be launched using the `Http()` class. The `bindAndHandle()` method is a convenience method which starts a new HTTP server at the given endpoint and uses the given 'handler' `Flow` for processing all incoming connections. 43 | 44 | The number of [concurrently accepted connections](https://github.com/akka/akka/blob/master/akka-http-core/src/main/scala/akka/http/scaladsl/Http.scala#L130) can be configured by overriding `akka.http.server.max-connections` setting. 45 | 46 | ```scala 47 | import akka.http.scaladsl._ 48 | import akka.http.scaladsl.model._ 49 | import akka.stream.scaladsl._ 50 | 51 | def routes: Flow[HttpRequest, HttpResponse, Unit] 52 | 53 | Http().bindAndHandle(routes, "0.0.0.0", 8080) 54 | ``` 55 | 56 | # Routes 57 | First some Akka Stream parley: 58 | 59 | * `Stream`: a continually moving flow of elements, 60 | * `Element`: the processing unit of streams, 61 | * `Processing stage`: building blocks that build up a `Flow` or `FlowGraph` for example `map()`, `filter()`, `transform()`, `junction()` etc, 62 | * `Source`: a processing stage with exactly one output, emitting data elements when downstream processing stages are ready to receive them, 63 | * `Sink`: a processing stage with exactly one input, requesting and accepting data elements 64 | * `Flow`: a processing stage with exactly one input and output, which connects its up- and downstream by moving/transforming the data elements flowing through it, 65 | * `Runnable flow`: A flow that has both ends attached to a `Source` and `Sink` respectively, 66 | * `Materialized flow`: An instantiation / incarnation / materialization of the abstract processing-flow template. 67 | 68 | The abstractions above (Flow, Source, Sink, Processing stage) are used to create a processing-stream `template` or `blueprint`. When the template has a `Source` connected to a `Sink` with optionally some `processing stages` between them, such a `template` is called a `Runnable Flow`. 69 | 70 | The materializer for `akka-stream` is the [ActorMaterializer](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.stream.ActorMaterializer) 71 | which takes the list of transformations comprising a [akka.stream.scaladsl.Flow](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.stream.javadsl.Flow) 72 | and materializes them in the form of [org.reactivestreams.Processor](https://github.com/reactive-streams/reactive-streams-jvm/blob/master/api/src/main/java/org/reactivestreams/Processor.java) 73 | instances, in which every stage is converted into one actor. 74 | 75 | In akka-http parley, a 'Route' is a `Flow[HttpRequest, HttpResponse, Unit]` so it is a processing stage that transforms 76 | `HttpRequest` elements to `HttpResponse` elements. 77 | 78 | ## Streams everywhere 79 | The following `reactive-streams` are defined in `akka-http`: 80 | 81 | * Requests on one HTTP connection, 82 | * Responses on one HTTP connection, 83 | * Chunks of a chunked message, 84 | * Bytes of a message entity. 85 | 86 | # Route Directives 87 | Akka http uses the route directives we know (and love) from Spray: 88 | 89 | ```scala 90 | import akka.http.scaladsl._ 91 | import akka.http.scaladsl.model._ 92 | import akka.http.scaladsl.server.Directives._ 93 | import akka.stream.scaladsl._ 94 | 95 | def routes: Flow[HttpRequest, HttpResponse, Unit] = 96 | logRequestResult("akka-http-test") { 97 | path("") { 98 | redirect("person", StatusCodes.PermanentRedirect) 99 | } ~ 100 | pathPrefix("person") { 101 | complete { 102 | Person("John Doe", 25) 103 | } 104 | } ~ 105 | pathPrefix("ping") { 106 | complete { 107 | Ping(TimeUtil.timestamp) 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | # Spray-Json 114 | I'm glad to see that `akka-http-spray-json-experimental` basically has the same API as spray: 115 | 116 | ```scala 117 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 118 | import akka.http.scaladsl.marshalling.Marshal 119 | import akka.http.scaladsl.model.RequestEntity 120 | import akka.http.scaladsl.unmarshalling.Unmarshal 121 | import spray.json.DefaultJsonProtocol._ 122 | import spray.json._ 123 | 124 | case class Person(name: String, age: Int) 125 | 126 | val personJson = """{"name":"John Doe","age":25}""" 127 | 128 | implicit val personJsonFormat = jsonFormat2(Person) 129 | 130 | Person("John Doe", 25).toJson.compactPrint shouldBe personJson 131 | 132 | personJson.parseJson.convertTo[Person] shouldBe Person("John Doe", 25) 133 | 134 | val person = Person("John Doe", 25) 135 | val entity = Marshal(person).to[RequestEntity].futureValue 136 | Unmarshal(entity).to[Person].futureValue shouldBe person 137 | ``` 138 | 139 | # Custom Marshalling/Unmarshalling 140 | Akka http has a cleaner API for custom types compared to Spray's. Out of the box it has support to marshal to/from basic types (Byte/String/NodeSeq) and 141 | so we can marshal/unmarshal from/to case classes from any line format. The API uses the [Marshal](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.http.scaladsl.marshalling.Marshal) 142 | object to do the marshalling and the [Unmarshal](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.http.scaladsl.unmarshalling.Unmarshal) 143 | object to to the unmarshal process. Both interfaces return Futures that contain the outcome. 144 | 145 | The `Unmarshal` class uses an [Unmarshaller](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.http.scaladsl.unmarshalling.Unmarshaller) 146 | that defines how an encoding like eg `XML` can be converted from eg. a `NodeSeq` to a custom type, like eg. a `Person`. 147 | 148 | To `Marshal` class uses [Marshaller](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.http.javadsl.server.Marshaller)s 149 | to do the heavy lifting. There are three kinds of marshallers, they all do the same, but one is not interested in the [MediaType](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.http.javadsl.model.MediaType) 150 | , the `opaque` marshaller, then there is the `withOpenCharset` marshaller, that is only interested in the mediatype, and forwards the received [HttpCharset](http://doc.akka.io/api/akka-stream-and-http-experimental/1.0/#akka.http.scaladsl.model.HttpCharset) 151 | to the `marshal function` so that the responsibility for handling the character encoding is up to the developer, 152 | and the last one, the `withFixedCharset` will handle only HttpCharsets that match the marshaller configured one. 153 | 154 | An example XML marshaller/unmarshaller: 155 | 156 | ```scala 157 | import akka.http.scaladsl.marshalling.{ Marshal, Marshaller, Marshalling } 158 | import akka.http.scaladsl.model.HttpCharset 159 | import akka.http.scaladsl.model.HttpCharsets._ 160 | import akka.http.scaladsl.model.MediaTypes._ 161 | import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } 162 | 163 | import scala.xml.NodeSeq 164 | 165 | case class Person(name: String, age: Int) 166 | 167 | val personXml = 168 | 169 | John Doe 170 | 25 171 | 172 | 173 | implicit val personUnmarshaller = Unmarshaller.strict[NodeSeq, Person] { xml ⇒ 174 | Person((xml \\ "name").text, (xml \\ "age").text.toInt) 175 | } 176 | 177 | val opaquePersonMarshalling = Marshalling.Opaque(() ⇒ personXml) 178 | val openCharsetPersonMarshalling = Marshalling.WithOpenCharset(`text/xml`, (charset: HttpCharset) ⇒ personXml) 179 | val fixedCharsetPersonMarshalling = Marshalling.WithFixedCharset(`text/xml`, `UTF-8`, () ⇒ personXml) 180 | 181 | val opaquePersonMarshaller = Marshaller.opaque[Person, NodeSeq] { person ⇒ personXml } 182 | val withFixedCharsetPersonMarshaller = Marshaller.withFixedCharset[Person, NodeSeq](`text/xml`, `UTF-8`) { person ⇒ personXml } 183 | val withOpenCharsetCharsetPersonMarshaller = Marshaller.withOpenCharset[Person, NodeSeq](`text/xml`) { (person, charset) ⇒ personXml } 184 | 185 | implicit val personMarshaller = Marshaller.oneOf[Person, NodeSeq](opaquePersonMarshaller, withFixedCharsetPersonMarshaller, withOpenCharsetCharsetPersonMarshaller) 186 | 187 | "personXml" should "be unmarshalled" in { 188 | Unmarshal(personXml).to[Person].futureValue shouldBe Person("John Doe", 25) 189 | } 190 | 191 | "Person" should "be marshalled" in { 192 | Marshal(Person("John Doe", 25)).to[NodeSeq].futureValue shouldBe personXml 193 | } 194 | ``` 195 | 196 | # Vendor specific media types 197 | Versioning an API can be tricky. The key is choosing a strategy on how to do versioning. I have found and tried the following stragegies as 198 | blogged by [Jim Liddell's blog](http://liddellj.com/2014/01/08/using-media-type-parameters-to-version-an-http-api), which is great by the way! 199 | 200 | 1. 'The URL is king' in which the URL is encoded in the URL eg. `http://localhost:8080/api/v1/person`. The downside of this strategy is that 201 | the location of a resource may not change, and when we request another representation, the url does change eg. to `http://localhost:8080/api/v2/person`. 202 | 2. Using a version request parameter like: `http://localhost:8080/api/person?version=1`. The downside of this stragegy lies in the fact that resource urls 203 | must be as lean as possible, and the only exception is for filtering, sorting, searching and paging, as stated by [Vinay Sahni](http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api) 204 | in his great blog 'Best Practices for Designing a Pragmatic RESTful API'. 205 | 3. Both bloggers and I agree that using request headers for versioning, and therefor relying on vendor specific media types is a great way to keep the 206 | resource urls clean, the location does not change and in code the versioning is only a presentation responsibility, easilly resolved by an 207 | in scope mashaller. 208 | 209 | When you run the example, you can try the following requests: 210 | 211 | ```bash 212 | # The latest version in JSON 213 | curl -H "Accept: application/json" localhost:8080/person 214 | http :8080/person 215 | # A stream of persons in CSV 216 | curl -H "Accept: text/csv" localhost:8080/persons/stream/100 217 | http :8080/persons/stream/100 Accept:text/csv 218 | # A stream of persons in JSON 219 | curl -H "Accept: application/json" localhost:8080/persons/stream/100 220 | http :8080/persons/stream/100 Accept:application/json 221 | # A list of of persons in JSON 222 | curl -H "Accept: application/json" localhost:8080/persons/strict/100 223 | http :8080/persons/strict/100 Accept:application/json 224 | # The latest version in XML 225 | curl -H "Accept: application/xml" localhost:8080/person 226 | http :8080/person Accept:application/xml 227 | # Vendor specific header for JSON v1 228 | curl -H "Accept: application/vnd.acme.v1+json" localhost:8080/person 229 | http :8080/person Accept:application/vnd.acme.v1+json 230 | # Vendor specific header for JSON v2 231 | curl -H "Accept: application/vnd.acme.v2+json" localhost:8080/person 232 | http :8080/person Accept:application/vnd.acme.v2+json 233 | # Vendor specific header for XML v1 234 | curl -H "Accept: application/vnd.acme.v1+xml" localhost:8080/person 235 | http :8080/person Accept:application/vnd.acme.v2+json 236 | # Vendor specific header for XML v2 237 | curl -H "Accept: application/vnd.acme.v2+xml" localhost:8080/person 238 | http :8080/person Accept:application/vnd.acme.v2+xml 239 | ``` 240 | 241 | Please take a look at the [Marshallers](https://github.com/dnvriend/akka-http-test/blob/master/src/main/scala/com/github/dnvriend/Marshallers.scala) 242 | trait for an example how you could implement this strategy and the [MarshallersTest](https://github.com/dnvriend/akka-http-test/blob/master/src/test/scala/com/github/dnvriend/MarshallersTest.scala) 243 | how to test the routes using the `Accept` header and leveraging the media types. 244 | 245 | # Video 246 | - [Parleys - Dr. Roland Kuhn - Akka HTTP - The Reactive Web Toolkit](https://www.parleys.com/tutorial/akka-http-reactive-web-toolkit) 247 | - [Parleys - Mathias Doenitz - Akka HTTP - Unrest your actors](https://www.parleys.com/tutorial/akka-http-un-rest-your-actors) 248 | - [Youtube - Mathias Doenitz - Akka HTTP — The What, Why and How](https://www.youtube.com/watch?v=y_slPbktLr0) 249 | - [Youtube - Mathias Doenitz - Spray & Akka HTTP](https://www.youtube.com/watch?v=o5PUDI4qi10) 250 | - [Youtube - Mathias Doenitz - Spray on Akka](https://www.youtube.com/watch?v=7MqD7_YvZ8Q) 251 | - [Parleys - Dr. Roland Kuhn - Go Reactive: Blueprint for Future Applications ](https://www.parleys.com/tutorial/go-reactive-blueprint-future-applications) 252 | - [Parleys - Dr. Roland Kuhn - Distributed in space and time](https://www.parleys.com/tutorial/roland-kuhn-distributed-space-time-1) 253 | - [Parleys - Mirco Dotta - Akka Streams](https://www.parleys.com/tutorial/akka-streams) 254 | 255 | # Slides 256 | - [Slides - Akka HTTP — The What, Why and How](http://spray.io/nescala2015/#/) 257 | - [Slides - Reactive Streams & Akka HTTP](http://spray.io/msug/#/) 258 | 259 | # Northeast Scala Symposium 2015 260 | - [Northeast Scala Symposium 2015](https://newcircle.com/s/post/1702/northeast_scala_symposium_2015_videos?utm_campaign=YouTube_Channel&utm_source=youtube&utm_medium=social&utm_content=Watch%20the%2013%20talks%20from%20NE%20Scala%202015%3A) 261 | 262 | 263 | # Projects that use akka-http 264 | - [GitHub - Example of (micro)service written in Scala & akka-http](https://github.com/theiterators/akka-http-microservice) 265 | -------------------------------------------------------------------------------- /app/ErrorHandler.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 | import javax.inject._ 18 | 19 | import play.api._ 20 | import play.api.http.DefaultHttpErrorHandler 21 | import play.api.libs.json.Json 22 | import play.api.mvc.Results._ 23 | import play.api.mvc.{ RequestHeader, Result } 24 | import play.api.routing.Router 25 | 26 | import scala.concurrent.Future 27 | 28 | @Singleton 29 | class ErrorHandler @Inject() (env: Environment, config: Configuration, sourceMapper: OptionalSourceMapper, router: Provider[Router]) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) { 30 | override protected def onNotFound(request: RequestHeader, message: String): Future[Result] = { 31 | Future.successful(NotFound(Json.obj("path" -> request.path, "messages" -> List("not found", message)))) 32 | } 33 | 34 | override protected def onDevServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { 35 | Future.successful(InternalServerError(Json.obj("path" -> request.path, "messages" -> List(exception.description)))) 36 | } 37 | 38 | override protected def onProdServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { 39 | Future.successful(InternalServerError(Json.obj("path" -> request.path, "messages" -> List(exception.description)))) 40 | } 41 | 42 | override protected def onForbidden(request: RequestHeader, message: String): Future[Result] = { 43 | Future.successful(Forbidden(Json.obj("path" -> request.path, "messages" -> List("forbidden", message)))) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Filters.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 | import javax.inject._ 18 | 19 | import akka.stream.Materializer 20 | import play.api._ 21 | import play.api.http.HttpFilters 22 | import play.api.mvc._ 23 | 24 | import scala.concurrent.{ ExecutionContext, Future } 25 | 26 | /** 27 | * This class configures filters that run on every request. This 28 | * class is queried by Play to get a list of filters. 29 | * 30 | * Play will automatically use filters from any class called 31 | * `Filters` that is placed the root package. You can load filters 32 | * from a different class by adding a `play.http.filters` setting to 33 | * the `application.conf` configuration file. 34 | * 35 | * @param env Basic environment settings for the current application. 36 | * @param exampleFilter A demonstration filter that adds a header to 37 | * each response. 38 | */ 39 | @Singleton 40 | class Filters @Inject() (env: Environment, exampleFilter: ExampleFilter) extends HttpFilters { 41 | println("init filter") 42 | override val filters = { 43 | // Use the example filter if we're running development mode. If 44 | // we're running in production or test mode then don't use any 45 | // filters at all. 46 | // if (env.mode == Mode.Dev) Seq(exampleFilter) else Seq.empty 47 | Seq(exampleFilter) 48 | } 49 | } 50 | 51 | /** 52 | * This is a simple filter that adds a header to all requests. It's 53 | * added to the application's list of filters by the 54 | * [[Filters]] class. 55 | * 56 | * @param mat This object is needed to handle streaming of requests 57 | * and responses. 58 | * @param exec This class is needed to execute code asynchronously. 59 | * It is used below by the `map` method. 60 | */ 61 | @Singleton 62 | class ExampleFilter @Inject() (implicit override val mat: Materializer, exec: ExecutionContext) extends Filter { 63 | override def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = { 64 | // Run the next filter in the chain. This will call other filters 65 | // and eventually call the action. Take the result and modify it 66 | // by adding a new header. 67 | nextFilter(requestHeader).map { result => 68 | result.withHeaders("X-ExampleFilter" -> "foo") 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Module.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 | import javax.inject.Inject 18 | 19 | import akka.actor.ActorSystem 20 | import akka.pattern.CircuitBreaker 21 | import akka.stream.Materializer 22 | import com.github.dnvriend.component.repository.PersonRepository 23 | import com.github.dnvriend.component.simpleserver.SimpleServer 24 | import com.google.inject.{ AbstractModule, Provider, Provides } 25 | import play.api.Configuration 26 | import play.api.libs.concurrent.AkkaGuiceSupport 27 | 28 | import scala.concurrent.ExecutionContext 29 | import scala.concurrent.duration._ 30 | 31 | class Module extends AbstractModule with AkkaGuiceSupport { 32 | override def configure(): Unit = { 33 | bind(classOf[SimpleServer]) 34 | .toProvider(classOf[SimpleServerProvider]) 35 | .asEagerSingleton() 36 | } 37 | 38 | @Provides 39 | def circuitBreakerProvider(system: ActorSystem)(implicit ec: ExecutionContext): CircuitBreaker = { 40 | val maxFailures: Int = 3 41 | val callTimeout: FiniteDuration = 1.seconds 42 | val resetTimeout: FiniteDuration = 10.seconds 43 | new CircuitBreaker(system.scheduler, maxFailures, callTimeout, resetTimeout) 44 | } 45 | } 46 | 47 | // alternative way to provide services 48 | class SimpleServerProvider @Inject() (personRepository: PersonRepository, cb: CircuitBreaker, config: Configuration)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext) extends Provider[SimpleServer] { 49 | override def get(): SimpleServer = 50 | new SimpleServer(personRepository, cb, config.getString("http.interface").getOrElse("0.0.0.0"), config.getInt("http.port").getOrElse(8080)) 51 | } 52 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/helloworld/controller/HelloWorldController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.helloworld.controller 18 | 19 | import com.github.dnvriend.component.helloworld.repository.HelloWorldRepository 20 | import com.google.inject.Inject 21 | import play.api.mvc._ 22 | import scalaz._ 23 | import Scalaz._ 24 | 25 | // Why does this work; the result type of getByIdD is a Disjunction[String, HelloWorld].. 26 | // 27 | // This is because of the implicit conversion from HelloWorld to a Result type 28 | // 29 | // Action assumes either a 'play.api.mvc.Result' or a function, lets call that function 30 | // 'f' that converts Request => Result. Here we transform the type Disjunction[String, HelloWorld] 31 | // to a 'play.api.mvc.Result'. 32 | // 33 | // You should look at the HelloWorld entity to read more. 34 | // 35 | class HelloWorldController @Inject() (repo: HelloWorldRepository) extends Controller { 36 | def getHelloWorld = Action { request => 37 | val header: String = request.headers.get("X-ExampleFilter").getOrElse("No header") 38 | val msg = repo.getHelloWorld 39 | msg.copy(msg = s"${msg.msg} - $header") 40 | } 41 | def getHelloWorldOpt(id: Long) = Action(repo.getById(id)) 42 | def getHelloWorldMB(id: Long) = Action(repo.getById(id).toMaybe) 43 | def getHelloWorldD(id: Long) = Action(repo.getByIdD(id)) 44 | def getHelloWorldV(id: Long) = Action(repo.getByIdD(id).validation) 45 | def getHelloWorldVN(id: Long) = Action(repo.getByIdD(id).validationNel) 46 | } 47 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/helloworld/controller/dto/HelloWorldDto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.helloworld.controller.dto 18 | 19 | import play.api.libs.json._ 20 | 21 | object HelloWorldDto { 22 | implicit val format: Format[HelloWorldDto] = Json.format[HelloWorldDto] 23 | } 24 | 25 | final case class HelloWorldDto(msg: String) 26 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/helloworld/repository/HelloWorldRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.helloworld.repository 18 | 19 | import com.github.dnvriend.component.helloworld.repository.entity.HelloWorld 20 | import scalaz._ 21 | import Scalaz._ 22 | 23 | class HelloWorldRepository { 24 | def getHelloWorld: HelloWorld = HelloWorld("Hello World!") 25 | def getById(id: Long): Option[HelloWorld] = Option(getHelloWorld) 26 | def getByIdD(id: Long): Disjunction[String, HelloWorld] = getById(id).toRightDisjunction(s"Could not find HelloWorld for id: $id") 27 | } 28 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/helloworld/repository/entity/HelloWorld.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.helloworld.repository.entity 18 | 19 | import play.api.libs.json.{ Format, Json } 20 | import play.api.mvc.{ Result, Results } 21 | 22 | import scala.language.implicitConversions 23 | import scalaz._ 24 | import Scalaz._ 25 | 26 | // The implicit resolution rules are 27 | // 1. First look in current scope 28 | // - Implicits defined in current scope 29 | // - Explicit imports 30 | // - wildcard imports 31 | // - _(*) Same scope in other files (*)_ 32 | // 33 | // We haven't imported anything in the HelloWorldController.. 34 | // 35 | // 2. Now look at associated types in 36 | // - Companion objects of a type 37 | // - Implicit scope of an argument's type (2.9.1) 38 | // - Implicit scope of type arguments (2.8.0) 39 | // - Outer objects for nested types 40 | // 41 | // If we returned an HelloWorld type in the controller, the companion object 42 | // (which is object HelloWorld) would be responsible to convert HelloWorld to a result 43 | // that can be done by means of one of the implicit methods eg. toResult 44 | // 45 | // If we return an Option[HelloWorld], the resolution would first to look at the companion of Option.. no luck there 46 | // next we will look in the 'Implicit scope of an argument type' of Option[T] if we can convert Option[HelloWorld] to 47 | // 'Result', the argument type is 'HelloWorld' and if we look at the companion object of HelloWorld we have a hit. 48 | // because we find a way to convert Option[HelloWorld] to a Result. 49 | // 50 | object HelloWorld extends GenericResult { 51 | implicit val format: Format[HelloWorld] = Json.format[HelloWorld] 52 | } 53 | 54 | final case class HelloWorld(msg: String) 55 | 56 | trait GenericResult extends Results { 57 | implicit def fromA[A: Format](a: A): Result = 58 | Ok(Json.toJson(a)) 59 | implicit def fromOption[A: Format](option: Option[A]): Result = 60 | option.map(a => fromA(a)).getOrElse(NotFound) 61 | implicit def fromMaybe[A: Format](maybe: Maybe[A]): Result = 62 | maybe.toOption 63 | implicit def fromDisjunction[A: Format](disjunction: Disjunction[String, A]): Result = 64 | disjunction.map(a => fromA(a)).valueOr(msg => NotFound(msg)) 65 | implicit def fromValidation[A: Format](validation: Validation[String, A]): Result = 66 | validation.disjunction 67 | implicit def fromValidationNel[A: Format](validation: ValidationNel[String, A]): Result = 68 | validation.leftMap(_.toList.mkString(",")).disjunction 69 | } 70 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/highlevelserver/HighLevelServer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 | ///* 18 | // * Copyright 2016 Dennis Vriend 19 | // * 20 | // * Licensed under the Apache License, Version 2.0 (the "License"); 21 | // * you may not use this file except in compliance with the License. 22 | // * You may obtain a copy of the License at 23 | // * 24 | // * http://www.apache.org/licenses/LICENSE-2.0 25 | // * 26 | // * Unless required by applicable law or agreed to in writing, software 27 | // * distributed under the License is distributed on an "AS IS" BASIS, 28 | // * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | // * See the License for the specific language governing permissions and 30 | // * limitations under the License. 31 | // */ 32 | // 33 | //package com.github.dnvriend.component.highlevelserver 34 | // 35 | //import akka.actor.{ ActorSystem, Props } 36 | //import akka.event.{ Logging, LoggingAdapter } 37 | //import akka.http.scaladsl.Http 38 | //import akka.stream.scaladsl.{ Sink, Source } 39 | //import akka.stream.{ ActorMaterializer, Materializer } 40 | //import akka.util.Timeout 41 | //import com.github.dnvriend.component.highlevelserver.repository.PersonRepository 42 | //import com.github.dnvriend.component.highlevelserver.route.RestRoute 43 | // 44 | //import scala.concurrent.duration._ 45 | //import scala.concurrent.{ ExecutionContext, Future } 46 | // 47 | //class HighLevelServer(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, logging: LoggingAdapter, timeout: Timeout) { 48 | // val personDb = system.actorOf(Props[PersonRepository]) 49 | // val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] = 50 | // Http().bind(interface = "localhost", port = 8080) 51 | // val binding: Future[Http.ServerBinding] = serverSource.to(Sink.foreach(_.handleWith(RestRoute.routes(personDb)))).run 52 | //} 53 | // 54 | //object HighLevelServer extends App { 55 | // // setting up some machinery 56 | // implicit val system: ActorSystem = ActorSystem() 57 | // implicit val mat: Materializer = ActorMaterializer() 58 | // implicit val ec: ExecutionContext = system.dispatcher 59 | // implicit val log: LoggingAdapter = Logging(system, this.getClass) 60 | // implicit val timeout: Timeout = Timeout(10.seconds) 61 | // new HighLevelServer() 62 | //} 63 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/highlevelserver/dto/Person.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.highlevelserver.dto 18 | 19 | final case class Person(name: String, age: Int, married: Boolean) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/highlevelserver/dto/PersonWithId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.highlevelserver.dto 18 | 19 | final case class PersonWithId(id: Long, name: String, age: Int, married: Boolean) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/highlevelserver/marshaller/Marshaller.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.highlevelserver.marshaller 18 | 19 | import com.github.dnvriend.component.highlevelserver.dto.{ Person, PersonWithId } 20 | import spray.json.DefaultJsonProtocol 21 | 22 | trait Marshaller extends DefaultJsonProtocol { 23 | // the jsonFormats for Person and PersonWithId 24 | implicit val personJsonFormat = jsonFormat3(Person) 25 | implicit val personWithIdJsonFormat = jsonFormat4(PersonWithId) 26 | } 27 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/highlevelserver/repository/PersonRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.highlevelserver.repository 18 | 19 | import akka.actor.Actor 20 | import com.github.dnvriend.component.highlevelserver.dto.{ Person, PersonWithId } 21 | 22 | class PersonRepository extends Actor { 23 | override def receive: Receive = database(0, Map.empty) 24 | def database(id: Int, people: Map[Int, PersonWithId]): Receive = { 25 | case person: Person => 26 | val personWithId = PersonWithId(id + 1, person.name, person.age, person.married) 27 | context.become(database(id + 1, people + (id + 1 -> personWithId))) 28 | sender() ! personWithId 29 | 30 | case "findAll" => 31 | sender() ! people.values.toList.sortBy(_.id) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/highlevelserver/route/RestRoute.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.highlevelserver.route 18 | 19 | import akka.actor.ActorRef 20 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 21 | import akka.http.scaladsl.marshalling.ToResponseMarshaller 22 | import akka.http.scaladsl.model.{ StatusCodes, Uri } 23 | import akka.http.scaladsl.server.{ Directives, Route } 24 | import akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller 25 | import akka.pattern.ask 26 | import akka.util.Timeout 27 | import com.github.dnvriend.component.highlevelserver.dto.PersonWithId 28 | import com.github.dnvriend.component.highlevelserver.marshaller.Marshaller 29 | import com.github.dnvriend.component.simpleserver.dto.http.Person 30 | 31 | import scala.concurrent.Future 32 | 33 | // see: akka.http.scaladsl.marshalling.ToResponseMarshallable 34 | // see: akka.http.scaladsl.marshalling.PredefinedToResponseMarshallers 35 | object RestRoute extends Directives with SprayJsonSupport with Marshaller { 36 | def routes(personDb: ActorRef)(implicit timeout: Timeout, trmSingle: ToResponseMarshaller[PersonWithId], trmList: ToResponseMarshaller[List[PersonWithId]], fru: FromRequestUnmarshaller[Person]): Route = { 37 | pathEndOrSingleSlash { 38 | redirect(Uri("/api/person"), StatusCodes.PermanentRedirect) 39 | } ~ 40 | pathPrefix("api" / "person") { 41 | get { 42 | path(IntNumber) { id => 43 | println(s"PathEndsInNumber=$id") 44 | complete((personDb ? "findAll").mapTo[List[PersonWithId]]) 45 | } ~ 46 | pathEndOrSingleSlash { 47 | parameter("foo") { foo => 48 | println(s"foo=$foo") 49 | complete((personDb ? "findAll").mapTo[List[PersonWithId]]) 50 | } ~ 51 | parameter('bar) { bar => 52 | println(s"bar=$bar") 53 | complete((personDb ? "findAll").mapTo[List[PersonWithId]]) 54 | } ~ 55 | complete((personDb ? "findAll").mapTo[List[PersonWithId]]) 56 | } 57 | } ~ 58 | (post & pathEndOrSingleSlash & entity(as[Person])) { person => 59 | complete((personDb ? person).mapTo[PersonWithId]) 60 | } 61 | } ~ 62 | path("failure") { 63 | pathEnd { 64 | complete(Future.failed[String](new RuntimeException("Simulated Failure"))) 65 | } 66 | } ~ 67 | path("success") { 68 | pathEnd { 69 | complete(Future.successful("Success!!")) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/lowlevelserver/LowLevelServer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.lowlevelserver 18 | 19 | import akka.NotUsed 20 | import akka.actor.{ ActorSystem, Props } 21 | import akka.event.{ Logging, LoggingAdapter } 22 | import akka.http.scaladsl.Http 23 | import akka.http.scaladsl.model._ 24 | import akka.pattern.ask 25 | import akka.stream.scaladsl.{ Flow, Sink, Source } 26 | import akka.stream.{ ActorMaterializer, Materializer } 27 | import akka.util.Timeout 28 | import com.github.dnvriend.component.lowlevelserver.dto.{ Person, PersonWithId } 29 | import com.github.dnvriend.component.lowlevelserver.marshaller.Marshaller 30 | import com.github.dnvriend.component.lowlevelserver.repository.PersonRepository 31 | import spray.json.{ DefaultJsonProtocol, _ } 32 | 33 | import scala.concurrent.duration._ 34 | import scala.concurrent.{ ExecutionContext, Future } 35 | 36 | class LowLevelServer(implicit val system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter, timeout: Timeout) extends DefaultJsonProtocol with Marshaller { 37 | val personDb = system.actorOf(Props[PersonRepository]) 38 | 39 | def debug(t: Any)(implicit log: LoggingAdapter = null): Unit = 40 | if (Option(log).isEmpty) println(t) else log.debug(t.toString) 41 | 42 | def http200Okay(req: HttpRequest): HttpResponse = 43 | HttpResponse(StatusCodes.OK) 44 | 45 | def http200AsyncOkay(req: HttpRequest): Future[HttpResponse] = 46 | Future(http200Okay(req)) 47 | 48 | val http200OkayFlow: Flow[HttpRequest, HttpResponse, NotUsed] = Flow[HttpRequest].map { req => 49 | HttpResponse(StatusCodes.OK) 50 | } 51 | 52 | val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] = 53 | Http().bind(interface = "localhost", port = 8080) 54 | 55 | val binding: Future[Http.ServerBinding] = serverSource.to(Sink.foreach { conn => 56 | // conn.handleWith(http200OkayFlow) 57 | // conn.handleWithSyncHandler(http200Okay) 58 | // conn.handleWithAsyncHandler(http200AsyncOkay, 8) 59 | conn.handleWithAsyncHandler(personRequestHandler) 60 | }).run() 61 | 62 | def personRequestHandler(req: HttpRequest): Future[HttpResponse] = req match { 63 | case HttpRequest(HttpMethods.GET, Uri.Path("/api/person"), _, _, _) => for { 64 | xs <- (personDb ? "findAll").mapTo[List[PersonWithId]] 65 | entity = HttpEntity(ContentTypes.`application/json`, xs.toJson.compactPrint) 66 | } yield HttpResponse(StatusCodes.OK, entity = entity) 67 | case HttpRequest(HttpMethods.POST, Uri.Path("/api/person"), _, ent, _) => for { 68 | strictEntity <- ent.toStrict(1.second) 69 | person <- (personDb ? strictEntity.data.utf8String.parseJson.convertTo[Person]).mapTo[PersonWithId] 70 | } yield HttpResponse(StatusCodes.OK, entity = person.toJson.compactPrint) 71 | case req => 72 | req.discardEntityBytes() 73 | Future.successful(HttpResponse(StatusCodes.NotFound)) 74 | } 75 | } 76 | 77 | object LowLevelServerLauncher extends App with DefaultJsonProtocol { 78 | // setting up some machinery 79 | implicit val system: ActorSystem = ActorSystem() 80 | implicit val mat: Materializer = ActorMaterializer() 81 | implicit val ec: ExecutionContext = system.dispatcher 82 | implicit val log: LoggingAdapter = Logging(system, this.getClass) 83 | implicit val timeout: Timeout = Timeout(10.seconds) 84 | 85 | new LowLevelServer() 86 | } -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/lowlevelserver/dto/Person.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.lowlevelserver.dto 18 | 19 | final case class Person(name: String, age: Int, married: Boolean) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/lowlevelserver/dto/PersonWithId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.lowlevelserver.dto 18 | 19 | final case class PersonWithId(id: Long, name: String, age: Int, married: Boolean) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/lowlevelserver/marshaller/Marshaller.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.lowlevelserver.marshaller 18 | 19 | import com.github.dnvriend.component.lowlevelserver.dto.{ Person, PersonWithId } 20 | import spray.json.DefaultJsonProtocol 21 | 22 | trait Marshaller extends DefaultJsonProtocol { 23 | implicit val personJsonFormat = jsonFormat3(Person) 24 | implicit val personWithIdJsonFormat = jsonFormat4(PersonWithId) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/lowlevelserver/repository/PersonRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.lowlevelserver.repository 18 | 19 | import akka.actor.Actor 20 | import com.github.dnvriend.component.lowlevelserver.dto.PersonWithId 21 | import com.github.dnvriend.component.simpleserver.dto.http.Person 22 | 23 | class PersonRepository extends Actor { 24 | override def receive: Receive = database(0, Map.empty) 25 | def database(id: Int, people: Map[Int, PersonWithId]): Receive = { 26 | case person: Person => 27 | val personWithId = PersonWithId(id + 1, person.name, person.age, person.married) 28 | context.become(database(id + 1, people + (id + 1 -> personWithId))) 29 | sender() ! personWithId 30 | 31 | case "findAll" => 32 | sender() ! people.values.toList.sortBy(_.id) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/repository/PersonRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.repository 18 | 19 | import javax.inject.{ Inject, Singleton } 20 | 21 | import akka.NotUsed 22 | import akka.stream.scaladsl.Source 23 | import com.github.dnvriend.component.simpleserver.dto.http.Person 24 | 25 | import scala.concurrent.{ ExecutionContext, Future } 26 | 27 | @Singleton 28 | class PersonRepository @Inject() (implicit ec: ExecutionContext) { 29 | def people(numberOfPeople: Int): Source[Person, NotUsed] = 30 | Source.repeat(Person("foo", 1, false)).zipWith(Source.fromIterator(() => Iterator from 0)) { 31 | case (p, i) => p.copy( 32 | name = if (i % 10 == 0) "baz-" + i else if (i % 2 == 0) "foo-" + i else "bar-" + i, 33 | age = i, 34 | married = i % 2 == 0 35 | ) 36 | }.take(numberOfPeople) 37 | 38 | def listOfPersons(numberOfPeople: Int): Seq[Person] = (0 to numberOfPeople).map { i => 39 | Person( 40 | name = if (i % 10 == 0) "baz-" + i else if (i % 2 == 0) "foo-" + i else "bar-" + i, 41 | age = i, 42 | married = i % 2 == 0 43 | ) 44 | } 45 | 46 | def personAsync: Future[Person] = Future.successful(personSync) 47 | 48 | def personAsyncFailed: Future[Person] = Future.failed(new RuntimeException("This should fail")).map(_ => personSync) 49 | 50 | def personSync: Person = Person("John Doe", 25, false) 51 | } 52 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/SimpleServer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver 18 | 19 | import javax.inject.Inject 20 | 21 | import akka.actor.ActorSystem 22 | import akka.event.{ Logging, LoggingAdapter } 23 | import akka.http.scaladsl._ 24 | import akka.pattern.CircuitBreaker 25 | import akka.stream.{ ActorMaterializer, Materializer } 26 | import com.github.dnvriend.component.repository.PersonRepository 27 | import com.github.dnvriend.component.simpleserver.route._ 28 | import com.google.inject.Singleton 29 | import play.api.Configuration 30 | 31 | import scala.concurrent.ExecutionContext 32 | import scala.concurrent.duration._ 33 | 34 | @Singleton 35 | class SimpleServer @Inject() (personDao: PersonRepository, cb: CircuitBreaker, interface: String, port: Int)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext) { 36 | Http().bindAndHandle(SimpleServerRestRoutes.routes(personDao, cb), interface, port) 37 | } 38 | 39 | object SimpleServerLauncher extends App { 40 | implicit val system: ActorSystem = ActorSystem() 41 | implicit val mat: Materializer = ActorMaterializer() 42 | implicit val ec: ExecutionContext = system.dispatcher 43 | implicit val log: LoggingAdapter = Logging(system, this.getClass) 44 | val maxFailures: Int = 3 45 | val callTimeout: FiniteDuration = 1.seconds 46 | val resetTimeout: FiniteDuration = 10.seconds 47 | val cb = new CircuitBreaker(system.scheduler, maxFailures, callTimeout, resetTimeout) 48 | val config: play.api.Configuration = Configuration(system.settings.config) 49 | 50 | sys.addShutdownHook { 51 | system.terminate() 52 | } 53 | 54 | new SimpleServer(new PersonRepository, cb, config.getString("http.interface").getOrElse("0.0.0.0"), config.getInt("http.port").getOrElse(8080)) 55 | } 56 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/http/OrderDto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.http 18 | 19 | final case class OrderDto(id: Long, name: String) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/http/Person.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.http 18 | 19 | final case class Person(name: String, age: Int, married: Boolean) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/http/PersonV1.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.http 18 | 19 | final case class PersonV1(name: String, age: Int) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/http/PersonV2.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.http 18 | 19 | final case class PersonV2(name: String, age: Int, married: Boolean) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/http/Ping.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.http 18 | 19 | case class Ping(timestamp: String) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/play/OrderDto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.play 18 | 19 | import play.api.libs.json.Json 20 | 21 | object OrderDto { 22 | implicit val format = Json.format[OrderDto] 23 | } 24 | 25 | final case class OrderDto(id: Long, name: String) 26 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/dto/play/Person.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.dto.play 18 | 19 | import play.api.libs.json.Json 20 | 21 | object Person { 22 | implicit val format = Json.format[Person] 23 | } 24 | 25 | final case class Person(name: String, age: Int, married: Boolean) 26 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/marshaller/DisjunctionMarshaller.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.marshaller 18 | 19 | import akka.http.scaladsl.marshalling.{ Marshaller, ToResponseMarshaller } 20 | import akka.http.scaladsl.model.{ HttpEntity, _ } 21 | import com.github.dnvriend.component.simpleserver.marshaller.DisjunctionMarshaller.{ ErrorMessage, FatalError } 22 | import spray.json.JsonWriter 23 | 24 | import scalaz.Scalaz._ 25 | import scalaz._ 26 | 27 | object DisjunctionMarshaller { 28 | trait ErrorMessage { def description: String } 29 | trait FatalError extends ErrorMessage 30 | trait NonFatalError extends ErrorMessage 31 | } 32 | 33 | trait DisjunctionMarshaller { 34 | type DisjunctionNel[A, +B] = Disjunction[NonEmptyList[A], B] 35 | 36 | implicit def disjunctionMarshaller[A1, A2, B](implicit m1: Marshaller[A1, B], m2: Marshaller[A2, B]): Marshaller[Disjunction[A1, A2], B] = Marshaller { implicit ec => 37 | { 38 | case -\/(a1) => m1(a1) 39 | case \/-(a2) => m2(a2) 40 | } 41 | } 42 | 43 | implicit def errorDisjunctionMarshaller[A](implicit w1: JsonWriter[A], w2: JsonWriter[List[String]]): ToResponseMarshaller[DisjunctionNel[String, A]] = 44 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { 45 | case -\/(errors) => HttpResponse( 46 | status = StatusCodes.BadRequest, 47 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), w2.write(errors.toList).compactPrint) 48 | ) 49 | case \/-(success) => HttpResponse(entity = HttpEntity(ContentType(MediaTypes.`application/json`), w1.write(success).compactPrint)) 50 | } 51 | 52 | implicit def errorMessageDisjunctionMarshaller[A <: ErrorMessage, B](implicit w1: JsonWriter[B], w2: JsonWriter[List[String]]): ToResponseMarshaller[DisjunctionNel[A, B]] = { 53 | def createResponseWithStatusCode(code: StatusCode, errors: List[ErrorMessage]) = HttpResponse( 54 | status = code, 55 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), w2.write(errors.map(_.description)).compactPrint) 56 | ) 57 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { 58 | case -\/(errors) if errors.toList.exists(_.isInstanceOf[FatalError]) => createResponseWithStatusCode(StatusCodes.InternalServerError, errors.toList) 59 | case -\/(errors) => createResponseWithStatusCode(StatusCodes.BadRequest, errors.toList) 60 | case \/-(success) => HttpResponse(entity = HttpEntity(ContentType(MediaTypes.`application/json`), w1.write(success).compactPrint)) 61 | } 62 | } 63 | } 64 | 65 | trait ValidationMarshaller { 66 | implicit def validationMarshaller[A1, A2, B](implicit m1: Marshaller[A1, B], m2: Marshaller[A2, B]): Marshaller[Validation[A1, A2], B] = Marshaller { implicit ec => 67 | { 68 | case Failure(a1) => m1(a1) 69 | case Success(a2) => m2(a2) 70 | } 71 | } 72 | 73 | implicit def errorValidationMarshaller[A](implicit w1: JsonWriter[A], w2: JsonWriter[List[String]]): ToResponseMarshaller[ValidationNel[String, A]] = 74 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { 75 | case Failure(errors) => HttpResponse( 76 | status = StatusCodes.BadRequest, 77 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), w2.write(errors.toList).compactPrint) 78 | ) 79 | case Success(success) => HttpResponse(entity = HttpEntity(ContentType(MediaTypes.`application/json`), w1.write(success).compactPrint)) 80 | } 81 | 82 | implicit def errorMessageValidationMarshaller[A <: ErrorMessage, B](implicit w1: JsonWriter[B], w2: JsonWriter[List[String]]): ToResponseMarshaller[ValidationNel[A, B]] = { 83 | def createResponseWithStatusCode(code: StatusCode, errors: List[ErrorMessage]) = HttpResponse( 84 | status = code, 85 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), w2.write(errors.map(_.description)).compactPrint) 86 | ) 87 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { 88 | case Failure(errors) if errors.toList.exists(_.isInstanceOf[FatalError]) => createResponseWithStatusCode(StatusCodes.InternalServerError, errors.toList) 89 | case Failure(errors) => createResponseWithStatusCode(StatusCodes.BadRequest, errors.toList) 90 | case Success(success) => HttpResponse(entity = HttpEntity(ContentType(MediaTypes.`application/json`), w1.write(success).compactPrint)) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/marshaller/Marshallers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.marshaller 18 | 19 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 20 | import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport 21 | import akka.http.scaladsl.marshalling._ 22 | import akka.http.scaladsl.model._ 23 | import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshaller } 24 | import akka.stream.Materializer 25 | import akka.stream.scaladsl.Source 26 | import akka.util.ByteString 27 | import com.github.dnvriend.component.simpleserver.dto._ 28 | import com.github.dnvriend.component.simpleserver.dto.http._ 29 | import spray.json.{ DefaultJsonProtocol, _ } 30 | 31 | import scala.concurrent.ExecutionContext 32 | import scala.xml.{ Elem, NodeSeq, XML } 33 | 34 | /** 35 | * See: http://liddellj.com/using-media-type-parameters-to-version-an-http-api/ 36 | * 37 | * As a rule, when requesting `application/json` or `application/xml` you should return the latest version and should 38 | * be the same as the latest vendor media type. 39 | * 40 | * When a client requests a representation, using the vendor specific media type which includes a version, the API should 41 | * return that representation 42 | */ 43 | object MediaVersionTypes { 44 | def customMediatype(subType: String) = MediaType.customWithFixedCharset("application", subType, HttpCharsets.`UTF-8`) 45 | 46 | val `application/vnd.acme.v1+json` = customMediatype("vnd.acme.v1+json") 47 | val `application/vnd.acme.v2+json` = customMediatype("vnd.acme.v2+json") 48 | val `application/vnd.acme.v1+xml` = customMediatype("vnd.acme.v1+xml") 49 | val `application/vnd.acme.v2+xml` = customMediatype("vnd.acme.v2+xml") 50 | } 51 | 52 | object Marshallers extends Marshallers 53 | 54 | trait Marshallers extends DefaultJsonProtocol with SprayJsonSupport with ScalaXmlSupport { 55 | implicit val personJsonFormatV1 = jsonFormat2(PersonV1) 56 | implicit val personJsonFormatV2 = jsonFormat3(PersonV2) 57 | implicit val pingJsonFormat = jsonFormat1(Ping) 58 | implicit val personJsonFormat = jsonFormat3(Person) 59 | implicit val orderDtoJsonFormat = jsonFormat2(OrderDto) 60 | 61 | def marshalPersonXmlV2(person: PersonV2): NodeSeq = 62 | 63 | 64 | { person.name } 65 | 66 | 67 | { person.age } 68 | 69 | 70 | { person.married } 71 | 72 | 73 | 74 | def marshalPersonsXmlV2(persons: Iterable[PersonV2]) = 75 | 76 | { persons.map(marshalPersonXmlV2) } 77 | 78 | 79 | def marshalPersonXmlV1(person: PersonV1): NodeSeq = 80 | 81 | 82 | { person.name } 83 | 84 | 85 | { person.age } 86 | 87 | 88 | 89 | def marshalPersonsXmlV1(persons: Iterable[PersonV1]) = 90 | 91 | { persons.map(marshalPersonXmlV1) } 92 | 93 | 94 | implicit def personsXmlFormatV1 = Marshaller.opaque[Iterable[PersonV1], NodeSeq](marshalPersonsXmlV1) 95 | 96 | implicit def personXmlFormatV1 = Marshaller.opaque[PersonV1, NodeSeq](marshalPersonXmlV1) 97 | 98 | implicit def personsXmlFormatV2 = Marshaller.opaque[Iterable[PersonV2], NodeSeq](marshalPersonsXmlV2) 99 | 100 | implicit def personXmlFormatV2 = Marshaller.opaque[PersonV2, NodeSeq](marshalPersonXmlV2) 101 | 102 | /** 103 | * From the Iterable[Person] value-object convert to a version and then marshal, wrap in an entity; 104 | * communicate with the VO in the API 105 | */ 106 | implicit def personsMarshaller(implicit ec: ExecutionContext): ToResponseMarshaller[Iterable[Person]] = Marshaller.oneOf( 107 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { persons => 108 | HttpResponse(entity = 109 | HttpEntity(ContentType(MediaTypes.`application/json`), persons.map(person => PersonV2(person.name, person.age, person.married)).toJson.compactPrint)) 110 | }, 111 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v1+json`) { persons => 112 | HttpResponse(entity = 113 | HttpEntity(ContentType(MediaVersionTypes.`application/vnd.acme.v1+json`), persons.map(person => PersonV1(person.name, person.age)).toJson.compactPrint)) 114 | }, 115 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v2+json`) { persons => 116 | HttpResponse(entity = 117 | HttpEntity(ContentType(MediaVersionTypes.`application/vnd.acme.v2+json`), persons.map(person => PersonV2(person.name, person.age, person.married)).toJson.compactPrint)) 118 | }, 119 | Marshaller.withOpenCharset(MediaTypes.`application/xml`) { (persons, charset) => 120 | HttpResponse(entity = 121 | HttpEntity.CloseDelimited( 122 | ContentType.WithCharset(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), 123 | Source.fromIterator(() => persons.iterator).mapAsync(1) { person => 124 | Marshal(persons.map(person => PersonV2(person.name, person.age, person.married))).to[NodeSeq] 125 | }.map(ns => ByteString(ns.toString)) 126 | )) 127 | }, 128 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v1+xml`) { persons => 129 | HttpResponse(entity = 130 | HttpEntity.CloseDelimited( 131 | ContentType(MediaVersionTypes.`application/vnd.acme.v1+xml`), 132 | Source.fromIterator(() => persons.iterator).mapAsync(1) { person => 133 | Marshal(persons.map(person => PersonV1(person.name, person.age))).to[NodeSeq] 134 | }.map(ns => ByteString(ns.toString)) 135 | )) 136 | }, 137 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v2+xml`) { persons => 138 | HttpResponse(entity = 139 | HttpEntity.CloseDelimited( 140 | ContentType(MediaVersionTypes.`application/vnd.acme.v2+xml`), 141 | Source.fromIterator(() => persons.iterator).mapAsync(1) { person => 142 | Marshal(persons.map(person => PersonV2(person.name, person.age, person.married))).to[NodeSeq] 143 | }.map(ns => ByteString(ns.toString)) 144 | )) 145 | } 146 | ) 147 | 148 | /** 149 | * From the Person value-object convert to a version and then marshal, wrap in an entity; 150 | * communicate with the VO in the API 151 | */ 152 | implicit def personMarshaller(implicit ec: ExecutionContext): ToResponseMarshaller[Person] = Marshaller.oneOf( 153 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { person => 154 | HttpResponse(entity = 155 | HttpEntity(ContentType(MediaTypes.`application/json`), PersonV2(person.name, person.age, person.married).toJson.compactPrint)) 156 | }, 157 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v1+json`) { person => 158 | HttpResponse(entity = 159 | HttpEntity(ContentType(MediaVersionTypes.`application/vnd.acme.v1+json`), PersonV1(person.name, person.age).toJson.compactPrint)) 160 | }, 161 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v2+json`) { person => 162 | HttpResponse(entity = 163 | HttpEntity(ContentType(MediaVersionTypes.`application/vnd.acme.v2+json`), PersonV2(person.name, person.age, person.married).toJson.compactPrint)) 164 | }, 165 | Marshaller.withOpenCharset(MediaTypes.`application/xml`) { (person, charset) => 166 | HttpResponse(entity = 167 | HttpEntity.CloseDelimited( 168 | ContentType.WithCharset(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), 169 | Source.fromFuture(Marshal(PersonV2(person.name, person.age, person.married)).to[NodeSeq]) 170 | .map(ns => ByteString(ns.toString)) 171 | )) 172 | }, 173 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v1+xml`) { person => 174 | HttpResponse(entity = 175 | HttpEntity.CloseDelimited( 176 | ContentType(MediaVersionTypes.`application/vnd.acme.v1+xml`), 177 | Source.fromFuture(Marshal(PersonV1(person.name, person.age)).to[NodeSeq]) 178 | .map(ns => ByteString(ns.toString)) 179 | )) 180 | }, 181 | Marshaller.withFixedContentType(MediaVersionTypes.`application/vnd.acme.v2+xml`) { person => 182 | HttpResponse(entity = 183 | HttpEntity.CloseDelimited( 184 | ContentType(MediaVersionTypes.`application/vnd.acme.v2+xml`), 185 | Source.fromFuture(Marshal(PersonV2(person.name, person.age, person.married)).to[NodeSeq]) 186 | .map(ns => ByteString(ns.toString)) 187 | )) 188 | } 189 | ) 190 | 191 | // curl -X POST -H "Content-Type: application/xml" -d 'John Doe25true' localhost:8080/person 192 | def personXmlEntityUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 193 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaTypes.`application/xml`).mapWithCharset { (data, charset) => 194 | val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 195 | val xml: Elem = XML.loadString(input) 196 | val name: String = (xml \\ "name").text 197 | val age: Int = (xml \\ "age").text.toInt 198 | val married: Boolean = (xml \\ "married").text.toBoolean 199 | Person(name, age, married) 200 | } 201 | 202 | // curl -X POST -H "Content-Type: application/vnd.acme.v1+xml" -d 'John Doe25' localhost:8080/person 203 | def personXmlV1EntityUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 204 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaVersionTypes.`application/vnd.acme.v1+xml`).mapWithCharset { (data, charset) => 205 | val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 206 | val xml: Elem = XML.loadString(input) 207 | val name: String = (xml \\ "name").text 208 | val age: Int = (xml \\ "age").text.toInt 209 | Person(name, age, false) 210 | } 211 | 212 | // curl -X POST -H "Content-Type: application/vnd.acme.v2+xml" -d 'John Doe25true' localhost:8080/person 213 | def personXmlV2EntityUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 214 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaVersionTypes.`application/vnd.acme.v2+xml`).mapWithCharset { (data, charset) => 215 | val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 216 | val xml: Elem = XML.loadString(input) 217 | val name: String = (xml \\ "name").text 218 | val age: Int = (xml \\ "age").text.toInt 219 | val married: Boolean = (xml \\ "married").text.toBoolean 220 | Person(name, age, married) 221 | } 222 | 223 | // curl -X POST -H "Content-Type: application/json" -d '{"age": 25, "married": false, "name": "John Doe"}' localhost:8080/person 224 | def personJsonEntityUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 225 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaTypes.`application/json`).mapWithCharset { (data, charset) => 226 | val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 227 | val tmp = input.parseJson.convertTo[PersonV2] 228 | Person(tmp.name, tmp.age, tmp.married) 229 | } 230 | 231 | // curl -X POST -H "Content-Type: application/vnd.acme.v1+json" -d '{"age": 25, "name": "John Doe"}' localhost:8080/person 232 | def personJsonV1EntityUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 233 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaVersionTypes.`application/vnd.acme.v1+json`).mapWithCharset { (data, charset) => 234 | val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 235 | val tmp = input.parseJson.convertTo[PersonV1] 236 | Person(tmp.name, tmp.age, false) 237 | } 238 | 239 | // curl -X POST -H "Content-Type: application/vnd.acme.v2+json" -d '{"age": 25, "married": false, "name": "John Doe"}' localhost:8080/person 240 | def personJsonV2EntityUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 241 | Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaVersionTypes.`application/vnd.acme.v2+json`).mapWithCharset { (data, charset) => 242 | val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name) 243 | val tmp = input.parseJson.convertTo[PersonV2] 244 | Person(tmp.name, tmp.age, tmp.married) 245 | } 246 | 247 | // will be used by the unmarshallers above 248 | implicit def personUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[Person] = 249 | Unmarshaller.firstOf[HttpEntity, Person]( 250 | personXmlEntityUnmarshaller, personXmlV1EntityUnmarshaller, personXmlV2EntityUnmarshaller, 251 | personJsonEntityUnmarshaller, personJsonV1EntityUnmarshaller, personJsonV2EntityUnmarshaller 252 | ) 253 | } 254 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/route/CsvStreamingRoute.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.route 18 | 19 | import akka.http.scaladsl.common.{ CsvEntityStreamingSupport, EntityStreamingSupport } 20 | import akka.http.scaladsl.marshalling.{ Marshaller, Marshalling } 21 | import akka.http.scaladsl.model.ContentTypes 22 | import akka.http.scaladsl.server.{ Directives, Route } 23 | import akka.util.ByteString 24 | import com.github.dnvriend.component.repository.PersonRepository 25 | import com.github.dnvriend.component.simpleserver.dto.http.Person 26 | import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport 27 | 28 | object CsvStreamingRoute extends Directives with PlayJsonSupport { 29 | implicit val personAsCsv = Marshaller.strict[Person, ByteString] { person => 30 | Marshalling.WithFixedContentType(ContentTypes.`text/csv(UTF-8)`, () => { 31 | ByteString(List(person.name.replace(",", "."), person.age, person.married).mkString(",")) 32 | }) 33 | } 34 | 35 | implicit val csvStreamingSupport: CsvEntityStreamingSupport = EntityStreamingSupport.csv() 36 | 37 | def route(dao: PersonRepository): Route = 38 | path("stream" / IntNumber) { numberOfPeople => 39 | pathEnd { 40 | get { 41 | complete(dao.people(numberOfPeople)) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/route/JsonStreamingRoute.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.route 18 | 19 | import akka.event.LoggingAdapter 20 | import akka.http.scaladsl.common.{ EntityStreamingSupport, JsonEntityStreamingSupport } 21 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 22 | import akka.http.scaladsl.server.{ Directives, Route } 23 | import akka.stream.Materializer 24 | import akka.stream.scaladsl.Flow 25 | import akka.util.ByteString 26 | import com.github.dnvriend.component.repository.PersonRepository 27 | import com.github.dnvriend.component.simpleserver.dto.http.Person 28 | import com.github.dnvriend.component.simpleserver.marshaller.Marshallers 29 | 30 | import scala.concurrent.ExecutionContext 31 | 32 | object JsonStreamingRoute extends Directives with SprayJsonSupport with Marshallers { 33 | val start = ByteString.empty 34 | val sep = ByteString("\n") 35 | val end = ByteString.empty 36 | 37 | implicit val jsonStreamingSupport: JsonEntityStreamingSupport = EntityStreamingSupport.json() 38 | .withFramingRenderer(Flow[ByteString].intersperse(start, sep, end)) 39 | .withParallelMarshalling(parallelism = 8, unordered = true) 40 | 41 | def route(dao: PersonRepository)(implicit mat: Materializer, ec: ExecutionContext): Route = 42 | path("stream" / IntNumber) { numberOfPersons => 43 | (get & pathEnd) { 44 | complete(dao.people(numberOfPersons)) 45 | } 46 | } ~ 47 | (post & path("stream") & entity(asSourceOf[Person])) { people => 48 | val total = people.log("people").runFold(0) { case (c, _) => c + 1 } 49 | complete(total.map(n => s"Received $n number of person")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/route/SimpleDisjunctionRoute.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.route 18 | 19 | import akka.http.scaladsl.server.{ Directives, Route } 20 | import com.github.dnvriend.component.simpleserver.dto.http.OrderDto 21 | import com.github.dnvriend.component.simpleserver.marshaller.DisjunctionMarshaller.{ ErrorMessage, FatalError, NonFatalError } 22 | import com.github.dnvriend.component.simpleserver.marshaller.{ DisjunctionMarshaller, Marshallers, ValidationMarshaller } 23 | 24 | object SimpleDisjunctionRoute extends Directives with DisjunctionMarshaller with ValidationMarshaller with Marshallers { 25 | import scalaz._ 26 | import Scalaz._ 27 | final case class MyFatalError(description: String) extends FatalError 28 | final case class MyNonFatalError(description: String) extends NonFatalError 29 | 30 | def route: Route = 31 | pathPrefix("disjunction" / "simple") { 32 | (get & path("failure")) { 33 | complete("failure-left".left[String]) 34 | } ~ 35 | (get & path("success")) { 36 | complete("success-right".right[String]) 37 | } 38 | } ~ 39 | pathPrefix("disjunction" / "nel") { 40 | (get & path("string" / "failure")) { 41 | complete(("failure1".failureNel[String] *> "failure2".failureNel[String]).disjunction) 42 | } ~ 43 | (get & path("nonfatal" / "failure")) { 44 | complete((MyNonFatalError("my-non-fatal-error-1").failureNel[OrderDto] *> MyNonFatalError("my-non-fatal-error-2").failureNel[OrderDto]).disjunction) 45 | } ~ 46 | (get & path("fatal" / "failure")) { 47 | complete((MyFatalError("my-fatal-error-1").failureNel[OrderDto] *> MyFatalError("my-fatal-error-2").failureNel[OrderDto]).disjunction) 48 | } ~ 49 | (get & path("combined" / "failure")) { 50 | complete((Validation.failureNel[ErrorMessage, OrderDto](MyFatalError("my-fatal-error-1")) *> Validation.failureNel[ErrorMessage, OrderDto](MyNonFatalError("my-non-fatal-error-1"))).disjunction) 51 | } ~ 52 | (get & path("nonfatal" / "success")) { 53 | complete(OrderDto(1, "test-OrderDto").successNel[ErrorMessage].disjunction) 54 | } 55 | } ~ 56 | pathPrefix("validation") { 57 | (get & path("failure")) { 58 | complete("failure".failureNel[String]) 59 | } ~ 60 | (get & path("success")) { 61 | complete("success".successNel[String]) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/route/SimpleServerRestRoutes.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.route 18 | 19 | import akka.http.scaladsl.model.StatusCodes 20 | import akka.http.scaladsl.server.{ Directives, Route } 21 | import akka.pattern.CircuitBreaker 22 | import akka.stream.Materializer 23 | import com.github.dnvriend.component.repository.PersonRepository 24 | import com.github.dnvriend.component.simpleserver.dto.http.{ Person, Ping } 25 | import com.github.dnvriend.component.simpleserver.marshaller.Marshallers 26 | import com.github.dnvriend.util.TimeUtil 27 | 28 | import scala.concurrent.ExecutionContext 29 | 30 | object SimpleServerRestRoutes extends Directives with Marshallers { 31 | def routes(dao: PersonRepository, cb: CircuitBreaker)(implicit mat: Materializer, ec: ExecutionContext): Route = 32 | logRequestResult("akka-http-test") { 33 | pathPrefix("person") { 34 | path("sync") { 35 | get { 36 | complete(dao.personSync) 37 | } 38 | } ~ 39 | path("async") { 40 | get { 41 | complete(cb.withCircuitBreaker(dao.personAsync)) 42 | } 43 | } ~ 44 | path("failed") { 45 | get { 46 | complete(cb.withCircuitBreaker(dao.personAsyncFailed)) 47 | } 48 | } ~ 49 | pathEnd { 50 | get { 51 | complete(cb.withSyncCircuitBreaker(dao.personSync)) 52 | } 53 | } ~ 54 | (post & entity(as[Person])) { person => 55 | complete(StatusCodes.Created) 56 | } 57 | } ~ pathPrefix("persons") { 58 | pathPrefix("strict" / IntNumber) { numberOfPersons => 59 | pathEnd { 60 | get { 61 | complete(cb.withSyncCircuitBreaker(dao.listOfPersons(numberOfPersons))) 62 | } 63 | } 64 | } ~ JsonStreamingRoute.route(dao) ~ CsvStreamingRoute.route(dao) 65 | } ~ (get & pathPrefix("ping")) { 66 | complete(Ping(TimeUtil.timestamp)) 67 | } ~ SimpleDisjunctionRoute.route ~ TryRoute.route 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/simpleserver/route/TryRoute.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.simpleserver.route 18 | 19 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 20 | import akka.http.scaladsl.server.{ Directives, Route } 21 | import spray.json.DefaultJsonProtocol 22 | 23 | import scala.util.Try 24 | 25 | object TryRoute extends Directives with SprayJsonSupport with DefaultJsonProtocol { 26 | def route: Route = { 27 | pathPrefix("try") { 28 | (get & path("failure")) { 29 | complete(Try((1 / 0).toString)) 30 | } ~ 31 | (get & path("success")) { 32 | complete(Try(1.toString)) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/webservices/common/Domain.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.webservices.common 18 | 19 | case class LatLon(lat: Double, lon: Double) 20 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/webservices/eetnu/EetNuClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.webservices.eetnu 18 | 19 | import akka.NotUsed 20 | import akka.actor.ActorSystem 21 | import akka.event.LoggingAdapter 22 | import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } 23 | import akka.stream.Materializer 24 | import akka.stream.scaladsl.Flow 25 | import com.github.dnvriend.component.webservices.common.LatLon 26 | import com.github.dnvriend.component.webservices.generic.HttpClient 27 | import spray.json.DefaultJsonProtocol 28 | 29 | import scala.concurrent.{ ExecutionContext, Future } 30 | import scala.util.Try 31 | import scala.util.matching.Regex 32 | 33 | // locations 34 | case class Locations(results: List[Location]) 35 | case class Location(id: Long, url: String, name: String, `type`: String, created_at: String, updated_at: Option[String], geolocation: Geolocation, counters: Counters, resources: Resources) 36 | case class Geolocation(latitude: Double, longitude: Double) 37 | case class Counters(venues: Long) 38 | case class Resources(venues: Option[String], nearby_venues: Option[String], tags: Option[String]) 39 | 40 | // venues 41 | case class Venues(results: List[Venue]) 42 | case class Venue(id: Long, name: String, category: String, telephone: Option[String], mobile: Option[String], website_url: Option[String], facebook_url: Option[String], twitter: Option[String], tagline: Option[String], rating: Option[Double], url: Option[String], created_at: String, updated_at: Option[String], address: Address, plan: String, images: Images) 43 | case class Address(street: String, zipcode: String, city: String, region: String, country: String) 44 | case class Images(original: List[String]) 45 | 46 | // reviews 47 | case class Reviews(results: List[Review]) 48 | case class Review(body: Option[String], author: Option[Author], scores: Option[Scores], created_at: Option[String], updated_at: Option[String], rating: Option[Double]) 49 | case class Author(name: Option[String], email: Option[String]) 50 | case class Scores(food: Option[Double], ambiance: Option[Double], service: Option[Double], value: Option[Double]) 51 | case class CreatedAt(createdAt: Option[String]) 52 | 53 | trait Marshallers extends DefaultJsonProtocol { 54 | implicit val countersFormat = jsonFormat1(Counters) 55 | implicit val resourcesFormat = jsonFormat3(Resources) 56 | implicit val geolocationFormat = jsonFormat2(Geolocation) 57 | implicit val locationJsonFormat = jsonFormat9(Location) 58 | implicit val locationsJsonFormat = jsonFormat1(Locations) 59 | 60 | implicit val imagesFormat = jsonFormat1(Images) 61 | implicit val addressFormat = jsonFormat5(Address) 62 | implicit val venueFormat = jsonFormat16(Venue) 63 | implicit val venuesFormat = jsonFormat1(Venues) 64 | 65 | implicit val authorJsonFormat = jsonFormat2(Author) 66 | implicit val scoresJsonFormat = jsonFormat4(Scores) 67 | implicit val reviewJsonFormat = jsonFormat6(Review) 68 | implicit val reviewsJsonFormat = jsonFormat1(Reviews) 69 | } 70 | 71 | trait EetNuClient { 72 | def venuesByZipcode(zipcode: String): Future[List[Venue]] 73 | 74 | def venuesByZipcode[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(String, T), (List[Venue], T), NotUsed] 75 | 76 | def venuesByGeo(lat: String, lon: String): Future[List[Venue]] 77 | 78 | def venuesByGeo[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(LatLon, T), (List[Venue], T), NotUsed] 79 | 80 | def venuesByQuery(query: String): Future[List[Venue]] 81 | 82 | def venueById(id: String): Future[Option[Venue]] 83 | 84 | def venueById[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(String, T), (Option[Venue], T), NotUsed] 85 | 86 | def reviewsByVenueId(id: String): Future[List[Review]] 87 | 88 | def reviewsByVenueId[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(String, T), (List[Review], T), NotUsed] 89 | } 90 | 91 | /** 92 | * see: https://docs.eet.nu/ 93 | * see: https://api.eet.nu/ 94 | */ 95 | object EetNuClient { 96 | import spray.json._ 97 | val ZipcodeWithoutSpacePattern: Regex = """([1-9][0-9]{3})([A-Za-z]{2})""".r 98 | val ZipcodeWithSpacePattern: Regex = """([1-9][0-9]{3})[\s]([A-Za-z]{2})""".r 99 | def normalizeZipcode(zipcode: String): Option[String] = zipcode.toUpperCase match { 100 | case ZipcodeWithoutSpacePattern(numbers, letters) => Option(s"$numbers $letters") 101 | case ZipcodeWithSpacePattern(numbers, letters) => Option(s"$numbers $letters") 102 | case _ => None 103 | } 104 | 105 | def responseToString(resp: HttpResponse)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[String] = 106 | HttpClient.responseToString(resp) 107 | 108 | def asVenues(json: String)(implicit reader: JsonReader[Venues]): Venues = 109 | json.parseJson.convertTo[Venues] 110 | 111 | def asVenuesFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[Venues]): Flow[(Try[HttpResponse], T), (Option[Venues], T), NotUsed] = 112 | HttpClient.responseToString[T].map { case (json, id) => (Try(json.parseJson.convertTo[Venues]).toOption, id) } 113 | 114 | def asVenueFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(Option[Venues], T), (List[Venue], T), NotUsed] = 115 | Flow[(Option[Venues], T)].map { case (venues, id) => (venues.map(_.results).getOrElse(Nil), id) } 116 | 117 | def responseToVenueFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[Venue]): Flow[(Try[HttpResponse], T), (Option[Venue], T), NotUsed] = 118 | HttpClient.responseToString[T].map { case (json, id) => (Try(json.parseJson.convertTo[Venue]).toOption, id) } 119 | 120 | def asVenue(json: String)(implicit reader: JsonReader[Venue]): Option[Venue] = 121 | Try(json.parseJson.convertTo[Venue]).toOption 122 | 123 | def asReviews(json: String)(implicit reader: JsonReader[Reviews]): Reviews = 124 | json.parseJson.convertTo[Reviews] 125 | 126 | def apply()(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter) = new EetNuClientImpl 127 | 128 | def venuesByQueryRequestFlow[T]: Flow[(String, T), (HttpRequest, T), NotUsed] = 129 | Flow[(String, T)].map { case (query, id) => (HttpClient.mkGetRequest("/venues", "", Map("query" → query)), id) } 130 | 131 | def venueByIdRequestFlow[T]: Flow[(String, T), (HttpRequest, T), NotUsed] = 132 | Flow[(String, T)].map { case (vendorId, id) => (HttpClient.mkGetRequest(s"/venues/$vendorId"), id) } 133 | 134 | def venuesByGeoRequestFlow[T]: Flow[(LatLon, T), (HttpRequest, T), NotUsed] = 135 | Flow[(LatLon, T)].map { case (LatLon(lat, lon), id) => (HttpClient.mkGetRequest("/venues", "", Map("geolocation" → s"$lat,$lon")), id) } 136 | 137 | def reviewsByVenueIdRequestFlow[T]: Flow[(String, T), (HttpRequest, T), NotUsed] = 138 | Flow[(String, T)].map { case (vendorId, id) => (HttpClient.mkGetRequest(s"/venues/$vendorId/reviews"), id) } 139 | 140 | def asReviewsFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[Reviews]): Flow[(Try[HttpResponse], T), (List[Review], T), NotUsed] = 141 | HttpClient.responseToString[T].map { case (json, id) => (Try(asReviews(json).results).toOption.getOrElse(Nil), id) } 142 | } 143 | 144 | class EetNuClientImpl()(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter) extends EetNuClient with Marshallers { 145 | import EetNuClient._ 146 | 147 | val client = HttpClient("eetnu") 148 | 149 | override def venuesByZipcode(zipcode: String): Future[List[Venue]] = 150 | normalizeZipcode(zipcode) match { 151 | case Some(zip) => venuesByQuery(zip) 152 | case None => Future.successful(Nil) 153 | } 154 | 155 | override def venuesByGeo(lat: String, lon: String): Future[List[Venue]] = 156 | client.get("/venues", "", Map("geolocation" → s"$lat,$lon")) 157 | .flatMap(responseToString) 158 | .map(asVenues) 159 | .map(_.results) 160 | 161 | override def venuesByQuery(query: String): Future[List[Venue]] = 162 | client.get("/venues", "", Map("query" → query)) 163 | .flatMap(responseToString) 164 | .map(asVenues) 165 | .map(_.results) 166 | 167 | override def venueById(id: String): Future[Option[Venue]] = 168 | client.get(s"/venues/$id") 169 | .flatMap(responseToString) 170 | .map(asVenue) 171 | 172 | override def venueById[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(String, T), (Option[Venue], T), NotUsed] = 173 | venueByIdRequestFlow[T] 174 | .via(client.cachedHostConnectionFlow[T]) 175 | .via(responseToVenueFlow[T]) 176 | 177 | override def reviewsByVenueId(id: String): Future[List[Review]] = 178 | client.get(s"/venues/$id/reviews") 179 | .flatMap(responseToString) 180 | .map(asReviews) 181 | .map(_.results) 182 | 183 | override def venuesByZipcode[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(String, T), (List[Venue], T), NotUsed] = 184 | Flow[(String, T)] 185 | .map { case (zip, id) => (normalizeZipcode(zip), id) } 186 | .collect { case (Some(zip), id) => (zip, id) } // drop elements that are no valid zipcodes (no use for them) 187 | .via(venuesByQueryRequestFlow[T]) 188 | .via(client.cachedHostConnectionFlow[T]) 189 | .via(asVenuesFlow[T]) 190 | .via(asVenueFlow[T]) 191 | 192 | override def venuesByGeo[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(LatLon, T), (List[Venue], T), NotUsed] = 193 | venuesByGeoRequestFlow[T] 194 | .via(client.cachedHostConnectionFlow[T]) 195 | .via(asVenuesFlow[T]) 196 | .via(asVenueFlow[T]) 197 | 198 | override def reviewsByVenueId[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(String, T), (List[Review], T), NotUsed] = 199 | reviewsByVenueIdRequestFlow[T] 200 | .via(client.cachedHostConnectionFlow[T]) 201 | .via(asReviewsFlow[T]) 202 | } 203 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/webservices/generic/HttpClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.webservices.generic 18 | 19 | import java.net.URLEncoder 20 | 21 | import akka.NotUsed 22 | import akka.actor.ActorSystem 23 | import akka.event.LoggingAdapter 24 | import akka.http.scaladsl.Http 25 | import akka.http.scaladsl.client.RequestBuilding 26 | import akka.http.scaladsl.model.HttpHeader.ParsingResult 27 | import akka.http.scaladsl.model._ 28 | import akka.http.scaladsl.model.headers.BasicHttpCredentials 29 | import akka.http.scaladsl.unmarshalling.Unmarshal 30 | import akka.stream.Materializer 31 | import akka.stream.scaladsl.{ Flow, Sink, Source } 32 | import com.hunorkovacs.koauth.domain.KoauthRequest 33 | import com.hunorkovacs.koauth.service.consumer.{ DefaultConsumerService, RequestWithInfo } 34 | import com.typesafe.config.Config 35 | 36 | import scala.concurrent.{ ExecutionContext, Future } 37 | import scala.util.{ Failure, Success, Try } 38 | 39 | object HttpClientConfig { 40 | def apply(config: Config): HttpClientConfig = 41 | HttpClientConfig( 42 | config.getString("host"), 43 | Try(config.getInt("port")).getOrElse(80), 44 | Try(config.getBoolean("tls")).toOption.getOrElse(false), 45 | Try(config.getString("username")).toOption.find(_.nonEmpty), 46 | Try(config.getString("password")).toOption.find(_.nonEmpty), 47 | Try(config.getString("consumerKey")).toOption.find(_.nonEmpty), 48 | Try(config.getString("consumerSecret")).toOption.find(_.nonEmpty) 49 | ) 50 | } 51 | 52 | case class HttpClientConfig(host: String, port: Int, tls: Boolean, username: Option[String], password: Option[String], consumerKey: Option[String], consumerSecret: Option[String]) 53 | 54 | object HttpClient { 55 | import scala.collection.JavaConversions._ 56 | 57 | /** 58 | * Creates a new HttpClient and uses the configuration name to look up the connection configuration 59 | */ 60 | def apply(name: String)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter): HttpClient = 61 | new HttpClient(HttpClientConfig(system.settings.config.getConfig(s"webservices.$name"))) 62 | 63 | /** 64 | * Creates a new HttpClient based on typesafe configuration 65 | */ 66 | def apply(config: Config)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter): HttpClient = 67 | new HttpClient(HttpClientConfig(config)) 68 | 69 | /** 70 | * Creates a new HttpClient based on the configuration given in the constructor 71 | */ 72 | def apply(host: String, port: Int, tls: Boolean, username: Option[String] = None, password: Option[String] = None, consumerKey: Option[String] = None, consumerSecret: Option[String] = None)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter): HttpClient = 73 | new HttpClient(HttpClientConfig(host, port, tls, username, password, consumerKey, consumerSecret)) 74 | 75 | /** 76 | * Translates a string into application/x-www-form-urlencoded format using 'UTF-8'. 77 | * This method uses the supplied encoding scheme to obtain the bytes for unsafe characters. 78 | */ 79 | def encode(value: String): String = URLEncoder.encode(value, "UTF-8") 80 | 81 | def responseToString(response: HttpResponse)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[String] = response.status match { 82 | // case StatusCodes.OK => Unmarshal(response.entity).to[String] 83 | // case StatusCodes.NotFound => Unmarshal(response.entity).to[String] 84 | case status => Unmarshal(response.entity).to[String] 85 | } 86 | 87 | def responseToString[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(Try[HttpResponse], T), (String, T), NotUsed] = 88 | Flow[(Try[HttpResponse], T)].mapAsync(1) { 89 | case (Failure(t), e) => Future.failed(t) 90 | case (Success(resp), e) => responseToString(resp).map(str => (str, e)) 91 | } 92 | 93 | def queryString(queryParams: Map[String, String]): String = 94 | if (queryParams.nonEmpty) 95 | "?" + queryParams 96 | .filterNot { 97 | case (key, value) => key.length == 0 98 | }.mapValues(encode) 99 | .toList 100 | .map { 101 | case (key, value) => s"$key=$value" 102 | }.mkString("&") 103 | else "" 104 | 105 | def header(key: String, value: String): Option[HttpHeader] = 106 | HttpHeader.parse(key, value) match { 107 | case ParsingResult.Ok(header, errors) => Option(header) 108 | case _ => None 109 | } 110 | 111 | def headers(headersMap: Map[String, String]): List[HttpHeader] = 112 | headersMap.flatMap { 113 | case (key, value) => header(key, value) 114 | }.toList 115 | 116 | /** 117 | * A cached host connection pool Flow 118 | */ 119 | def cachedConnection[T](host: String, port: Int)(implicit system: ActorSystem, mat: Materializer): Flow[(HttpRequest, T), (Try[HttpResponse], T), Http.HostConnectionPool] = 120 | Http().cachedHostConnectionPool[T](host, port) 121 | 122 | /** 123 | * An encrypted cached host connection pool Flow 124 | */ 125 | def cachedTlsConnection[T](host: String, port: Int)(implicit system: ActorSystem, mat: Materializer): Flow[(HttpRequest, T), (Try[HttpResponse], T), Http.HostConnectionPool] = 126 | Http().cachedHostConnectionPoolHttps[T](host, port) 127 | 128 | /** 129 | * An encrypted HTTP client connection to the given endpoint. 130 | */ 131 | def tlsConnection(host: String, port: Int)(implicit system: ActorSystem): Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = 132 | Http().outgoingConnectionHttps(host, port) 133 | 134 | /** 135 | * A HTTP client connection to the given endpoint. 136 | */ 137 | def httpConnection(host: String, port: Int)(implicit system: ActorSystem): Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = 138 | Http().outgoingConnection(host, port) 139 | 140 | def cachedConnection[T](config: HttpClientConfig)(implicit system: ActorSystem, mat: Materializer): Flow[(HttpRequest, T), (Try[HttpResponse], T), Http.HostConnectionPool] = 141 | if (config.tls) cachedTlsConnection(config.host, config.port) else 142 | cachedConnection(config.host, config.port) 143 | 144 | /** 145 | * Returns a flow that will be configured based on the client's config, that accepts HttpRequest elements and outputs HttpResponse elements 146 | * It materializes a Future[Http.OutgoingConnection] 147 | */ 148 | def connection(config: HttpClientConfig)(implicit system: ActorSystem): Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = 149 | if (config.tls) tlsConnection(config.host, config.port) else 150 | httpConnection(config.host, config.port) 151 | 152 | def cachedConnectionPipeline[T](config: HttpClientConfig)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(HttpRequest, T), (Try[HttpResponse], T), NotUsed] = 153 | Flow[(HttpRequest, T)].mapAsync(1) { 154 | case (request, id) => addCredentials(config)(request).map(req => (req, id)) 155 | }.via(cachedConnection(config)) 156 | 157 | def singleRequestPipeline(request: HttpRequest, config: HttpClientConfig)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[HttpResponse] = 158 | Source.single(request) 159 | .mapAsync(1)(addCredentials(config)) 160 | .via(connection(config)) 161 | .runWith(Sink.head) 162 | 163 | def basicAuthenticationCredentials(username: String, password: String)(implicit ec: ExecutionContext): HttpRequest => Future[HttpRequest] = { request => 164 | Future.successful(RequestBuilding.addCredentials(BasicHttpCredentials(username, password))(request)) 165 | } 166 | 167 | def oneLeggedOAuth1Credentials(uri: String, consumerKey: String, consumerSecret: String, tls: Boolean, host: String)(implicit ec: ExecutionContext): HttpRequest => Future[HttpRequest] = { request => 168 | val scheme: String = if (tls) "https" else "http" 169 | def consumerService = new DefaultConsumerService(ec) 170 | // please note that the used URL (and request params) must be the same as the request we send the request to!! 171 | def koAuthRequest(url: String) = KoauthRequest("GET", url, None, None) 172 | def oAuthHeader(uri: String): Future[RequestWithInfo] = consumerService.createOauthenticatedRequest(koAuthRequest(s"$scheme://$host$uri"), consumerKey, consumerSecret, "", "") 173 | oAuthHeader(uri).map { oauth => 174 | request.addHeader(header("Authorization", oauth.header).orNull) 175 | } 176 | } 177 | 178 | private def addCredentials(config: HttpClientConfig)(request: HttpRequest)(implicit ec: ExecutionContext): Future[HttpRequest] = config match { 179 | case HttpClientConfig(_, _, _, Some(username), Some(password), None, None) => 180 | basicAuthenticationCredentials(username, password)(ec)(request) 181 | case HttpClientConfig(_, _, _, None, None, Some(consumerKey), Some(consumerSecret)) => 182 | oneLeggedOAuth1Credentials(request.uri.toString(), consumerKey, consumerSecret, config.tls, config.host)(ec)(request) 183 | case _ => Future.successful(request) 184 | } 185 | 186 | def mkEntity(body: String): HttpEntity.Strict = HttpEntity(ContentTypes.`application/json`, body) 187 | 188 | def mkRequest(requestBuilder: RequestBuilding#RequestBuilder, url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty) = 189 | requestBuilder(url + queryString(queryParamsMap), mkEntity(body)).addHeaders(headers(headersMap)) 190 | 191 | def mkGetRequest(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty) = 192 | mkRequest(RequestBuilding.Get, url, body, queryParamsMap, headersMap) 193 | 194 | def mkPostRequest(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty) = 195 | mkRequest(RequestBuilding.Post, url, body, queryParamsMap, headersMap) 196 | } 197 | 198 | class HttpClient(val config: HttpClientConfig)(implicit val system: ActorSystem, val log: LoggingAdapter, val ec: ExecutionContext, val mat: Materializer) extends RequestBuilding { 199 | import HttpClient._ 200 | 201 | /** 202 | * A cached host connection pool Flow that will be configured based on the client configuration. It accepts 'tagged' tuples of 203 | * (HttpRequest, T) elements and outputs 'tagged' tuples of (Try[HttpResponse], T) elements 204 | */ 205 | def cachedHostConnectionFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(HttpRequest, T), (Try[HttpResponse], T), NotUsed] = 206 | cachedConnectionPipeline(config) 207 | 208 | def get(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 209 | singleRequestPipeline(mkRequest(RequestBuilding.Get, url, body, queryParamsMap, headersMap), config) 210 | 211 | def post(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 212 | singleRequestPipeline(mkRequest(RequestBuilding.Post, url, body, queryParamsMap, headersMap), config) 213 | 214 | def put(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 215 | singleRequestPipeline(mkRequest(RequestBuilding.Put, url, body, queryParamsMap, headersMap), config) 216 | 217 | def patch(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 218 | singleRequestPipeline(mkRequest(RequestBuilding.Patch, url, body, queryParamsMap, headersMap), config) 219 | 220 | def delete(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 221 | singleRequestPipeline(mkRequest(RequestBuilding.Delete, url, body, queryParamsMap, headersMap), config) 222 | 223 | def options(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 224 | singleRequestPipeline(mkRequest(RequestBuilding.Options, url, body, queryParamsMap, headersMap), config) 225 | 226 | def head(url: String, body: String = "", queryParamsMap: Map[String, String] = Map.empty, headersMap: Map[String, String] = Map.empty): Future[HttpResponse] = 227 | singleRequestPipeline(mkRequest(RequestBuilding.Head, url, body, queryParamsMap, headersMap), config) 228 | } 229 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/webservices/iens/IensClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.webservices.iens 18 | 19 | import akka.NotUsed 20 | import akka.actor.ActorSystem 21 | import akka.event.LoggingAdapter 22 | import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } 23 | import akka.stream.Materializer 24 | import akka.stream.scaladsl.Flow 25 | import com.github.dnvriend.component.webservices.common.LatLon 26 | import com.github.dnvriend.component.webservices.generic.HttpClient 27 | import spray.json.DefaultJsonProtocol 28 | 29 | import scala.concurrent.{ ExecutionContext, Future } 30 | import scala.util.Try 31 | 32 | // SearchRestaurants 33 | case class Rating(name: Option[String], rating: Option[String]) 34 | case class SearchRestaurant(id: Long, name: Option[String], imageurl: Option[String], kitchenname: Option[String], address: Option[String], zipcode: Option[String], place: Option[String], country: Option[String], latitude: Option[String], longitude: Option[String], ratings: Option[List[Rating]], averageprice: Option[Double], seatmeid: Option[Long], distance: Option[Double]) 35 | case class SearchRestaurantsResponse(result: Boolean, message: String, restaurants: List[SearchRestaurant], count: Long) 36 | 37 | // GetRestaurantDetails 38 | case class RestaurantDetail(id: String, name: Option[String], imageurl: Option[String], kitchenname: Option[String], address: Option[String], zipcode: Option[String], place: Option[String], country: Option[String], latitude: Option[String], longitude: Option[String], ratings: Option[List[Rating]], averageprice: Option[String], seatmeid: Option[String]) 39 | case class GetRestaurantDetailsResponse(result: Boolean, message: String, restaurant: Option[RestaurantDetail]) 40 | 41 | // GetReviews 42 | case class Review(restaurant_id: String, restaurant_name: Option[String], name: Option[String], user_star_value: Option[Int], date: Option[String], description: Option[String], ratings: List[Rating]) 43 | case class GetReviewResponse(result: Boolean, message: String, reviews: List[Review], num_reviews: String) 44 | 45 | trait Marshallers extends DefaultJsonProtocol { 46 | implicit val ratingJsonFormat = jsonFormat2(Rating) 47 | implicit val restaurantJsonFormat = jsonFormat14(SearchRestaurant) 48 | implicit val searchRestaurantsResponseJsonFormat = jsonFormat4(SearchRestaurantsResponse) 49 | implicit val restaurantDetailJsonFormat = jsonFormat13(RestaurantDetail) 50 | implicit val getRestaurantDetailsResponseJsonFormat = jsonFormat3(GetRestaurantDetailsResponse) 51 | implicit val reviewJsonFormat = jsonFormat7(Review) 52 | implicit val getReviewResponseJsonFormat = jsonFormat4(GetReviewResponse) 53 | } 54 | 55 | object IensClient { 56 | import spray.json._ 57 | def apply()(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter) = new IensClientImpl 58 | 59 | def responseToString(resp: HttpResponse)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[String] = 60 | HttpClient.responseToString(resp) 61 | 62 | def asSearchRestaurantsResponse(json: String)(implicit reader: JsonReader[SearchRestaurantsResponse]): SearchRestaurantsResponse = 63 | json.parseJson.convertTo[SearchRestaurantsResponse] 64 | 65 | def asGetRestaurantDetailsResponse(json: String)(implicit reader: JsonReader[GetRestaurantDetailsResponse]): GetRestaurantDetailsResponse = 66 | json.parseJson.convertTo[GetRestaurantDetailsResponse] 67 | 68 | def asGetReviewResponse(json: String)(implicit reader: JsonReader[GetReviewResponse]): GetReviewResponse = 69 | json.parseJson.convertTo[GetReviewResponse] 70 | 71 | def restaurantsByGeoRequestFlow[T]: Flow[(LatLon, T), (HttpRequest, T), NotUsed] = 72 | Flow[(LatLon, T)].map { 73 | case (LatLon(lat, lon), id) => 74 | val requestParams = Map( 75 | "id" → "searchrestaurants", 76 | "limit" → "1", 77 | "offset" → "0", 78 | "latitude" → lat.toString, 79 | "longitude" → lon.toString 80 | ) 81 | (HttpClient.mkGetRequest("/rest/restaurant", "", requestParams), id) 82 | } 83 | 84 | def asSearchRestaurantsResponseFlow[A](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[SearchRestaurantsResponse]): Flow[(Try[HttpResponse], A), (Try[SearchRestaurantsResponse], A), NotUsed] = 85 | HttpClient.responseToString[A].map { 86 | case (json, id) => (Try(json.parseJson.convertTo[SearchRestaurantsResponse]), id) 87 | } 88 | 89 | def restaurantDetailsRequestFlow[T]: Flow[(Long, T), (HttpRequest, T), NotUsed] = 90 | Flow[(Long, T)].map { 91 | case (vendorId, id) => (HttpClient.mkGetRequest("/rest/restaurant", queryParamsMap = Map("id" → "getrestaurantdetails", "restaurant_id" → vendorId.toString)), id) 92 | } 93 | 94 | def asGetRestaurantDetailsResponseFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[GetRestaurantDetailsResponse]): Flow[(Try[HttpResponse], T), (Try[GetRestaurantDetailsResponse], T), NotUsed] = 95 | HttpClient.responseToString[T].map { 96 | case (json, id) => (Try(json.parseJson.convertTo[GetRestaurantDetailsResponse]), id) 97 | } 98 | 99 | def reviewsRequestFlow[T]: Flow[(Long, T), (HttpRequest, T), NotUsed] = 100 | Flow[(Long, T)].map { 101 | case (vendorId, id) => (HttpClient.mkGetRequest("/rest/review", queryParamsMap = Map("restaurant_id" → id.toString)), id) 102 | } 103 | 104 | def asGetReviewResponseFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[GetReviewResponse]): Flow[(Try[HttpResponse], T), (Try[GetReviewResponse], T), NotUsed] = 105 | HttpClient.responseToString[T].map { 106 | case (json, id) => (Try(json.parseJson.convertTo[GetReviewResponse]), id) 107 | } 108 | } 109 | 110 | trait IensClient { 111 | def restaurantsByGeo(langitude: Double, longitude: Double, limit: Int = Int.MaxValue, offset: Int = 0): Future[SearchRestaurantsResponse] 112 | 113 | def restaurantsByGeo[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(LatLon, T), (Try[SearchRestaurantsResponse], T), NotUsed] 114 | 115 | def restaurantDetails(id: Long): Future[GetRestaurantDetailsResponse] 116 | 117 | def restaurantDetails[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(Long, T), (Try[GetRestaurantDetailsResponse], T), NotUsed] 118 | 119 | def reviews(id: Long): Future[GetReviewResponse] 120 | 121 | def reviews[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(Long, T), (Try[GetReviewResponse], T), NotUsed] 122 | } 123 | 124 | class IensClientImpl()(implicit val system: ActorSystem, val mat: Materializer, val ec: ExecutionContext, val log: LoggingAdapter) extends IensClient with Marshallers { 125 | import IensClient._ 126 | 127 | private val client = HttpClient("iens") 128 | 129 | override def restaurantsByGeo(langitude: Double, longitude: Double, limit: Int, offset: Int): Future[SearchRestaurantsResponse] = { 130 | client.get("/rest/restaurant", queryParamsMap = Map( 131 | "id" → "searchrestaurants", 132 | "limit" → limit.toString, 133 | "offset" → offset.toString, 134 | "latitude" → langitude.toString, 135 | "longitude" → longitude.toString 136 | )) 137 | .flatMap(responseToString) 138 | .map(asSearchRestaurantsResponse) 139 | } 140 | 141 | override def restaurantsByGeo[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(LatLon, T), (Try[SearchRestaurantsResponse], T), NotUsed] = 142 | restaurantsByGeoRequestFlow[T] 143 | .via(client.cachedHostConnectionFlow[T]) 144 | .via(asSearchRestaurantsResponseFlow[T]) 145 | 146 | override def restaurantDetails(id: Long): Future[GetRestaurantDetailsResponse] = { 147 | client.get("/rest/restaurant", queryParamsMap = Map("id" → "getrestaurantdetails", "restaurant_id" → id.toString)) 148 | .flatMap(responseToString) 149 | .map(asGetRestaurantDetailsResponse) 150 | } 151 | 152 | override def restaurantDetails[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(Long, T), (Try[GetRestaurantDetailsResponse], T), NotUsed] = 153 | restaurantDetailsRequestFlow[T] 154 | .via(client.cachedHostConnectionFlow[T]) 155 | .via(asGetRestaurantDetailsResponseFlow[T]) 156 | 157 | override def reviews(id: Long): Future[GetReviewResponse] = 158 | client.get("/rest/review", queryParamsMap = Map("restaurant_id" → id.toString)) 159 | .flatMap(responseToString) 160 | .map(asGetReviewResponse) 161 | 162 | override def reviews[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(Long, T), (Try[GetReviewResponse], T), NotUsed] = 163 | reviewsRequestFlow[T] 164 | .via(client.cachedHostConnectionFlow[T]) 165 | .via(asGetReviewResponseFlow[T]) 166 | } 167 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/webservices/postcode/PostcodeClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.webservices.postcode 18 | 19 | import akka.NotUsed 20 | import akka.actor.ActorSystem 21 | import akka.event.LoggingAdapter 22 | import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } 23 | import akka.stream.Materializer 24 | import akka.stream.scaladsl.Flow 25 | import com.github.dnvriend.component.webservices.generic.HttpClient 26 | import spray.json.DefaultJsonProtocol 27 | 28 | import scala.concurrent.{ ExecutionContext, Future } 29 | import scala.util.Try 30 | import scala.util.matching.Regex 31 | 32 | case class Address( 33 | street: String, 34 | houseNumber: Int, 35 | houseNumberAddition: String, 36 | postcode: String, 37 | city: String, 38 | municipality: String, 39 | province: String, 40 | rdX: Option[Int], 41 | rdY: Option[Int], 42 | latitude: Double, 43 | longitude: Double, 44 | bagNumberDesignationId: String, 45 | bagAddressableObjectId: String, 46 | addressType: String, 47 | purposes: Option[List[String]], 48 | surfaceArea: Int, 49 | houseNumberAdditions: List[String] 50 | ) 51 | 52 | trait Marshallers extends DefaultJsonProtocol { 53 | implicit val addressJsonFormat = jsonFormat17(Address) 54 | } 55 | 56 | case class GetAddressRequest(zip: String, houseNumber: String) 57 | 58 | trait PostcodeClient { 59 | def address(postcode: String, houseNumber: Int): Future[Option[Address]] 60 | 61 | def address[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(GetAddressRequest, T), (Option[Address], T), NotUsed] 62 | } 63 | 64 | object PostcodeClient { 65 | import spray.json._ 66 | val ZipcodeWithoutSpacePattern: Regex = """([1-9][0-9]{3})([A-Za-z]{2})""".r 67 | val ZipcodeWithSpacePattern: Regex = """([1-9][0-9]{3})[\s]([A-Za-z]{2})""".r 68 | 69 | def mapToAddress(json: String)(implicit reader: JsonReader[Address]): Option[Address] = 70 | Try(json.parseJson.convertTo[Address]).toOption 71 | 72 | def responseToString(resp: HttpResponse)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[String] = 73 | HttpClient.responseToString(resp) 74 | 75 | def getAddressRequestFlow[T]: Flow[(GetAddressRequest, T), (HttpRequest, T), NotUsed] = 76 | Flow[(GetAddressRequest, T)].map { case (request, id) => (HttpClient.mkGetRequest(s"/rest/addresses/${request.zip}/${request.houseNumber}/"), id) } 77 | 78 | def mapResponseToAddressFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[Address]): Flow[(Try[HttpResponse], T), (Option[Address], T), NotUsed] = 79 | HttpClient.responseToString[T].map { case (json, id) => (mapToAddress(json), id) } 80 | /** 81 | * Returns an option of the zipcode without spaces, if there were any. Any invalid zipcode 82 | * will be returned as None 83 | * 84 | * @param zipcode 85 | * @return 86 | */ 87 | def normalizeZipcode(zipcode: String): Option[String] = zipcode.toUpperCase match { 88 | case ZipcodeWithoutSpacePattern(numbers, letters) => Option(s"$numbers$letters") 89 | case ZipcodeWithSpacePattern(numbers, letters) => Option(s"$numbers$letters") 90 | case _ => None 91 | } 92 | 93 | def apply()(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter) = new PostcodeClientImpl 94 | } 95 | 96 | class PostcodeClientImpl()(implicit val system: ActorSystem, val mat: Materializer, val ec: ExecutionContext, val log: LoggingAdapter) extends PostcodeClient with Marshallers { 97 | import PostcodeClient._ 98 | private val client = HttpClient("postcode") 99 | 100 | override def address(postcode: String, houseNumber: Int): Future[Option[Address]] = 101 | normalizeZipcode(postcode) match { 102 | case Some(zip) => client.get(s"/rest/addresses/$zip/$houseNumber/") 103 | .flatMap(responseToString).map(mapToAddress) 104 | case None => Future.successful(None) 105 | } 106 | 107 | override def address[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(GetAddressRequest, T), (Option[Address], T), NotUsed] = 108 | getAddressRequestFlow[T] 109 | .via(client.cachedHostConnectionFlow[T]) 110 | .via(mapResponseToAddressFlow[T]) 111 | } 112 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/component/webservices/weather/WeatherClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.component.webservices.weather 18 | 19 | import akka.NotUsed 20 | import akka.actor.ActorSystem 21 | import akka.event.LoggingAdapter 22 | import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } 23 | import akka.stream.Materializer 24 | import akka.stream.scaladsl.Flow 25 | import com.github.dnvriend.component.webservices.generic.HttpClient 26 | import spray.json.DefaultJsonProtocol 27 | 28 | import scala.concurrent.{ ExecutionContext, Future } 29 | import scala.util.Try 30 | 31 | case class Wind(speed: Double, deg: Double) 32 | case class Main(temp: Double, temp_min: Double, temp_max: Double, pressure: Double, sea_level: Option[Double], grnd_level: Option[Double], humidity: Int) 33 | case class Cloud(all: Int) 34 | case class Weather(id: Int, main: String, description: String, icon: String) 35 | case class Sys(message: Double, country: String, sunrise: Long, sunset: Long) 36 | case class Coord(lon: Double, lat: Double) 37 | case class WeatherResult(coord: Coord, sys: Sys, weather: List[Weather], base: String, main: Main, wind: Wind, clouds: Cloud, dt: Long, id: Int, name: String, cod: Int) 38 | 39 | trait Marshallers extends DefaultJsonProtocol { 40 | implicit val windJsonFormat = jsonFormat2(Wind) 41 | implicit val mainJsonFormat = jsonFormat7(Main) 42 | implicit val cloudJsonFormat = jsonFormat1(Cloud) 43 | implicit val weatherJsonFormat = jsonFormat4(Weather) 44 | implicit val sysJsonFormat = jsonFormat4(Sys) 45 | implicit val coordJsonFormat = jsonFormat2(Coord) 46 | implicit val weatherResultJsonFormat = jsonFormat11(WeatherResult) 47 | } 48 | 49 | case class GetWeatherRequest(zip: String, country: String) 50 | 51 | trait OpenWeatherApi { 52 | def getWeather(zip: String, country: String): Future[Option[WeatherResult]] 53 | 54 | def getWeather[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(GetWeatherRequest, T), (Option[WeatherResult], T), NotUsed] 55 | } 56 | 57 | object OpenWeatherApi { 58 | import spray.json._ 59 | def apply()(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter) = new OpenWeatherApiImpl 60 | 61 | def mapResponseToWeatherResult(json: String)(implicit reader: JsonReader[WeatherResult]): Option[WeatherResult] = 62 | Try(json.parseJson.convertTo[WeatherResult]).toOption 63 | 64 | def responseToString(resp: HttpResponse)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[String] = 65 | HttpClient.responseToString(resp) 66 | 67 | def getWeatherRequestFlow[T]: Flow[(GetWeatherRequest, T), (HttpRequest, T), NotUsed] = 68 | Flow[(GetWeatherRequest, T)].map { case (request, id) => (HttpClient.mkGetRequest(s"/data/2.5/weather?zip=${request.zip},${request.country}"), id) } 69 | 70 | def mapResponseToWeatherResultFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[WeatherResult]): Flow[(Try[HttpResponse], T), (Option[WeatherResult], T), NotUsed] = 71 | HttpClient.responseToString[T].map { case (json, id) => (mapResponseToWeatherResult(json), id) } 72 | } 73 | 74 | class OpenWeatherApiImpl()(implicit val system: ActorSystem, val ec: ExecutionContext, val mat: Materializer, val log: LoggingAdapter) extends OpenWeatherApi with Marshallers { 75 | import OpenWeatherApi._ 76 | 77 | private val client = HttpClient("weather") 78 | 79 | override def getWeather(zip: String, country: String): Future[Option[WeatherResult]] = 80 | client.get(s"/data/2.5/weather?zip=$zip,$country"). 81 | flatMap(responseToString) 82 | .map(mapResponseToWeatherResult) 83 | 84 | override def getWeather[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(GetWeatherRequest, T), (Option[WeatherResult], T), NotUsed] = 85 | getWeatherRequestFlow[T] 86 | .via(client.cachedHostConnectionFlow[T]) 87 | .via(mapResponseToWeatherResultFlow[T]) 88 | } 89 | -------------------------------------------------------------------------------- /app/com/github/dnvriend/util/TimeUtil.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.util 18 | 19 | import java.text.SimpleDateFormat 20 | import java.util.Date 21 | 22 | object TimeUtil { 23 | def timestamp: String = { 24 | val sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX") 25 | sdf.format(new Date) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "akka-http-test" 2 | 3 | version := "1.0.0" 4 | 5 | scalaVersion := "2.11.8" 6 | 7 | resolvers += Resolver.bintrayRepo("hseeberger", "maven") 8 | 9 | val akkaVersion = "2.4.17" 10 | // akka v2.5.0 doesn't work with Play 11 | //val akkaVersion = "2.5.0" 12 | val httpVersion = "10.0.5" 13 | val scalazVersion = "7.2.10" 14 | 15 | libraryDependencies += "com.typesafe.akka" %% "akka-actor" % akkaVersion 16 | libraryDependencies += "com.typesafe.akka" %% "akka-slf4j" % akkaVersion 17 | libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % httpVersion 18 | libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % httpVersion 19 | libraryDependencies += "com.typesafe.akka" %% "akka-http-xml" % httpVersion 20 | libraryDependencies += "de.heikoseeberger" %% "akka-http-play-json" % "1.15.0" 21 | libraryDependencies += "com.github.nscala-time" %% "nscala-time" % "2.16.0" 22 | libraryDependencies += "org.scalaz" %% "scalaz-core" % scalazVersion 23 | libraryDependencies += "org.typelevel" %% "scalaz-outlaws" % "0.2" 24 | libraryDependencies += "com.hunorkovacs" %% "koauth" % "1.1.0" exclude("com.typesafe.akka", "akka-actor_2.11") exclude("org.specs2", "specs2_2.11") 25 | libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.1" 26 | libraryDependencies += "com.h2database" % "h2" % "1.4.194" 27 | libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % httpVersion % Test 28 | libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test 29 | libraryDependencies += "org.typelevel" %% "scalaz-scalatest" % "1.1.2" % Test 30 | libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % Test 31 | 32 | licenses += ("Apache-2.0", url("http://opensource.org/licenses/apache2.0.php")) 33 | 34 | // enable updating file headers // 35 | import de.heikoseeberger.sbtheader.license.Apache2_0 36 | 37 | headers := Map( 38 | "scala" -> Apache2_0("2016", "Dennis Vriend"), 39 | "conf" -> Apache2_0("2016", "Dennis Vriend", "#") 40 | ) 41 | 42 | // enable scala code formatting // 43 | import com.typesafe.sbt.SbtScalariform 44 | import scalariform.formatter.preferences._ 45 | 46 | // Scalariform settings 47 | SbtScalariform.autoImport.scalariformPreferences := SbtScalariform.autoImport.scalariformPreferences.value 48 | .setPreference(AlignSingleLineCaseStatements, true) 49 | .setPreference(AlignSingleLineCaseStatements.MaxArrowIndent, 100) 50 | .setPreference(DoubleIndentClassDeclaration, true) 51 | 52 | // enable plugins // 53 | enablePlugins(AutomateHeaderPlugin, SbtScalariform, PlayScala) 54 | 55 | // ==================================== 56 | // ==== Lightbend Monitoring (Cinnamon) 57 | // ==================================== 58 | // Enable the Cinnamon Lightbend Monitoring sbt plugin 59 | enablePlugins (Cinnamon) 60 | 61 | libraryDependencies += Cinnamon.library.cinnamonSandbox 62 | 63 | // Add the Monitoring Agent for run and test 64 | cinnamon in run := true 65 | cinnamon in test := true 66 | 67 | // ======================================= 68 | // ==== Lightbend Orchestration (ConductR) 69 | // ======================================= 70 | // read: https://github.com/typesafehub/conductr-lib#play25-conductr-bundle-lib 71 | // ======================================= 72 | enablePlugins(PlayBundlePlugin) 73 | 74 | // Declares endpoints. The default is Map("web" -> Endpoint("http", 0, Set.empty)). 75 | // The endpoint key is used to form a set of environment variables for your components, 76 | // e.g. for the endpoint key "web" ConductR creates the environment variable WEB_BIND_PORT. 77 | BundleKeys.endpoints := Map( 78 | "play" -> Endpoint(bindProtocol = "http", bindPort = 0, services = Set(URI("http://:9000/play"))), 79 | "akka-remote" -> Endpoint("tcp") 80 | ) 81 | 82 | normalizedName in Bundle := name.value // the human readable name for your bundle 83 | 84 | BundleKeys.system := name.value + "-system" // represents the clustered ActorSystem 85 | 86 | BundleKeys.startCommand += "-Dhttp.address=$PLAY_BIND_IP -Dhttp.port=$PLAY_BIND_PORT" 87 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Dennis Vriend 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | akka { 16 | 17 | stdout-loglevel = off // defaults to WARNING can be disabled with off. The stdout-loglevel is only in effect during system startup and shutdown 18 | log-dead-letters-during-shutdown = on 19 | loglevel = DEBUG 20 | log-dead-letters = on 21 | log-config-on-start = off // Log the complete configuration at INFO level when the actor system is started 22 | 23 | loggers = ["akka.event.slf4j.Slf4jLogger"] 24 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 25 | 26 | actor { 27 | debug { 28 | receive = off // log all messages sent to an actor if that actors receive method is a LoggingReceive 29 | autoreceive = off // log all special messages like Kill, PoisoffPill etc sent to all actors 30 | lifecycle = off // log all actor lifecycle events of all actors 31 | fsm = off // enable logging of all events, transitioffs and timers of FSM Actors that extend LoggingFSM 32 | event-stream = off // enable logging of subscriptions (subscribe/unsubscribe) on the ActorSystem.eventStream 33 | } 34 | } 35 | 36 | stream { 37 | materializer { 38 | debug-logging = on // Enable additional troubleshooting logging at DEBUG log level 39 | } 40 | } 41 | 42 | http { 43 | host-connection-pool { 44 | max-connections = 4 45 | max-retries = 20 46 | } 47 | } 48 | } 49 | 50 | http { 51 | interface = "0.0.0.0" 52 | port = 9001 53 | } 54 | 55 | webservices { 56 | 57 | eetnu { 58 | host = "api.eet.nu" 59 | port = 443 60 | tls = true 61 | } 62 | 63 | iens { 64 | host = "www.iens.nl" 65 | port = 443 66 | tls = true 67 | consumerKey = "YOUR_KEY_HERE" 68 | consumerSecret = "YOUR_SECRET_HERE" 69 | } 70 | 71 | postcode { 72 | host = "api.postcode.nl" 73 | port = 443 74 | tls = true 75 | username = "YOUR_USERNAME_HERE" 76 | password = "YOUR_PASSWORD_HERE" 77 | } 78 | 79 | weather { 80 | host = "api.openweathermap.org" 81 | port = 80 82 | tls = false 83 | } 84 | } 85 | 86 | play.akka.actor-system = "PlayTestSystem" 87 | 88 | play.crypto.secret = "4284168" 89 | 90 | //play.modules.enabled += "com.github.dnvriend.Module" 91 | 92 | # Default database configuration 93 | slick.dbs.default.driver="slick.driver.H2Driver$" 94 | slick.dbs.default.db.driver="org.h2.Driver" 95 | slick.dbs.default.db.url="jdbc:h2:mem:play" 96 | slick.dbs.default.db.connectionTimeout=5000 97 | slick.dbs.default.db.validationTimeout=5000 98 | slick.dbs.default.db.initializationFailFast=false 99 | slick.dbs.default.db.numThreads=20 100 | slick.dbs.default.db.maxConnections=40 101 | slick.dbs.default.db.minConnections=1 102 | 103 | cinnamon.akka { 104 | actors { 105 | "/user/*" { 106 | report-by = class 107 | } 108 | //"/system/*" { 109 | // report-by = class 110 | //} 111 | } 112 | } 113 | 114 | cinnamon.trace { 115 | thresholds { 116 | "foo-request" = "500 millis" 117 | "bar-response" = "500 millis" 118 | } 119 | } -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | debug 7 | 8 | 9 | %date{ISO8601} - %logger -> %-5level[%thread] %logger{0} - %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | GET /helloworld com.github.dnvriend.component.helloworld.controller.HelloWorldController.getHelloWorld 2 | GET /helloworld/:id com.github.dnvriend.component.helloworld.controller.HelloWorldController.getHelloWorldOpt(id: Long) 3 | GET /helloworld/mb/:id com.github.dnvriend.component.helloworld.controller.HelloWorldController.getHelloWorldMB(id: Long) 4 | GET /helloworld/d/:id com.github.dnvriend.component.helloworld.controller.HelloWorldController.getHelloWorldD(id: Long) 5 | GET /helloworld/v/:id com.github.dnvriend.component.helloworld.controller.HelloWorldController.getHelloWorldV(id: Long) 6 | GET /helloworld/vn/:id com.github.dnvriend.component.helloworld.controller.HelloWorldController.getHelloWorldVN(id: Long) -------------------------------------------------------------------------------- /people.json: -------------------------------------------------------------------------------- 1 | {"name": "foo1", "age": 20, "married":false} 2 | {"name": "foo2", "age": 20, "married":false} 3 | {"name": "foo3", "age": 20, "married":false} 4 | {"name": "foo4", "age": 20, "married":false} 5 | {"name": "foo5", "age": 20, "married":false} 6 | {"name": "foo6", "age": 20, "married":false} 7 | {"name": "foo7", "age": 20, "married":false} 8 | {"name": "foo8", "age": 20, "married":false} 9 | {"name": "foo9", "age": 20, "married":false} 10 | {"name": "foo10", "age": 20, "married":false} -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.15 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // see: https://github.com/playframework/playframework/blob/master/framework/src/sbt-plugin/src/main/scala/play/sbt/PlayImport.scala 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.14") 3 | 4 | // to format scala source code 5 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") 6 | 7 | // enable updating file headers eg. for copyright 8 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.8.0") 9 | 10 | // https://github.com/typesafehub/sbt-conductr 11 | addSbtPlugin("com.lightbend.conductr" % "sbt-conductr" % "2.3.3") 12 | 13 | // see: https://developer.lightbend.com/docs/monitoring/latest/home.html 14 | addSbtPlugin("com.lightbend.cinnamon" % "sbt-cinnamon" % "2.3.1") 15 | 16 | // to create your '.credentials' file: https://developer.lightbend.com/docs/reactive-platform/2.0/setup/setup-sbt.html 17 | // for credentials: https://www.lightbend.com/product/lightbend-reactive-platform/credentials 18 | // create a developer account: https://www.lightbend.com/account 19 | credentials += Credentials(Path.userHome / ".lightbend" / "commercial.credentials") 20 | 21 | resolvers += Resolver.url("lightbend-commercial", url("https://repo.lightbend.com/commercial-releases"))(Resolver.ivyStylePatterns) 22 | -------------------------------------------------------------------------------- /sandbox-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo sh -c "ifconfig lo0 alias 192.168.10.1 255.255.255.0 && \ 3 | ifconfig lo0 alias 192.168.10.2 255.255.255.0 && \ 4 | ifconfig lo0 alias 192.168.10.3 255.255.255.0" 5 | sandbox run 2.0.2 --feature visualization --feature monitoring --nr-of-containers 2 -------------------------------------------------------------------------------- /sandbox-stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sandbox stop -------------------------------------------------------------------------------- /stream-persons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | http -v POST :8080/persons/stream < people.json -------------------------------------------------------------------------------- /test/com/github/dnvriend/TestSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend 18 | 19 | import akka.actor.{ ActorRef, ActorSystem, PoisonPill } 20 | import akka.stream.Materializer 21 | import akka.stream.scaladsl.Source 22 | import akka.stream.testkit.TestSubscriber 23 | import akka.stream.testkit.scaladsl.TestSink 24 | import akka.testkit.TestProbe 25 | import akka.util.Timeout 26 | import org.scalatest._ 27 | import org.scalatest.concurrent.{ Eventually, ScalaFutures } 28 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite 29 | import play.api.inject.BindingKey 30 | import play.api.test.WsTestClient 31 | 32 | import scala.concurrent.duration._ 33 | import scala.concurrent.{ ExecutionContext, Future } 34 | import scala.reflect.ClassTag 35 | import scala.util.Try 36 | 37 | class TestSpec extends FlatSpec 38 | with Matchers 39 | with GivenWhenThen 40 | with OptionValues 41 | with TryValues 42 | with ScalaFutures 43 | with WsTestClient 44 | with BeforeAndAfterAll 45 | with BeforeAndAfterEach 46 | with Eventually 47 | with GuiceOneServerPerSuite { 48 | 49 | def getComponent[A: ClassTag] = app.injector.instanceOf[A] 50 | 51 | def getAnnotatedComponent[A](name: String)(implicit ct: ClassTag[A]): A = 52 | app.injector.instanceOf[A](BindingKey(ct.runtimeClass.asInstanceOf[Class[A]]).qualifiedWith(name)) 53 | 54 | // set the port number of the HTTP server 55 | override lazy val port: Int = 8080 56 | implicit val timeout: Timeout = 10.seconds 57 | implicit val pc: PatienceConfig = PatienceConfig(timeout = 30.seconds, interval = 300.millis) 58 | implicit val system: ActorSystem = getComponent[ActorSystem] 59 | implicit val ec: ExecutionContext = getComponent[ExecutionContext] 60 | implicit val mat: Materializer = getComponent[Materializer] 61 | 62 | // ================================== Supporting Operations ==================================== 63 | implicit class PimpedByteArray(self: Array[Byte]) { 64 | def getString: String = new String(self) 65 | } 66 | 67 | implicit class PimpedFuture[T](self: Future[T]) { 68 | def toTry: Try[T] = Try(self.futureValue) 69 | } 70 | 71 | implicit class SourceOps[A](src: Source[A, _]) { 72 | def testProbe(f: TestSubscriber.Probe[A] => Unit): Unit = 73 | f(src.runWith(TestSink.probe(system))) 74 | } 75 | 76 | def killActors(actors: ActorRef*): Unit = { 77 | val tp = TestProbe() 78 | actors.foreach { (actor: ActorRef) => 79 | tp watch actor 80 | actor ! PoisonPill 81 | tp.expectTerminated(actor) 82 | } 83 | } 84 | 85 | override protected def beforeEach(): Unit = { 86 | } 87 | } -------------------------------------------------------------------------------- /test/com/github/dnvriend/marshaller/XmlMarshallerTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Dennis Vriend 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 com.github.dnvriend.marshaller 18 | 19 | import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._ 20 | import akka.http.scaladsl.marshalling.{ Marshal, Marshaller } 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.unmarshalling._ 23 | import com.github.dnvriend.TestSpec 24 | 25 | import scala.concurrent.Future 26 | import scala.xml._ 27 | 28 | // see: http://doc.akka.io/docs/akka-http/current/scala/http/client-side/host-level.html#host-level-api 29 | // see: http://doc.akka.io/docs/akka-http/current/scala/http/implications-of-streaming-http-entity.html#implications-of-streaming-http-entities 30 | // see: http://doc.akka.io/docs/akka/2.4.9/scala/http/routing-dsl/directives/marshalling-directives/entity.html#entity 31 | // see: http://doc.akka.io/docs/akka-http/current/scala/http/common/http-model.html#httpresponse 32 | // see: http://doc.akka.io/docs/akka-http/current/scala/http/common/marshalling.html#http-marshalling-scala 33 | // see: http://doc.akka.io/docs/akka-http/current/scala/http/common/xml-support.html 34 | trait NodeSeqUnmarshaller { 35 | implicit def responseToAUnmarshaller[A](implicit 36 | resp: FromResponseUnmarshaller[NodeSeq], 37 | toA: Unmarshaller[NodeSeq, A]): Unmarshaller[HttpResponse, A] = { 38 | resp.flatMap(toA).asScala 39 | } 40 | } 41 | case class Person(name: String, age: Int) 42 | object Person extends NodeSeqUnmarshaller { 43 | implicit val unmarshaller: Unmarshaller[NodeSeq, Person] = Unmarshaller.strict[NodeSeq, Person] { xml => 44 | val name: String = (xml \ "name").text 45 | val age: Int = (xml \ "age").text.toInt 46 | Person(name, age) 47 | } 48 | implicit val marshaller: Marshaller[Person, NodeSeq] = Marshaller.opaque[Person, NodeSeq] { person => 49 | 50 | { person.name } 51 | { person.age } 52 | 53 | } 54 | } 55 | 56 | class XmlMarshallerTest extends TestSpec { 57 | it should "unmarshal to a person" in { 58 | val resp = HttpResponse( 59 | entity = HttpEntity(contentType = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), "dennis42") 60 | ) 61 | Unmarshal[HttpResponse](resp).to[Person].futureValue shouldBe Person("dennis", 42) 62 | } 63 | 64 | it should "marshal to NodeSeq" in { 65 | Marshal[Person](Person("dennis", 42)).to[NodeSeq].futureValue shouldBe a[NodeSeq] 66 | } 67 | 68 | it should "marshal to a RequestEntity" in { 69 | val result: Future[RequestEntity] = for { 70 | xml <- Marshal[Person](Person("dennis", 42)).to[NodeSeq] 71 | ent <- Marshal[NodeSeq](xml).to[RequestEntity] 72 | } yield ent 73 | 74 | result.futureValue shouldBe "" 75 | } 76 | 77 | it should "" in { 78 | import WsClientUnmarshallers._ 79 | withClient { client => 80 | client.url("").get().map { 81 | resp => resp.xml.as[Person] 82 | } 83 | } 84 | } 85 | } 86 | 87 | object WsClientUnmarshallers { 88 | implicit class ElemOps(val xml: NodeSeq) extends AnyVal { 89 | def as[A](implicit unmarshaller: XmlUnmarshaller[A]): A = { 90 | unmarshaller.fromXml(xml) 91 | } 92 | } 93 | } 94 | 95 | trait XmlUnmarshaller[A] { 96 | def fromXml(xml: NodeSeq): A 97 | } --------------------------------------------------------------------------------