├── .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 | [](https://gitter.im/dnvriend/akka-http-test?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](https://travis-ci.org/dnvriend/akka-http-test)
5 | [](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 | }
--------------------------------------------------------------------------------