├── .DS_Store ├── .bsp └── sbt.json ├── .gitignore ├── .idea ├── misc.xml ├── modules.xml ├── modules │ ├── akka-http-build.iml │ └── akka-http.iml ├── sbt.xml ├── scala_compiler.xml └── vcs.xml ├── README.md ├── build.sbt ├── project └── build.properties └── src └── main ├── .DS_Store ├── html └── websockets.html ├── json ├── guitar.json ├── invalidPaymentRequest.json ├── login.json ├── paymentRequest.json ├── person.json └── player.json ├── resources ├── .DS_Store ├── application.conf └── keystore.pkcs12 └── scala ├── part1_recap ├── AkkaRecap.scala ├── AkkaStreamsRecap.scala └── ScalaRecap.scala ├── part2_lowlevelserver ├── LowLevelAPI.scala ├── LowLevelHttps.scala └── LowLevelRest.scala ├── part3_highlevelserver ├── DirectivesBreakdown.scala ├── HandlingExceptions.scala ├── HandlingRejections.scala ├── HighLevelExample.scala ├── HighLevelExercise.scala ├── HighLevelIntro.scala ├── JwtAuthorization.scala ├── MarshallingJSON.scala ├── RouteDSLSpec.scala ├── UploadingFiles.scala └── WebsocketsDemo.scala ├── part4_client ├── ConnectionLevel.scala ├── HostLevel.scala ├── PaymentSystem.scala └── RequestLevel.scala └── playground └── Playground.scala /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockthejvm/akka-http/e1bfdf45fe5b98897d3481b617c5cb66c3a41ba0/.DS_Store -------------------------------------------------------------------------------- /.bsp/sbt.json: -------------------------------------------------------------------------------- 1 | {"name":"sbt","version":"1.9.9","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/daniel/.sdkman/candidates/java/21.0.1-tem/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/daniel/Library/Application Support/JetBrains/IdeaIC2023.3/plugins/Scala/launcher/sbt-launch.jar","-Dsbt.script=/Users/daniel/Library/Application%20Support/Coursier/bin/sbt","xsbt.boot.Boot","-bsp"]} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/sbt,java,scala,intellij 3 | # Edit at https://www.gitignore.io/?templates=sbt,java,scala,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | ### Intellij Patch ### 74 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 75 | 76 | # *.iml 77 | # modules.xml 78 | # .idea/misc.xml 79 | # *.ipr 80 | 81 | # Sonarlint plugin 82 | .idea/sonarlint 83 | 84 | ### Java ### 85 | # Compiled class file 86 | *.class 87 | 88 | # Log file 89 | *.log 90 | 91 | # BlueJ files 92 | *.ctxt 93 | 94 | # Mobile Tools for Java (J2ME) 95 | .mtj.tmp/ 96 | 97 | # Package Files # 98 | *.jar 99 | *.war 100 | *.nar 101 | *.ear 102 | *.zip 103 | *.tar.gz 104 | *.rar 105 | 106 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 107 | hs_err_pid* 108 | 109 | ### SBT ### 110 | # Simple Build Tool 111 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 112 | 113 | dist/* 114 | target/ 115 | lib_managed/ 116 | src_managed/ 117 | project/boot/ 118 | project/plugins/project/ 119 | .history 120 | .cache 121 | .lib/ 122 | 123 | ### Scala ### 124 | 125 | # End of https://www.gitignore.io/api/sbt,java,scala,intellij 126 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules/akka-http-build.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 116 | -------------------------------------------------------------------------------- /.idea/modules/akka-http.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.idea/sbt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/scala_compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The official repository for the Rock the JVM Akka HTTP with Scala course 2 | 3 | **(for the Udemy edition, click [here](https://github.com/rockthejvm/udemy-akka-http))** 4 | 5 | This repository contains the code we wrote during [Rock the JVM's Akka HTTP with Scala](https://rockthejvm.com/course/akka-http) course. Unless explicitly mentioned, the code in this repository is exactly what was caught on camera. 6 | 7 | ### How to install 8 | - either clone the repo or download as zip 9 | - open with IntelliJ as an SBT project 10 | 11 | No need to do anything else, as the IDE will take care to download and apply the appropriate library dependencies. 12 | 13 | ### How to start 14 | 15 | Clone this repository and checkout the `start` tag by running the following in the repo folder: 16 | 17 | ``` 18 | git checkout start 19 | ``` 20 | 21 | ### How to run an intermediate state 22 | 23 | The repository was built while recording the lectures. Prior to each lecture, I tagged each commit so you can easily go back to an earlier state of the repo! 24 | 25 | The tags are as follows: 26 | 27 | * `1.1-scala-recap` 28 | * `1.2-akka-recap` 29 | * `1.3-akka-streams-recap` 30 | * `2.1-low-level-api` 31 | * `2.2-low-level-api-exercise` 32 | * `2.3-marshalling-json` 33 | * `2.4-low-level-server-with-json` 34 | * `2.5-low-level-server-query-params` 35 | * `2.6-low-level-https` 36 | * `3.1-high-level-intro` 37 | * `3.2-directives-breakdown` 38 | * `3.3-directives-breakdown-part-2` 39 | * `3.4-high-level-example` 40 | * `3.5-high-level-exercise` 41 | * `3.6-marshalling-json` 42 | * `3.7-marshalling-json-part-2` 43 | * `3.8-dealing-with-rejections` 44 | * `3.9-handling-exceptions` 45 | * `3.10-routing-testkit` 46 | * `3.11-websockets` 47 | * `3.12-uploading-files` 48 | * `3.13-jwt` 49 | * `4.1-client-connection-level-api` 50 | * `4.2-client-host-level-api` 51 | * `4.3-client-request-level-api` 52 | 53 | When you watch a lecture, you can `git checkout` the appropriate tag and the repo will go back to the exact code I had when I started the lecture. 54 | 55 | ### For questions or suggestions 56 | 57 | If you have changes to suggest to this repo, either 58 | - submit a GitHub issue 59 | - tell me in the course Q/A forum 60 | - submit a pull request! 61 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "akka-http" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.13.14" 6 | 7 | val akkaVersion = "2.6.20" 8 | val akkaHttpVersion = "10.2.10" 9 | val scalaTestVersion = "3.2.7" 10 | 11 | libraryDependencies ++= Seq( 12 | // akka streams 13 | "com.typesafe.akka" %% "akka-stream" % akkaVersion, 14 | // akka http 15 | "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, 16 | "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, 17 | "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion, 18 | // testing 19 | "com.typesafe.akka" %% "akka-testkit" % akkaVersion, 20 | "org.scalatest" %% "scalatest" % scalaTestVersion, 21 | 22 | // JWT 23 | "com.pauldijou" %% "jwt-spray-json" % "5.0.0" 24 | ) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.9.9 -------------------------------------------------------------------------------- /src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockthejvm/akka-http/e1bfdf45fe5b98897d3481b617c5cb66c3a41ba0/src/main/.DS_Store -------------------------------------------------------------------------------- /src/main/html/websockets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 | Starting websocket... 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/json/guitar.json: -------------------------------------------------------------------------------- 1 | { 2 | "make": "Taylor", 3 | "model": "914", 4 | "quantity": 5 5 | } 6 | -------------------------------------------------------------------------------- /src/main/json/invalidPaymentRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "creditCard": { 3 | "serialNumber": "1234-1234-1234-1234", 4 | "securityCode": "123", 5 | "account": "rx-65-sd-23" 6 | }, 7 | "receiverAccount": "td-23-gb-67", 8 | "amount": 99 9 | } -------------------------------------------------------------------------------- /src/main/json/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "daniel", 3 | "password": "Rockthejvm1!" 4 | } -------------------------------------------------------------------------------- /src/main/json/paymentRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "creditCard": { 3 | "serialNumber": "4362-2522-2156-6893", 4 | "securityCode": "123", 5 | "account": "rx-65-sd-23" 6 | }, 7 | "receiverAccount": "td-23-gb-67", 8 | "amount": 99 9 | } -------------------------------------------------------------------------------- /src/main/json/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "pin": 4, 3 | "name": "Daniel" 4 | } -------------------------------------------------------------------------------- /src/main/json/player.json: -------------------------------------------------------------------------------- 1 | { 2 | "nickname": "bonjovi123", 3 | "characterClass": "Rockstar", 4 | "level": 56 5 | } -------------------------------------------------------------------------------- /src/main/resources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockthejvm/akka-http/e1bfdf45fe5b98897d3481b617c5cb66c3a41ba0/src/main/resources/.DS_Store -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockthejvm/akka-http/e1bfdf45fe5b98897d3481b617c5cb66c3a41ba0/src/main/resources/application.conf -------------------------------------------------------------------------------- /src/main/resources/keystore.pkcs12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockthejvm/akka-http/e1bfdf45fe5b98897d3481b617c5cb66c3a41ba0/src/main/resources/keystore.pkcs12 -------------------------------------------------------------------------------- /src/main/scala/part1_recap/AkkaRecap.scala: -------------------------------------------------------------------------------- 1 | package part1_recap 2 | 3 | import akka.actor.SupervisorStrategy.{Restart, Stop} 4 | import akka.actor.{Actor, ActorLogging, ActorSystem, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy} 5 | import akka.util.Timeout 6 | 7 | object AkkaRecap extends App { 8 | 9 | class SimpleActor extends Actor with ActorLogging with Stash { 10 | override def receive: Receive = { 11 | case "createChild" => 12 | val childActor = context.actorOf(Props[SimpleActor], "myChild") 13 | childActor ! "hello" 14 | case "stashThis" => 15 | stash() 16 | case "change handler NOW" => 17 | unstashAll() 18 | context.become(anotherHandler) 19 | 20 | case "change" => context.become(anotherHandler) 21 | case message => println(s"I received: $message") 22 | } 23 | 24 | def anotherHandler: Receive = { 25 | case message => println(s"In another receive handler: $message") 26 | } 27 | 28 | override def preStart(): Unit = { 29 | log.info("I'm starting") 30 | } 31 | 32 | override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { 33 | case _: RuntimeException => Restart 34 | case _ => Stop 35 | } 36 | } 37 | 38 | // actor encapsulation 39 | val system = ActorSystem("AkkaRecap") 40 | // #1: you can only instantiate an actor through the actor system 41 | val actor = system.actorOf(Props[SimpleActor], "simpleActor") 42 | // #2: sending messages 43 | actor ! "hello" 44 | /* 45 | - messages are sent asynchronously 46 | - many actors (in the millions) can share a few dozen threads 47 | - each message is processed/handled ATOMICALLY 48 | - no need for locks 49 | */ 50 | 51 | // changing actor behavior + stashing 52 | // actors can spawn other actors 53 | // guardians: /system, /user, / = root guardian 54 | 55 | // actors have a defined lifecycle: they can be started, stopped, suspended, resumed, restarted 56 | 57 | // stopping actors - context.stop 58 | actor ! PoisonPill 59 | 60 | // logging 61 | // supervision 62 | 63 | // configure Akka infrastructure: dispatchers, routers, mailboxes 64 | 65 | // schedulers 66 | import system.dispatcher 67 | 68 | import scala.concurrent.duration._ 69 | system.scheduler.scheduleOnce(2.seconds) { 70 | actor ! "delayed happy birthday!" 71 | } 72 | 73 | // Akka patterns including FSM + ask pattern 74 | import akka.pattern.ask 75 | implicit val timeout: Timeout = Timeout(3.seconds) 76 | 77 | val future = actor ? "question" 78 | 79 | // the pipe pattern 80 | import akka.pattern.pipe 81 | val anotherActor = system.actorOf(Props[SimpleActor], "anotherSimpleActor") 82 | future.mapTo[String].pipeTo(anotherActor) 83 | } 84 | -------------------------------------------------------------------------------- /src/main/scala/part1_recap/AkkaStreamsRecap.scala: -------------------------------------------------------------------------------- 1 | package part1_recap 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.{ActorMaterializer, OverflowStrategy} 5 | import akka.stream.scaladsl.{Flow, Keep, Sink, Source} 6 | 7 | import scala.util.{Failure, Success} 8 | 9 | object AkkaStreamsRecap extends App { 10 | 11 | implicit val system: ActorSystem = ActorSystem("AkkaStreamsRecap") 12 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 13 | import system.dispatcher 14 | 15 | val source = Source(1 to 100) 16 | val sink = Sink.foreach[Int](println) 17 | val flow = Flow[Int].map(x => x + 1) 18 | 19 | val runnableGraph = source.via(flow).to(sink) 20 | val simpleMaterializedValue = runnableGraph 21 | // .run() // materialization 22 | 23 | // MATERIALIZED VALUE 24 | val sumSink = Sink.fold[Int, Int](0)((currentSum, element) => currentSum + element) 25 | // val sumFuture = source.runWith(sumSink) 26 | // 27 | // sumFuture.onComplete { 28 | // case Success(sum) => println(s"The sum of all the numbers from the simple source is: $sum") 29 | // case Failure(ex) => println(s"Summing all the numbers from the simple source FAILED: $ex") 30 | // } 31 | 32 | val anotherMaterializedValue = source.viaMat(flow)(Keep.right).toMat(sink)(Keep.left) 33 | // .run() 34 | /* 35 | 1 - materializing a graph means materializing ALL the components 36 | 2 - a materialized value can be ANYTHING AT ALL 37 | */ 38 | 39 | /* 40 | Backpressure actions 41 | 42 | - buffer elements 43 | - apply a strategy in case the buffer overflows 44 | - fail the entire stream 45 | */ 46 | 47 | val bufferedFlow = Flow[Int].buffer(10, OverflowStrategy.dropHead) 48 | 49 | source.async 50 | .via(bufferedFlow).async 51 | .runForeach { e => 52 | // a slow consumer 53 | Thread.sleep(100) 54 | println(e) 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/part1_recap/ScalaRecap.scala: -------------------------------------------------------------------------------- 1 | package part1_recap 2 | 3 | import scala.concurrent.Future 4 | import scala.util.{Failure, Success} 5 | 6 | object ScalaRecap extends App { 7 | 8 | val aCondition: Boolean = false 9 | def myFunction(x: Int) = { 10 | // code 11 | if (x > 4) 42 else 65 12 | } 13 | // instructions vs expressions 14 | // types + type inference 15 | 16 | // OO features of Scala 17 | class Animal 18 | trait Carnivore { 19 | def eat(a: Animal): Unit 20 | } 21 | 22 | object Carnivore 23 | 24 | // generics 25 | abstract class MyList[+A] 26 | 27 | // method notations 28 | 1 + 2 // infix notation 29 | 1.+(2) 30 | 31 | // FP 32 | val anIncrementer: Int => Int = (x: Int) => x + 1 33 | anIncrementer(1) 34 | 35 | List(1,2,3).map(anIncrementer) 36 | // HOF: flatMap, filter 37 | // for-comprehensions 38 | 39 | // Monads: Option, Try 40 | 41 | // Pattern matching! 42 | val unknown: Any = 2 43 | val order = unknown match { 44 | case 1 => "first" 45 | case 2 => "second" 46 | case _ => "unknown" 47 | } 48 | 49 | try { 50 | // code that can throw an exception 51 | throw new RuntimeException 52 | } catch { 53 | case e: Exception => println("I caught one!") 54 | } 55 | 56 | /** 57 | * Scala advanced 58 | */ 59 | 60 | // multithreading 61 | 62 | import scala.concurrent.ExecutionContext.Implicits.global 63 | val future = Future { 64 | // long computation here 65 | // executed on SOME other thread 66 | 42 67 | } 68 | // map, flatMap, filter + other niceties e.g. recover/recoverWith 69 | 70 | future.onComplete { 71 | case Success(value) => println(s"I found the meaning of life: $value") 72 | case Failure(exception) => println(s"I found $exception while searching for the meaning of life!") 73 | } // on SOME thread 74 | 75 | val partialFunction: PartialFunction[Int, Int] = { 76 | case 1 => 42 77 | case 2 => 65 78 | case _ => 999 79 | } 80 | // based on pattern matching! 81 | 82 | // type aliases 83 | type AkkaReceive = PartialFunction[Any, Unit] 84 | def receive: AkkaReceive = { 85 | case 1 => println("hello!") 86 | case _ => println("confused...") 87 | } 88 | 89 | // Implicits! 90 | implicit val timeout: Int = 3000 91 | def setTimeout(f: () => Unit)(implicit timeout: Int) = f() 92 | 93 | setTimeout(() => println("timeout"))// other arg list injected by the compiler 94 | 95 | // conversions 96 | // 1) implicit methods 97 | case class Person(name: String) { 98 | def greet: String = s"Hi, my name is $name" 99 | } 100 | 101 | implicit def fromStringToPerson(name: String): Person = Person(name) 102 | "Peter".greet 103 | // fromStringToPerson("Peter").greet 104 | 105 | // 2) implicit classes 106 | implicit class Dog(name: String) { 107 | def bark = println("Bark!") 108 | } 109 | "Lassie".bark 110 | // new Dog("Lassie").bark 111 | 112 | // implicit organizations 113 | // local scope 114 | implicit val numberOrdering: Ordering[Int] = Ordering.fromLessThan(_ > _) 115 | List(1,2,3).sorted //(numberOrdering) => List(3,2,1) 116 | 117 | // imported scope 118 | 119 | // companion objects of the types involved in the call 120 | object Person { 121 | implicit val personOrdering: Ordering[Person] = Ordering.fromLessThan((a, b) => a.name.compareTo(b.name) < 0) 122 | } 123 | 124 | List(Person("Bob"), Person("Alice")).sorted // (Person.personOrdering) 125 | // => List(Person("Alice"), Person("Bob")) 126 | 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/scala/part2_lowlevelserver/LowLevelAPI.scala: -------------------------------------------------------------------------------- 1 | package part2_lowlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.Http.IncomingConnection 6 | import akka.http.scaladsl.model._ 7 | import akka.http.scaladsl.model.headers.Location 8 | import akka.stream.ActorMaterializer 9 | import akka.stream.scaladsl.{Flow, Sink} 10 | 11 | import scala.concurrent.Future 12 | import scala.concurrent.duration._ 13 | import scala.util.{Failure, Success} 14 | 15 | object LowLevelAPI extends App { 16 | 17 | implicit val system: ActorSystem = ActorSystem("LowLevelServerAPI") 18 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 19 | import system.dispatcher 20 | 21 | val serverSource = Http().bind("localhost", 8000) 22 | val connectionSink = Sink.foreach[IncomingConnection] { connection => 23 | println(s"Accepted incoming connection from: ${connection.remoteAddress}") 24 | } 25 | 26 | val serverBindingFuture = serverSource.to(connectionSink).run() 27 | serverBindingFuture.onComplete { 28 | case Success(binding) => 29 | println("Server binding successful.") 30 | binding.terminate(2.seconds) 31 | case Failure(ex) => println(s"Server binding failed: $ex") 32 | } 33 | 34 | /* 35 | Method 1: synchronously serve HTTP responses 36 | */ 37 | val requestHandler: HttpRequest => HttpResponse = { 38 | case HttpRequest(HttpMethods.GET, _, _, _, _) => 39 | HttpResponse( 40 | StatusCodes.OK, // HTTP 200 41 | entity = HttpEntity( 42 | ContentTypes.`text/html(UTF-8)`, 43 | """ 44 | | 45 | | 46 | | Hello from Akka HTTP! 47 | | 48 | | 49 | """.stripMargin 50 | ) 51 | ) 52 | 53 | case request: HttpRequest => 54 | request.discardEntityBytes() 55 | HttpResponse( 56 | StatusCodes.NotFound, // 404 57 | entity = HttpEntity( 58 | ContentTypes.`text/html(UTF-8)`, 59 | """ 60 | | 61 | | 62 | | OOPS! The resource can't be found. 63 | | 64 | | 65 | """.stripMargin 66 | ) 67 | ) 68 | } 69 | 70 | val httpSyncConnectionHandler = Sink.foreach[IncomingConnection] { connection => 71 | connection.handleWithSyncHandler(requestHandler) 72 | } 73 | 74 | // Http().bind("localhost", 8080).runWith(httpSyncConnectionHandler) 75 | 76 | // shorthand version: 77 | // Http().bindAndHandleSync(requestHandler, "localhost", 8080) 78 | 79 | 80 | /* 81 | Method 2: serve back HTTP response ASYNCHRONOUSLY 82 | */ 83 | val asyncRequestHandler: HttpRequest => Future[HttpResponse] = { 84 | case HttpRequest(HttpMethods.GET, Uri.Path("/home"), _, _, _) => // method, URI, HTTP headers, content and the protocol (HTTP1.1/HTTP2.0) 85 | Future(HttpResponse( 86 | StatusCodes.OK, // HTTP 200 87 | entity = HttpEntity( 88 | ContentTypes.`text/html(UTF-8)`, 89 | """ 90 | | 91 | | 92 | | Hello from Akka HTTP! 93 | | 94 | | 95 | """.stripMargin 96 | ) 97 | )) 98 | 99 | case request: HttpRequest => 100 | request.discardEntityBytes() 101 | Future(HttpResponse( 102 | StatusCodes.NotFound, // 404 103 | entity = HttpEntity( 104 | ContentTypes.`text/html(UTF-8)`, 105 | """ 106 | | 107 | | 108 | | OOPS! The resource can't be found. 109 | | 110 | | 111 | """.stripMargin 112 | ) 113 | )) 114 | } 115 | 116 | val httpAsyncConnectionHandler = Sink.foreach[IncomingConnection] { connection => 117 | connection.handleWithAsyncHandler(asyncRequestHandler) 118 | } 119 | 120 | // streams-based "manual" version 121 | // Http().bind("localhost", 8081).runWith(httpAsyncConnectionHandler) 122 | 123 | // shorthand version 124 | Http().bindAndHandleAsync(asyncRequestHandler, "localhost", 8081) 125 | 126 | /* 127 | Method 3: async via Akka streams 128 | */ 129 | val streamsBasedRequestHandler: Flow[HttpRequest, HttpResponse, _] = Flow[HttpRequest].map { 130 | case HttpRequest(HttpMethods.GET, Uri.Path("/home"), _, _, _) => // method, URI, HTTP headers, content and the protocol (HTTP1.1/HTTP2.0) 131 | HttpResponse( 132 | StatusCodes.OK, // HTTP 200 133 | entity = HttpEntity( 134 | ContentTypes.`text/html(UTF-8)`, 135 | """ 136 | | 137 | | 138 | | Hello from Akka HTTP! 139 | | 140 | | 141 | """.stripMargin 142 | ) 143 | ) 144 | 145 | case request: HttpRequest => 146 | request.discardEntityBytes() 147 | HttpResponse( 148 | StatusCodes.NotFound, // 404 149 | entity = HttpEntity( 150 | ContentTypes.`text/html(UTF-8)`, 151 | """ 152 | | 153 | | 154 | | OOPS! The resource can't be found. 155 | | 156 | | 157 | """.stripMargin 158 | ) 159 | ) 160 | } 161 | 162 | // "manual" version 163 | // Http().bind("localhost", 8082).runForeach { connection => 164 | // connection.handleWith(streamsBasedRequestHandler) 165 | // } 166 | 167 | // shorthand version 168 | Http().bindAndHandle(streamsBasedRequestHandler, "localhost", 8082) 169 | 170 | /** 171 | * Exercise: create your own HTTP server running on localhost on 8388, which replies 172 | * - with a welcome message on the "front door" localhost:8388 173 | * - with a proper HTML on localhost:8388/about 174 | * - with a 404 message otherwise 175 | */ 176 | 177 | val syncExerciseHandler: HttpRequest => HttpResponse = { 178 | case HttpRequest(HttpMethods.GET, Uri.Path("/"), _, _, _) => 179 | HttpResponse( 180 | // status code OK (200) is default 181 | entity = HttpEntity( 182 | ContentTypes.`text/html(UTF-8)`, 183 | "Hello from the exercise front door!" 184 | ) 185 | ) 186 | 187 | case HttpRequest(HttpMethods.GET, Uri.Path("/about"), _, _, _) => 188 | HttpResponse( 189 | // status code OK (200) is default 190 | entity = HttpEntity( 191 | ContentTypes.`text/html(UTF-8)`, 192 | """ 193 | | 194 | | 195 | |
196 | | Hello from the about page! 197 | |
198 | | 199 | | 200 | """.stripMargin 201 | ) 202 | ) 203 | 204 | // path /search redirects to some other part of our website/webapp/microservice 205 | case HttpRequest(HttpMethods.GET, Uri.Path("/search"), _, _, _) => 206 | HttpResponse( 207 | StatusCodes.Found, 208 | headers = List(Location("http://google.com")) 209 | ) 210 | 211 | case request: HttpRequest => 212 | request.discardEntityBytes() 213 | HttpResponse( 214 | StatusCodes.NotFound, 215 | entity = HttpEntity( 216 | ContentTypes.`text/html(UTF-8)`, 217 | "OOPS, you're in no man's land, sorry." 218 | ) 219 | ) 220 | } 221 | 222 | val bindingFuture = Http().bindAndHandleSync(syncExerciseHandler, "localhost", 8388) 223 | 224 | // shutdown the server: 225 | bindingFuture 226 | .flatMap(binding => binding.unbind()) 227 | .onComplete(_ => system.terminate()) 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/main/scala/part2_lowlevelserver/LowLevelHttps.scala: -------------------------------------------------------------------------------- 1 | package part2_lowlevelserver 2 | 3 | import java.io.InputStream 4 | import java.security.{KeyStore, SecureRandom} 5 | 6 | import akka.actor.ActorSystem 7 | import akka.http.scaladsl.model._ 8 | import akka.http.scaladsl.{ConnectionContext, Http, HttpsConnectionContext} 9 | import akka.stream.ActorMaterializer 10 | import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} 11 | 12 | object HttpsContext { 13 | // Step 1: key store 14 | val ks: KeyStore = KeyStore.getInstance("PKCS12") 15 | val keystoreFile: InputStream = getClass.getClassLoader.getResourceAsStream("keystore.pkcs12") 16 | // alternative: new FileInputStream(new File("src/main/resources/keystore.pkcs12")) 17 | val password = "akka-https".toCharArray // fetch the password from a secure place! 18 | ks.load(keystoreFile, password) 19 | 20 | // Step 2: initialize a key manager 21 | val keyManagerFactory = KeyManagerFactory.getInstance("SunX509") // PKI = public key infrastructure 22 | keyManagerFactory.init(ks, password) 23 | 24 | // Step 3: initialize a trust manager 25 | val trustManagerFactory = TrustManagerFactory.getInstance("SunX509") 26 | trustManagerFactory.init(ks) 27 | 28 | // Step 4: initialize an SSL context 29 | val sslContext: SSLContext = SSLContext.getInstance("TLS") 30 | sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom) 31 | 32 | // Step 5: return the https connection context 33 | val httpsConnectionContext: HttpsConnectionContext = ConnectionContext.https(sslContext) 34 | } 35 | 36 | object LowLevelHttps extends App { 37 | 38 | implicit val system: ActorSystem = ActorSystem("LowLevelHttps") 39 | // implicit val materializer: ActorMaterializer = ActorMaterializer() // needed only for Akka Streams < 2.6 40 | 41 | val requestHandler: HttpRequest => HttpResponse = { 42 | case HttpRequest(HttpMethods.GET, _, _, _, _) => 43 | HttpResponse( 44 | StatusCodes.OK, // HTTP 200 45 | entity = HttpEntity( 46 | ContentTypes.`text/html(UTF-8)`, 47 | """ 48 | | 49 | | 50 | | Hello from Akka HTTP! 51 | | 52 | | 53 | """.stripMargin 54 | ) 55 | ) 56 | 57 | case request: HttpRequest => 58 | request.discardEntityBytes() 59 | HttpResponse( 60 | StatusCodes.NotFound, // 404 61 | entity = HttpEntity( 62 | ContentTypes.`text/html(UTF-8)`, 63 | """ 64 | | 65 | | 66 | | OOPS! The resource can't be found. 67 | | 68 | | 69 | """.stripMargin 70 | ) 71 | ) 72 | } 73 | 74 | val httpsBinding = Http().bindAndHandleSync(requestHandler, "localhost", 8443, HttpsContext.httpsConnectionContext) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/part2_lowlevelserver/LowLevelRest.scala: -------------------------------------------------------------------------------- 1 | package part2_lowlevelserver 2 | 3 | import akka.pattern.ask 4 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props} 5 | import akka.http.scaladsl.Http 6 | import akka.http.scaladsl.model.Uri.Query 7 | import akka.http.scaladsl.model._ 8 | import akka.stream.ActorMaterializer 9 | import akka.util.Timeout 10 | 11 | 12 | import scala.concurrent.Future 13 | import scala.concurrent.duration._ 14 | 15 | // step 1 16 | import spray.json._ 17 | 18 | case class Guitar(make: String, model: String, quantity: Int = 0) 19 | 20 | object GuitarDB { 21 | case class CreateGuitar(guitar: Guitar) 22 | case class GuitarCreated(id: Int) 23 | case class FindGuitar(id: Int) 24 | case object FindAllGuitars 25 | case class AddQuantity(id: Int, quantity: Int) 26 | case class FindGuitarsInStock(inStock: Boolean) 27 | } 28 | 29 | class GuitarDB extends Actor with ActorLogging { 30 | import GuitarDB._ 31 | 32 | var guitars: Map[Int, Guitar] = Map() 33 | var currentGuitarId: Int = 0 34 | 35 | override def receive: Receive = { 36 | case FindAllGuitars => 37 | log.info("Searching for all guitars") 38 | sender() ! guitars.values.toList 39 | 40 | case FindGuitar(id) => 41 | log.info(s"Searching guitar by id: $id") 42 | sender() ! guitars.get(id) 43 | 44 | case CreateGuitar(guitar) => 45 | log.info(s"Adding guitar $guitar with id $currentGuitarId") 46 | guitars = guitars + (currentGuitarId -> guitar) 47 | sender() ! GuitarCreated(currentGuitarId) 48 | currentGuitarId += 1 49 | 50 | case AddQuantity(id, quantity) => 51 | log.info(s"Trying to add $quantity items for guitar $id") 52 | val guitar: Option[Guitar] = guitars.get(id) 53 | val newGuitar: Option[Guitar] = guitar.map { 54 | case Guitar(make, model, q) => Guitar(make, model, q + quantity) 55 | } 56 | 57 | newGuitar.foreach(guitar => guitars = guitars + (id -> guitar)) 58 | sender() ! newGuitar 59 | 60 | case FindGuitarsInStock(inStock) => 61 | log.info(s"Searching for all guitars ${if(inStock) "in" else "out of"} stock") 62 | if (inStock) 63 | sender() ! guitars.values.filter(_.quantity > 0) 64 | else 65 | sender() ! guitars.values.filter(_.quantity == 0) 66 | 67 | } 68 | } 69 | 70 | // step 2 71 | trait GuitarStoreJsonProtocol extends DefaultJsonProtocol { 72 | // step 3 73 | implicit val guitarFormat = jsonFormat3(Guitar) 74 | } 75 | 76 | object LowLevelRest extends App with GuitarStoreJsonProtocol { 77 | 78 | implicit val system: ActorSystem = ActorSystem("LowLevelRest") 79 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 80 | import system.dispatcher 81 | import GuitarDB._ 82 | 83 | /* 84 | - GET on localhost:8080/api/guitar => ALL the guitars in the store 85 | - GET on localhost:8080/api/guitar?id=X => fetches the guitar associated with id X 86 | - POST on localhost:8080/api/guitar => insert the guitar into the store 87 | */ 88 | 89 | // JSON -> marshalling 90 | val simpleGuitar = Guitar("Fender", "Stratocaster") 91 | println(simpleGuitar.toJson.prettyPrint) 92 | 93 | // unmarshalling 94 | val simpleGuitarJsonString = 95 | """ 96 | |{ 97 | | "make": "Fender", 98 | | "model": "Stratocaster", 99 | | "quantity": 3 100 | |} 101 | """.stripMargin 102 | println(simpleGuitarJsonString.parseJson.convertTo[Guitar]) 103 | 104 | /* 105 | setup 106 | */ 107 | val guitarDb = system.actorOf(Props[GuitarDB], "LowLevelGuitarDB") 108 | val guitarList = List( 109 | Guitar("Fender", "Stratocaster"), 110 | Guitar("Gibson", "Les Paul"), 111 | Guitar("Martin", "LX1") 112 | ) 113 | 114 | guitarList.foreach { guitar => 115 | guitarDb ! CreateGuitar(guitar) 116 | } 117 | 118 | /* 119 | server code 120 | */ 121 | implicit val defaultTimeout: Timeout = Timeout(2.seconds) 122 | 123 | def getGuitar(query: Query): Future[HttpResponse] = { 124 | val guitarId = query.get("id").map(_.toInt) // Option[Int] 125 | 126 | guitarId match { 127 | case None => Future(HttpResponse(StatusCodes.NotFound)) // /api/guitar?id= 128 | case Some(id: Int) => 129 | val guitarFuture: Future[Option[Guitar]] = (guitarDb ? FindGuitar(id)).mapTo[Option[Guitar]] 130 | guitarFuture.map { 131 | case None => HttpResponse(StatusCodes.NotFound) // /api/guitar?id=9000 132 | case Some(guitar) => 133 | HttpResponse( 134 | entity = HttpEntity( 135 | ContentTypes.`application/json`, 136 | guitar.toJson.prettyPrint 137 | ) 138 | ) 139 | } 140 | } 141 | } 142 | 143 | val requestHandler: HttpRequest => Future[HttpResponse] = { 144 | case HttpRequest(HttpMethods.POST, uri@Uri.Path("/api/guitar/inventory"), _, _, _) => 145 | val query = uri.query() 146 | val guitarId: Option[Int] = query.get("id").map(_.toInt) 147 | val guitarQuantity: Option[Int] = query.get("quantity").map(_.toInt) 148 | 149 | val validGuitarResponseFuture: Option[Future[HttpResponse]] = for { 150 | id <- guitarId 151 | quantity <- guitarQuantity 152 | } yield { 153 | val newGuitarFuture: Future[Option[Guitar]] = (guitarDb ? AddQuantity(id, quantity)).mapTo[Option[Guitar]] 154 | newGuitarFuture.map(_ => HttpResponse(StatusCodes.OK)) 155 | } 156 | 157 | validGuitarResponseFuture.getOrElse(Future(HttpResponse(StatusCodes.BadRequest))) 158 | 159 | case HttpRequest(HttpMethods.GET, uri@Uri.Path("/api/guitar/inventory"), _, _, _) => 160 | val query = uri.query() 161 | val inStockOption = query.get("inStock").map(_.toBoolean) 162 | 163 | inStockOption match { 164 | case Some(inStock) => 165 | val guitarsFuture: Future[List[Guitar]] = (guitarDb ? FindGuitarsInStock(inStock)).mapTo[List[Guitar]] 166 | guitarsFuture.map { guitars => 167 | HttpResponse( 168 | entity = HttpEntity( 169 | ContentTypes.`application/json`, 170 | guitars.toJson.prettyPrint 171 | ) 172 | ) 173 | } 174 | case None => Future(HttpResponse(StatusCodes.BadRequest)) 175 | } 176 | 177 | case HttpRequest(HttpMethods.GET, uri@Uri.Path("/api/guitar"), _, _, _) => 178 | /* 179 | query parameter handling code 180 | */ 181 | val query = uri.query() // query object <=> Map[String, String] 182 | if (query.isEmpty) { 183 | val guitarsFuture: Future[List[Guitar]] = (guitarDb ? FindAllGuitars).mapTo[List[Guitar]] 184 | guitarsFuture.map { guitars => 185 | HttpResponse( 186 | entity = HttpEntity( 187 | ContentTypes.`application/json`, 188 | guitars.toJson.prettyPrint 189 | ) 190 | ) 191 | } 192 | } else { 193 | // fetch guitar associated to the guitar id 194 | // localhost:8080/api/guitar?id=45 195 | getGuitar(query) 196 | } 197 | 198 | case HttpRequest(HttpMethods.POST, Uri.Path("/api/guitar"), _, entity, _) => 199 | // entities are a Source[ByteString] 200 | val strictEntityFuture = entity.toStrict(3.seconds) 201 | strictEntityFuture.flatMap { strictEntity => 202 | 203 | val guitarJsonString = strictEntity.data.utf8String 204 | val guitar = guitarJsonString.parseJson.convertTo[Guitar] 205 | 206 | val guitarCreatedFuture: Future[GuitarCreated] = (guitarDb ? CreateGuitar(guitar)).mapTo[GuitarCreated] 207 | guitarCreatedFuture.map { _ => 208 | HttpResponse(StatusCodes.OK) 209 | } 210 | } 211 | 212 | case request: HttpRequest => 213 | request.discardEntityBytes() 214 | Future { 215 | HttpResponse(status = StatusCodes.NotFound) 216 | } 217 | } 218 | 219 | Http().bindAndHandleAsync(requestHandler, "localhost", 8080) 220 | 221 | /** 222 | * Exercise: enhance the Guitar case class with a quantity field, by default 0 223 | * - GET to /api/guitar/inventory?inStock=true/false which returns the guitars in stock as a JSON 224 | * - POST to /api/guitar/inventory?id=X&quantity=Y which adds Y guitars to the stock for guitar with id X 225 | * 226 | */ 227 | } 228 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/DirectivesBreakdown.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.event.LoggingAdapter 5 | import akka.http.scaladsl.Http 6 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpRequest, StatusCodes} 7 | import akka.stream.ActorMaterializer 8 | 9 | object DirectivesBreakdown extends App { 10 | 11 | implicit val system: ActorSystem = ActorSystem("DirectivesBreakdown") 12 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 13 | import system.dispatcher 14 | import akka.http.scaladsl.server.Directives._ 15 | 16 | /** 17 | * Type #1: filtering directives 18 | */ 19 | val simpleHttpMethodRoute = 20 | post { // equivalent directives for get, put, patch, delete, head, options 21 | complete(StatusCodes.Forbidden) 22 | } 23 | 24 | val simplePathRoute = 25 | path("about") { 26 | complete( 27 | HttpEntity( 28 | ContentTypes.`text/html(UTF-8)`, 29 | """ 30 | | 31 | | 32 | | Hello from the about page! 33 | | 34 | | 35 | """.stripMargin 36 | ) 37 | ) 38 | } 39 | 40 | val complexPathRoute = 41 | path("api" / "myEndpoint") { 42 | complete(StatusCodes.OK) 43 | } // /api/myEndpoint 44 | 45 | val dontConfuse = 46 | path("api/myEndpoint") { 47 | host() 48 | complete(StatusCodes.OK) 49 | } 50 | 51 | val pathEndRoute = 52 | pathEndOrSingleSlash { // localhost:8080 OR localhost:8080/ 53 | complete(StatusCodes.OK) 54 | } 55 | 56 | // Http().bindAndHandle(complexPathRoute, "localhost", 8080) 57 | 58 | 59 | /** 60 | * Type #2: extraction directives 61 | */ 62 | 63 | // GET on /api/item/42 64 | val pathExtractionRoute = 65 | path("api" / "item" / IntNumber) { (itemNumber: Int) => 66 | // other directives 67 | println(s"I've got a number in my path: $itemNumber") 68 | complete(StatusCodes.OK) 69 | } 70 | 71 | val pathMultiExtractRoute = 72 | path("api" / "order" / IntNumber / IntNumber) { (id, inventory) => 73 | println(s"I've got TWO numbers in my path: $id, $inventory") 74 | complete(StatusCodes.OK) 75 | } 76 | 77 | val queryParamExtractionRoute = 78 | // /api/item?id=45 79 | path("api" / "item") { 80 | parameter('id.as[Int]) { (itemId: Int) => 81 | println(s"I've extracted the ID as $itemId") 82 | complete(StatusCodes.OK) 83 | } 84 | } 85 | 86 | val extractRequestRoute = 87 | path("controlEndpoint") { 88 | extractRequest { (httpRequest: HttpRequest) => 89 | extractLog { (log: LoggingAdapter) => 90 | log.info(s"I got the http request: $httpRequest") 91 | complete(StatusCodes.OK) 92 | } 93 | } 94 | } 95 | 96 | Http().bindAndHandle(queryParamExtractionRoute, "localhost", 8080) 97 | 98 | /** 99 | * Type #3: composite directives 100 | */ 101 | 102 | val simpleNestedRoute = 103 | path("api" / "item") { 104 | get { 105 | complete(StatusCodes.OK) 106 | } 107 | } 108 | 109 | val compactSimpleNestedRoute = (path("api" / "item") & get) { 110 | complete(StatusCodes.OK) 111 | } 112 | 113 | val compactExtractRequestRoute = 114 | (path("controlEndpoint") & extractRequest & extractLog) { (request, log) => 115 | log.info(s"I got the http request: $request") 116 | complete(StatusCodes.OK) 117 | } 118 | 119 | // /about and /aboutUs 120 | val repeatedRoute = 121 | path("about") { 122 | complete(StatusCodes.OK) 123 | } ~ 124 | path("aboutUs") { 125 | complete(StatusCodes.OK) 126 | } 127 | 128 | val dryRoute = 129 | (path("about") | path("aboutUs")) { 130 | complete(StatusCodes.OK) 131 | } 132 | 133 | // yourblog.com/42 AND yourblog.com?postId=42 134 | 135 | val blogByIdRoute = 136 | path(IntNumber) { (blogpostId: Int) => 137 | // complex server logic 138 | complete(StatusCodes.OK) 139 | } 140 | 141 | val blogByQueryParamRoute = 142 | parameter('postId.as[Int]) { (blogpostId: Int) => 143 | // the SAME server logic 144 | complete(StatusCodes.OK) 145 | } 146 | 147 | val combinedBlodByIdRoute = 148 | (path(IntNumber) | parameter('postId.as[Int])) { (blogpostId: Int) => 149 | // your original server logic 150 | complete(StatusCodes.OK) 151 | } 152 | 153 | /** 154 | * Type #4: "actionable" directives 155 | */ 156 | 157 | val completeOkRoute = complete(StatusCodes.OK) 158 | 159 | val failedRoute = 160 | path("notSupported") { 161 | failWith(new RuntimeException("Unsupported!")) // completes with HTTP 500 162 | } 163 | 164 | val routeWithRejection = 165 | // path("home") { 166 | // reject 167 | // } ~ 168 | path("index") { 169 | completeOkRoute 170 | } 171 | 172 | /** 173 | * Exercise: can you spot the mistake?! 174 | */ 175 | val getOrPutPath = 176 | path("api" / "myEndpoint") { 177 | get { 178 | completeOkRoute 179 | } ~ 180 | post { 181 | complete(StatusCodes.Forbidden) 182 | } 183 | } 184 | 185 | Http().bindAndHandle(getOrPutPath, "localhost", 8081) 186 | } 187 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/HandlingExceptions.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.StatusCodes 6 | import akka.stream.ActorMaterializer 7 | import akka.http.scaladsl.server.Directives._ 8 | import akka.http.scaladsl.server.ExceptionHandler 9 | 10 | object HandlingExceptions extends App { 11 | 12 | implicit val system: ActorSystem = ActorSystem("HandlingExceptions") 13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 14 | import system.dispatcher 15 | 16 | val simpleRoute = 17 | path("api" / "people") { 18 | get { 19 | // directive that throws some exception 20 | throw new RuntimeException("Getting all the people took too long") 21 | } ~ 22 | post { 23 | parameter('id) { id => 24 | if (id.length > 2) 25 | throw new NoSuchElementException(s"Parameter $id cannot be found in the database, TABLE FLIP!") 26 | 27 | complete(StatusCodes.OK) 28 | } 29 | } 30 | } 31 | 32 | implicit val customExceptionHandler: ExceptionHandler = ExceptionHandler { 33 | case e: RuntimeException => 34 | complete(StatusCodes.NotFound, e.getMessage) 35 | case e: IllegalArgumentException => 36 | complete(StatusCodes.BadRequest, e.getMessage) 37 | } 38 | 39 | 40 | // Http().bindAndHandle(simpleRoute, "localhost", 8080) 41 | 42 | val runtimeExceptionHandler: ExceptionHandler = ExceptionHandler { 43 | case e: RuntimeException => 44 | complete(StatusCodes.NotFound, e.getMessage) 45 | } 46 | 47 | val noSuchElementExceptionHandler: ExceptionHandler = ExceptionHandler { 48 | case e: NoSuchElementException => 49 | complete(StatusCodes.BadRequest, e.getMessage) 50 | } 51 | 52 | val delicateHandleRoute = 53 | handleExceptions(runtimeExceptionHandler) { 54 | path("api" / "people") { 55 | get { 56 | // directive that throws some exception 57 | throw new RuntimeException("Getting all the people took too long") 58 | } ~ 59 | handleExceptions(noSuchElementExceptionHandler) { 60 | post { 61 | parameter('id) { id => 62 | if (id.length > 2) 63 | throw new NoSuchElementException(s"Parameter $id cannot be found in the database, TABLE FLIP!") 64 | 65 | complete(StatusCodes.OK) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | Http().bindAndHandle(delicateHandleRoute, "localhost", 8080) 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/HandlingRejections.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.StatusCodes 6 | import akka.stream.ActorMaterializer 7 | import akka.http.scaladsl.server.Directives._ 8 | import akka.http.scaladsl.server.{MethodRejection, MissingQueryParamRejection, Rejection, RejectionHandler} 9 | 10 | object HandlingRejections extends App { 11 | 12 | implicit val system: ActorSystem = ActorSystem("HandlingRejections") 13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 14 | import system.dispatcher 15 | 16 | 17 | val simpleRoute = 18 | path("api" / "myEndpoint") { 19 | get { 20 | complete(StatusCodes.OK) 21 | } ~ 22 | parameter('id) { _ => 23 | complete(StatusCodes.OK) 24 | } 25 | } 26 | 27 | // Rejection handlers 28 | val badRequestHandler: RejectionHandler = { rejections: Seq[Rejection] => 29 | println(s"I have encountered rejections: $rejections") 30 | Some(complete(StatusCodes.BadRequest)) 31 | } 32 | 33 | val forbiddenHandler: RejectionHandler = { rejections: Seq[Rejection] => 34 | println(s"I have encountered rejections: $rejections") 35 | Some(complete(StatusCodes.Forbidden)) 36 | } 37 | 38 | val simpleRouteWithHandlers = 39 | handleRejections(badRequestHandler) { // handle rejections from the top level 40 | // define server logic inside 41 | path("api" / "myEndpoint") { 42 | get { 43 | complete(StatusCodes.OK) 44 | } ~ 45 | post { 46 | handleRejections(forbiddenHandler) { // handle rejections WITHIN 47 | parameter('myParam) { _ => 48 | complete(StatusCodes.OK) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | // Http().bindAndHandle(simpleRouteWithHandlers, "localhost", 8080) 56 | 57 | // list(method rejection, query param rejection) 58 | implicit val customRejectionHandler = RejectionHandler.newBuilder() 59 | .handle { 60 | case m: MissingQueryParamRejection => 61 | println(s"I got a query param rejection: $m") 62 | complete("Rejected query param!") 63 | } 64 | .handle { 65 | case m: MethodRejection => 66 | println(s"I got a method rejection: $m") 67 | complete("Rejected method!") 68 | } 69 | .result() 70 | 71 | // sealing a route 72 | 73 | Http().bindAndHandle(simpleRoute, "localhost", 8080) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/HighLevelExample.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity} 6 | import akka.pattern.ask 7 | import akka.stream.ActorMaterializer 8 | import akka.http.scaladsl.server.Directives._ 9 | import akka.util.Timeout 10 | 11 | import scala.concurrent.duration._ 12 | import part2_lowlevelserver.{Guitar, GuitarDB, GuitarStoreJsonProtocol} 13 | 14 | import scala.concurrent.Future 15 | 16 | // step 1 17 | import spray.json._ 18 | 19 | object HighLevelExample extends App with GuitarStoreJsonProtocol { 20 | 21 | implicit val system: ActorSystem = ActorSystem("HighLevelExample") 22 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 23 | import system.dispatcher 24 | 25 | import GuitarDB._ 26 | 27 | /* 28 | GET /api/guitar fetches ALL the guitars in the store 29 | GET /api/guitar?id=x fetches the guitar with id X 30 | GET /api/guitar/X fetches guitar with id X 31 | GET /api/guitar/inventory?inStock=true 32 | */ 33 | 34 | /* 35 | setup 36 | */ 37 | val guitarDb = system.actorOf(Props[GuitarDB], "LowLevelGuitarDB") 38 | val guitarList = List( 39 | Guitar("Fender", "Stratocaster"), 40 | Guitar("Gibson", "Les Paul"), 41 | Guitar("Martin", "LX1") 42 | ) 43 | 44 | guitarList.foreach { guitar => 45 | guitarDb ! CreateGuitar(guitar) 46 | } 47 | 48 | implicit val timeout: Timeout = Timeout(2.seconds) 49 | val guitarServerRoute = 50 | path("api" / "guitar") { 51 | // ALWAYS PUT THE MORE SPECIFIC ROUTE FIRST 52 | parameter('id.as[Int]) { guitarId => 53 | get { 54 | val guitarFuture: Future[Option[Guitar]] = (guitarDb ? FindGuitar(guitarId)).mapTo[Option[Guitar]] 55 | val entityFuture = guitarFuture.map { guitarOption => 56 | HttpEntity( 57 | ContentTypes.`application/json`, 58 | guitarOption.toJson.prettyPrint 59 | ) 60 | } 61 | complete(entityFuture) 62 | } 63 | } ~ 64 | get { 65 | val guitarsFuture: Future[List[Guitar]] = (guitarDb ? FindAllGuitars).mapTo[List[Guitar]] 66 | val entityFuture = guitarsFuture.map { guitars => 67 | HttpEntity( 68 | ContentTypes.`application/json`, 69 | guitars.toJson.prettyPrint 70 | ) 71 | } 72 | 73 | complete(entityFuture) 74 | } 75 | } ~ 76 | path("api" / "guitar" / IntNumber) { guitarId => 77 | get { 78 | val guitarFuture: Future[Option[Guitar]] = (guitarDb ? FindGuitar(guitarId)).mapTo[Option[Guitar]] 79 | val entityFuture = guitarFuture.map { guitarOption => 80 | HttpEntity( 81 | ContentTypes.`application/json`, 82 | guitarOption.toJson.prettyPrint 83 | ) 84 | } 85 | complete(entityFuture) 86 | } 87 | } ~ 88 | path("api" / "guitar" / "inventory") { 89 | get { 90 | parameter('inStock.as[Boolean]) { inStock => 91 | val guitarFuture: Future[List[Guitar]] = (guitarDb ? FindGuitarsInStock(inStock)).mapTo[List[Guitar]] 92 | val entityFuture = guitarFuture.map { guitars => 93 | HttpEntity( 94 | ContentTypes.`application/json`, 95 | guitars.toJson.prettyPrint 96 | ) 97 | } 98 | complete(entityFuture) 99 | 100 | } 101 | } 102 | } 103 | 104 | 105 | def toHttpEntity(payload: String) = HttpEntity(ContentTypes.`application/json`, payload) 106 | 107 | val simplifiedGuitarServerRoute = 108 | (pathPrefix("api" / "guitar") & get) { 109 | path("inventory") { 110 | parameter('inStock.as[Boolean]) { inStock => 111 | complete( 112 | (guitarDb ? FindGuitarsInStock(inStock)) 113 | .mapTo[List[Guitar]] 114 | .map(_.toJson.prettyPrint) 115 | .map(toHttpEntity) 116 | ) 117 | } 118 | } ~ 119 | (path(IntNumber) | parameter('id.as[Int])) { guitarId => 120 | complete( 121 | (guitarDb ? FindGuitar(guitarId)) 122 | .mapTo[Option[Guitar]] 123 | .map(_.toJson.prettyPrint) 124 | .map(toHttpEntity) 125 | ) 126 | } ~ 127 | pathEndOrSingleSlash { 128 | complete( 129 | (guitarDb ? FindAllGuitars) 130 | .mapTo[List[Guitar]] 131 | .map(_.toJson.prettyPrint) 132 | .map(toHttpEntity) 133 | ) 134 | } 135 | } 136 | 137 | Http().bindAndHandle(simplifiedGuitarServerRoute, "localhost", 8080) 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/HighLevelExercise.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes} 6 | import akka.stream.ActorMaterializer 7 | import akka.http.scaladsl.server.Directives._ 8 | import spray.json._ 9 | 10 | import scala.concurrent.duration._ 11 | import scala.util.{Failure, Success} 12 | 13 | case class Person(pin: Int, name: String) 14 | 15 | trait PersonJsonProtocol extends DefaultJsonProtocol { 16 | implicit val personJson = jsonFormat2(Person) 17 | } 18 | 19 | object HighLevelExercise extends App with PersonJsonProtocol { 20 | 21 | implicit val system: ActorSystem = ActorSystem("HighLevelExercise") 22 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 23 | import system.dispatcher 24 | 25 | 26 | /** 27 | * Exercise: 28 | * 29 | * - GET /api/people: retrieve ALL the people you have registered 30 | * - GET /api/people/pin: retrieve the person with that PIN, return as JSON 31 | * - GET /api/people?pin=X (same) 32 | * - (harder) POST /api/people with a JSON payload denoting a Person, add that person to your database 33 | * - extract the HTTP request's payload (entity) 34 | * - extract the request 35 | * - process the entity's data 36 | */ 37 | 38 | var people = List( 39 | Person(1, "Alice"), 40 | Person(2, "Bob"), 41 | Person(3, "Charlie") 42 | ) 43 | 44 | val personServerRoute = 45 | pathPrefix("api" / "people") { 46 | get { 47 | (path(IntNumber) | parameter('pin.as[Int])) { pin => 48 | complete( 49 | HttpEntity( 50 | ContentTypes.`application/json`, 51 | people.find(_.pin == pin).toJson.prettyPrint 52 | ) 53 | ) 54 | } ~ 55 | pathEndOrSingleSlash { 56 | complete( 57 | HttpEntity( 58 | ContentTypes.`application/json`, 59 | people.toJson.prettyPrint 60 | ) 61 | ) 62 | } 63 | } ~ 64 | (post & pathEndOrSingleSlash & extractRequest & extractLog) { (request, log) => 65 | val entity = request.entity 66 | val strictEntityFuture = entity.toStrict(2.seconds) 67 | val personFuture = strictEntityFuture.map(_.data.utf8String.parseJson.convertTo[Person]) 68 | 69 | onComplete(personFuture) { 70 | case Success(person) => 71 | log.info(s"Got person: $person") 72 | people = people :+ person 73 | complete(StatusCodes.OK) 74 | case Failure(ex) => 75 | failWith(ex) 76 | } 77 | 78 | // // "side-effect" 79 | // personFuture.onComplete { 80 | // case Success(person) => 81 | // log.info(s"Got person: $person") 82 | // people = people :+ person 83 | // case Failure(ex) => 84 | // log.warning(s"Something failed with fetching the person from the entity: $ex") 85 | // } 86 | // 87 | // complete(personFuture 88 | // .map(_ => StatusCodes.OK) 89 | // .recover { 90 | // case _ => StatusCodes.InternalServerError 91 | // }) 92 | } 93 | } 94 | 95 | Http().bindAndHandle(personServerRoute, "localhost", 8080) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/HighLevelIntro.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes} 6 | import akka.http.scaladsl.server.Route 7 | import akka.stream.ActorMaterializer 8 | import part2_lowlevelserver.HttpsContext 9 | 10 | object HighLevelIntro extends App { 11 | 12 | implicit val system: ActorSystem = ActorSystem("HighLevelIntro") 13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 14 | import system.dispatcher 15 | 16 | // directives 17 | import akka.http.scaladsl.server.Directives._ 18 | 19 | val simpleRoute: Route = 20 | path("home") { // DIRECTIVE 21 | complete(StatusCodes.OK) // DIRECTIVE 22 | } 23 | 24 | val pathGetRoute: Route = 25 | path("home") { 26 | get { 27 | complete(StatusCodes.OK) 28 | } 29 | } 30 | 31 | // chaining directives with ~ 32 | 33 | val chainedRoute: Route = 34 | path("myEndpoint") { 35 | get { 36 | complete(StatusCodes.OK) 37 | } /* VERY IMPORTANT ---> */ ~ 38 | post { 39 | complete(StatusCodes.Forbidden) 40 | } 41 | } ~ 42 | path("home") { 43 | complete( 44 | HttpEntity( 45 | ContentTypes.`text/html(UTF-8)`, 46 | """ 47 | | 48 | | 49 | | Hello from the high level Akka HTTP! 50 | | 51 | | 52 | """.stripMargin 53 | ) 54 | ) 55 | } // Routing tree 56 | 57 | 58 | Http().bindAndHandle(pathGetRoute, "localhost", 8080) 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/JwtAuthorization.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import akka.actor.ActorSystem 6 | import akka.http.scaladsl.Http 7 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 8 | import akka.http.scaladsl.model.{HttpResponse, StatusCodes} 9 | import akka.http.scaladsl.model.headers.RawHeader 10 | import akka.stream.ActorMaterializer 11 | import akka.http.scaladsl.server.Directives._ 12 | import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtSprayJson} 13 | import spray.json._ 14 | 15 | import scala.util.{Failure, Success} 16 | 17 | 18 | object SecurityDomain extends DefaultJsonProtocol { 19 | case class LoginRequest(username: String, password: String) 20 | implicit val loginRequestFormat = jsonFormat2(LoginRequest) 21 | } 22 | 23 | object JwtAuthorization extends App with SprayJsonSupport { 24 | 25 | implicit val system: ActorSystem = ActorSystem() 26 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 27 | import system.dispatcher 28 | import SecurityDomain._ 29 | 30 | val superSecretPasswordDb = Map( 31 | "admin" -> "admin", 32 | "daniel" -> "Rockthejvm1!" 33 | ) 34 | 35 | val algorithm = JwtAlgorithm.HS256 36 | val secretKey = "rockthejvmsecret" 37 | 38 | def checkPassword(username: String, password: String): Boolean = 39 | superSecretPasswordDb.contains(username) && superSecretPasswordDb(username) == password 40 | 41 | def createToken(username: String, expirationPeriodInDays: Int): String = { 42 | val claims = JwtClaim( 43 | expiration = Some(System.currentTimeMillis() / 1000 + TimeUnit.DAYS.toSeconds(expirationPeriodInDays)), 44 | issuedAt = Some(System.currentTimeMillis() / 1000), 45 | issuer = Some("rockthejvm.com") 46 | ) 47 | 48 | JwtSprayJson.encode(claims, secretKey, algorithm) // JWT string 49 | } 50 | 51 | def isTokenExpired(token: String): Boolean = JwtSprayJson.decode(token, secretKey, Seq(algorithm)) match { 52 | case Success(claims) => claims.expiration.getOrElse(0L) < System.currentTimeMillis() / 1000 53 | case Failure(_) => true 54 | } 55 | 56 | def isTokenValid(token: String): Boolean = JwtSprayJson.isValid(token, secretKey, Seq(algorithm)) 57 | 58 | val loginRoute = 59 | post { 60 | entity(as[LoginRequest]) { 61 | case LoginRequest(username, password) if checkPassword(username, password) => 62 | val token = createToken(username, 1) 63 | respondWithHeader(RawHeader("Access-Token", token)) { 64 | complete(StatusCodes.OK) 65 | } 66 | case _ => complete(StatusCodes.Unauthorized) 67 | } 68 | } 69 | 70 | val authenticatedRoute = 71 | (path("secureEndpoint") & get) { 72 | optionalHeaderValueByName("Authorization") { 73 | case Some(token) => 74 | if (isTokenValid(token)) { 75 | if (isTokenExpired(token)) { 76 | complete(HttpResponse(status = StatusCodes.Unauthorized, entity = "Token expired.")) 77 | } else { 78 | complete("User accessed authorized endpoint!") 79 | } 80 | } else { 81 | complete(HttpResponse(status = StatusCodes.Unauthorized, entity = "Token is invalid, or has been tampered with.")) 82 | } 83 | case _ => complete(HttpResponse(status = StatusCodes.Unauthorized, entity = "No token provided!")) 84 | } 85 | } 86 | 87 | val route = loginRoute ~ authenticatedRoute 88 | 89 | Http().bindAndHandle(route, "localhost", 8080) 90 | } 91 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/MarshallingJSON.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props} 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 6 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes} 7 | import akka.stream.ActorMaterializer 8 | import akka.http.scaladsl.server.Directives._ 9 | import akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller 10 | import akka.pattern.ask 11 | import akka.util.Timeout 12 | // step 1 13 | import spray.json._ 14 | 15 | import scala.concurrent.duration._ 16 | 17 | case class Player(nickname: String, characterClass: String, level: Int) 18 | 19 | object GameAreaMap { 20 | case object GetAllPlayers 21 | case class GetPlayer(nickname: String) 22 | case class GetPlayersByClass(characterClass: String) 23 | case class AddPlayer(player: Player) 24 | case class RemovePlayer(player: Player) 25 | case object OperationSuccess 26 | } 27 | 28 | class GameAreaMap extends Actor with ActorLogging { 29 | import GameAreaMap._ 30 | 31 | var players = Map[String, Player]() 32 | 33 | override def receive: Receive = { 34 | case GetAllPlayers => 35 | log.info("Getting all players") 36 | sender() ! players.values.toList 37 | 38 | case GetPlayer(nickname) => 39 | log.info(s"Getting player with nickname $nickname") 40 | sender() ! players.get(nickname) 41 | 42 | case GetPlayersByClass(characterClass) => 43 | log.info(s"Getting all players with the character class $characterClass") 44 | sender() ! players.values.toList.filter(_.characterClass == characterClass) 45 | 46 | case AddPlayer(player) => 47 | log.info(s"Trying to add player $player") 48 | players = players + (player.nickname -> player) 49 | sender() ! OperationSuccess 50 | 51 | case RemovePlayer(player) => 52 | log.info(s"Trying to remove $player") 53 | players = players - player.nickname 54 | sender() ! OperationSuccess 55 | } 56 | } 57 | 58 | // step 2 59 | trait PlayerJsonProtocol extends DefaultJsonProtocol { 60 | implicit val playerFormat = jsonFormat3(Player) 61 | } 62 | 63 | object MarshallingJSON extends App 64 | // step 3 65 | with PlayerJsonProtocol 66 | // step 4 67 | with SprayJsonSupport { 68 | 69 | implicit val system: ActorSystem = ActorSystem("MarshallingJSON") 70 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 71 | import system.dispatcher 72 | import GameAreaMap._ 73 | 74 | val rtjvmGameMap = system.actorOf(Props[GameAreaMap], "rockTheJVMGameAreaMap") 75 | val playersList = List( 76 | Player("martin_killz_u", "Warrior", 70), 77 | Player("rolandbraveheart007", "Elf", 67), 78 | Player("daniel_rock03", "Wizard", 30) 79 | ) 80 | 81 | playersList.foreach { player => 82 | rtjvmGameMap ! AddPlayer(player) 83 | } 84 | 85 | /* 86 | - GET /api/player, returns all the players in the map, as JSON 87 | - GET /api/player/(nickname), returns the player with the given nickname (as JSON) 88 | - GET /api/player?nickname=X, does the same 89 | - GET /api/player/class/(charClass), returns all the players with the given character class 90 | - POST /api/player with JSON payload, adds the player to the map 91 | - (Exercise) DELETE /api/player with JSON payload, removes the player from the map 92 | */ 93 | 94 | implicit val timeout: Timeout = Timeout(2.seconds) 95 | val rtjvmGameRouteSkel = 96 | pathPrefix("api" / "player") { 97 | get { 98 | path("class" / Segment) { characterClass => 99 | val playersByClassFuture = (rtjvmGameMap ? GetPlayersByClass(characterClass)).mapTo[List[Player]] 100 | complete(playersByClassFuture) 101 | 102 | } ~ 103 | (path(Segment) | parameter('nickname)) { nickname => 104 | val playerOptionFuture = (rtjvmGameMap ? GetPlayer(nickname)).mapTo[Option[Player]] 105 | complete(playerOptionFuture) 106 | } ~ 107 | pathEndOrSingleSlash { 108 | complete((rtjvmGameMap ? GetAllPlayers).mapTo[List[Player]]) 109 | } 110 | } ~ 111 | post { 112 | entity(implicitly[FromRequestUnmarshaller[Player]]) { player => 113 | complete((rtjvmGameMap ? AddPlayer(player)).map(_ => StatusCodes.OK)) 114 | } 115 | } ~ 116 | delete { 117 | entity(as[Player]) { player => 118 | complete((rtjvmGameMap ? RemovePlayer(player)).map(_ => StatusCodes.OK)) 119 | } 120 | } 121 | } 122 | 123 | Http().bindAndHandle(rtjvmGameRouteSkel, "localhost", 8080) 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/RouteDSLSpec.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 4 | import akka.http.scaladsl.model.{ContentTypes, StatusCodes} 5 | import akka.http.scaladsl.server.Directives.{not => _, _} 6 | import akka.http.scaladsl.server.MethodRejection 7 | import akka.http.scaladsl.testkit.ScalatestRouteTest 8 | import org.scalatest.matchers.should.Matchers._ 9 | import org.scalatest.wordspec.AnyWordSpecLike 10 | import spray.json._ 11 | 12 | import scala.concurrent.Await 13 | import scala.concurrent.duration._ 14 | 15 | case class Book(id: Int, author: String, title: String) 16 | 17 | trait BookJsonProtocol extends DefaultJsonProtocol { 18 | implicit val bookFormat = jsonFormat3(Book) 19 | } 20 | 21 | class RouteDSLSpec extends AnyWordSpecLike with ScalatestRouteTest with BookJsonProtocol { 22 | 23 | import RouteDSLSpec._ 24 | 25 | "A digital library backend" should { 26 | "return all the books in the library" in { 27 | // send an HTTP request through an endpoint that you want to test 28 | // inspect the response 29 | Get("/api/book") ~> libraryRoute ~> check { 30 | // assertions 31 | status shouldBe StatusCodes.OK 32 | entityAs[List[Book]] shouldBe books 33 | } 34 | } 35 | 36 | "return a book by hitting the query parameter endpoint" in { 37 | Get("/api/book?id=2") ~> libraryRoute ~> check { 38 | status shouldBe StatusCodes.OK 39 | responseAs[Option[Book]] shouldBe Some(Book(2, "JRR Tolkien", "The Lord of the Rings")) 40 | } 41 | } 42 | 43 | "return a book by calling the endpoint with the id in the path" in { 44 | Get("/api/book/2") ~> libraryRoute ~> check { 45 | response.status shouldBe StatusCodes.OK 46 | 47 | val strictEntityFuture = response.entity.toStrict(1.seconds) 48 | val strictEntity = Await.result(strictEntityFuture, 1.seconds) 49 | 50 | strictEntity.contentType shouldBe ContentTypes.`application/json` 51 | 52 | val book = strictEntity.data.utf8String.parseJson.convertTo[Option[Book]] 53 | book shouldBe Some(Book(2, "JRR Tolkien", "The Lord of the Rings")) 54 | } 55 | } 56 | 57 | "insert a book into the 'database'" in { 58 | val newBook = Book(5, "Steven Pressfield", "The War of Art") 59 | Post("/api/book", newBook) ~> libraryRoute ~> check { 60 | status shouldBe StatusCodes.OK 61 | assert(books.contains(newBook)) 62 | books should contain(newBook) // same 63 | } 64 | } 65 | 66 | "not accept other methods than POST and GET" in { 67 | Delete("/api/book") ~> libraryRoute ~> check { 68 | // careful with the `not` verb because it's a directive as well 69 | // can solve the ambiguity by adding an `import akka.http.scaladsl.server.Directives.{not => _, _}` to remove it 70 | rejections should not be empty // "natural language" style 71 | rejections.should(not).be(empty) // same 72 | 73 | val methodRejections = rejections.collect { 74 | case rejection: MethodRejection => rejection 75 | } 76 | 77 | methodRejections.length shouldBe 2 78 | } 79 | } 80 | 81 | "return all the books of a given author" in { 82 | Get("/api/book/author/JRR%20Tolkien") ~> libraryRoute ~> check { 83 | status shouldBe StatusCodes.OK 84 | entityAs[List[Book]] shouldBe books.filter(_.author == "JRR Tolkien") 85 | } 86 | } 87 | } 88 | } 89 | 90 | object RouteDSLSpec extends BookJsonProtocol with SprayJsonSupport { 91 | 92 | // code under test 93 | var books = List( 94 | Book(1, "Harper Lee", "To Kill a Mockingbird"), 95 | Book(2, "JRR Tolkien", "The Lord of the Rings"), 96 | Book(3, "GRR Marting", "A Song of Ice and Fire"), 97 | Book(4, "Tony Robbins", "Awaken the Giant Within") 98 | ) 99 | 100 | /* 101 | GET /api/book - returns all the books in the library 102 | GET /api/book/X - return a single book with id X 103 | GET /api/book?id=X - same 104 | POST /api/book - adds a new book to the library 105 | GET /api/book/author/X - returns all the books from the actor X 106 | */ 107 | val libraryRoute = 108 | pathPrefix("api" / "book") { 109 | (path("author" / Segment) & get) { author => 110 | complete(books.filter(_.author == author)) 111 | } ~ 112 | get { 113 | (path(IntNumber) | parameter('id.as[Int])) { id => 114 | complete(books.find(_.id == id)) 115 | } ~ 116 | pathEndOrSingleSlash { 117 | complete(books) 118 | } 119 | } ~ 120 | post { 121 | entity(as[Book]) { book => 122 | books = books :+ book 123 | complete(StatusCodes.OK) 124 | } ~ 125 | complete(StatusCodes.BadRequest) 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/UploadingFiles.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import java.io.File 4 | 5 | import akka.Done 6 | import akka.actor.ActorSystem 7 | import akka.http.scaladsl.Http 8 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, Multipart} 9 | import akka.stream.{ActorMaterializer, IOResult} 10 | import akka.http.scaladsl.server.Directives._ 11 | import akka.stream.scaladsl.{FileIO, Sink, Source} 12 | import akka.util.ByteString 13 | 14 | import scala.concurrent.Future 15 | import scala.util.{Failure, Success} 16 | 17 | object UploadingFiles extends App { 18 | 19 | implicit val system: ActorSystem = ActorSystem() 20 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 21 | import system.dispatcher 22 | 23 | val filesRoute = 24 | 25 | (pathEndOrSingleSlash & get) { 26 | complete( 27 | HttpEntity( 28 | ContentTypes.`text/html(UTF-8)`, 29 | """ 30 | | 31 | | 32 | |
33 | | 34 | | 35 | |
36 | | 37 | | 38 | """.stripMargin 39 | ) 40 | ) 41 | } ~ 42 | (path("upload") & extractLog) { log => 43 | // handle uploading files 44 | // multipart/form-data 45 | 46 | entity(as[Multipart.FormData]) { formdata => 47 | // handle file payload 48 | val partsSource: Source[Multipart.FormData.BodyPart, Any] = formdata.parts 49 | 50 | val filePartsSink: Sink[Multipart.FormData.BodyPart, Future[Done]] = Sink.foreach[Multipart.FormData.BodyPart] { bodyPart => 51 | if (bodyPart.name == "myFile") { 52 | // create a file 53 | val filename = "src/main/resources/download/" + bodyPart.filename.getOrElse("tempFile_" + System.currentTimeMillis()) 54 | val file = new File(filename) 55 | 56 | log.info(s"Writing to file: $filename") 57 | 58 | val fileContentsSource: Source[ByteString, _] = bodyPart.entity.dataBytes 59 | val fileContentsSink: Sink[ByteString, Future[IOResult]] = FileIO.toPath(file.toPath) 60 | 61 | // writing the data to the file 62 | val mat = fileContentsSource.runWith(fileContentsSink) 63 | // treat the Future[IOResult] here 64 | } 65 | } 66 | 67 | val writeOperationFuture = partsSource.runWith(filePartsSink) 68 | onComplete(writeOperationFuture) { 69 | case Success(_) => complete("File uploaded.") 70 | case Failure(ex) => complete(s"File failed to upload: $ex") 71 | } 72 | } 73 | } 74 | 75 | Http().bindAndHandle(filesRoute, "localhost", 8080) 76 | 77 | def jeg = ??? 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/part3_highlevelserver/WebsocketsDemo.scala: -------------------------------------------------------------------------------- 1 | package part3_highlevelserver 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity} 6 | import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage} 7 | import akka.stream.ActorMaterializer 8 | import akka.stream.scaladsl.{Flow, Sink, Source} 9 | import akka.util.CompactByteString 10 | import akka.http.scaladsl.server.Directives._ 11 | 12 | import scala.concurrent.duration._ 13 | 14 | object WebsocketsDemo extends App { 15 | 16 | implicit val system: ActorSystem = ActorSystem() 17 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 18 | import system.dispatcher 19 | 20 | // Message: TextMessage vs BinaryMessage 21 | 22 | val textMessage = TextMessage(Source.single("hello via a text message")) 23 | val binaryMessage = BinaryMessage(Source.single(CompactByteString("hello via a binary message"))) 24 | 25 | 26 | val html = 27 | """ 28 | | 29 | | 30 | | 46 | | 47 | | 48 | | 49 | | Starting websocket... 50 | |
51 | |
52 | | 53 | | 54 | | 55 | """.stripMargin 56 | 57 | 58 | def websocketFlow: Flow[Message, Message, Any] = Flow[Message].map { 59 | case tm: TextMessage => 60 | TextMessage(Source.single("Server says back:") ++ tm.textStream ++ Source.single("!")) 61 | case bm: BinaryMessage => 62 | bm.dataStream.runWith(Sink.ignore) 63 | TextMessage(Source.single("Server received a binary message...")) 64 | } 65 | 66 | val websocketRoute = 67 | (pathEndOrSingleSlash & get) { 68 | complete( 69 | HttpEntity( 70 | ContentTypes.`text/html(UTF-8)`, 71 | html 72 | ) 73 | ) 74 | } ~ 75 | path("greeter") { 76 | handleWebSocketMessages(socialFlow) 77 | } 78 | 79 | Http().bindAndHandle(websocketRoute, "localhost", 8080) 80 | 81 | 82 | case class SocialPost(owner: String, content: String) 83 | 84 | val socialFeed = Source( 85 | List( 86 | SocialPost("Martin", "Scala 3 has been announced!"), 87 | SocialPost("Daniel", "A new Rock the JVM course is open!"), 88 | SocialPost("Martin", "I killed Java.") 89 | ) 90 | ) 91 | 92 | val socialMessages = socialFeed 93 | .throttle(1, 2.seconds) 94 | .map(socialPost => TextMessage(s"${socialPost.owner} said: ${socialPost.content}")) 95 | 96 | val socialFlow: Flow[Message, Message, Any] = Flow.fromSinkAndSource( 97 | Sink.foreach[Message](println), 98 | socialMessages 99 | ) 100 | 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/scala/part4_client/ConnectionLevel.scala: -------------------------------------------------------------------------------- 1 | package part4_client 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model._ 6 | import akka.stream.ActorMaterializer 7 | import akka.stream.scaladsl.{Sink, Source} 8 | 9 | import scala.util.{Failure, Success} 10 | 11 | import spray.json._ 12 | 13 | object ConnectionLevel extends App with PaymentJsonProtocol { 14 | 15 | implicit val system: ActorSystem = ActorSystem("ConnectionLevel") 16 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 17 | import system.dispatcher 18 | 19 | 20 | val connectionFlow = Http().outgoingConnection("www.google.com") 21 | 22 | def oneOffRequest(request: HttpRequest) = 23 | Source.single(request).via(connectionFlow).runWith(Sink.head) 24 | 25 | oneOffRequest(HttpRequest()).onComplete { 26 | case Success(response) => println(s"Got successful response: $response") 27 | case Failure(ex) => println(s"Sending the request failed: $ex") 28 | } 29 | 30 | /* 31 | A small payments system 32 | */ 33 | 34 | import PaymentSystemDomain._ 35 | 36 | val creditCards = List( 37 | CreditCard("4242-4242-4242-4242", "424", "tx-test-account"), 38 | CreditCard("1234-1234-1234-1234", "123", "tx-daniels-account"), 39 | CreditCard("1234-1234-4321-4321", "321", "my-awesome-account") 40 | ) 41 | 42 | val paymentRequests = creditCards.map(creditCard => PaymentRequest(creditCard, "rtjvm-store-account", 99)) 43 | val serverHttpRequests = paymentRequests.map(paymentRequest => 44 | HttpRequest( 45 | HttpMethods.POST, 46 | uri = Uri("/api/payments"), 47 | entity = HttpEntity( 48 | ContentTypes.`application/json`, 49 | paymentRequest.toJson.prettyPrint 50 | ) 51 | ) 52 | ) 53 | 54 | Source(serverHttpRequests) 55 | .via(Http().outgoingConnection("localhost", 8080)) 56 | .to(Sink.foreach[HttpResponse](println)) 57 | .run() 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/part4_client/HostLevel.scala: -------------------------------------------------------------------------------- 1 | package part4_client 2 | 3 | import java.util.UUID 4 | 5 | import akka.actor.ActorSystem 6 | import akka.http.scaladsl.Http 7 | import akka.http.scaladsl.model._ 8 | import akka.stream.ActorMaterializer 9 | import akka.stream.scaladsl.{Sink, Source} 10 | 11 | import scala.util.{Failure, Success, Try} 12 | import spray.json._ 13 | 14 | object HostLevel extends App with PaymentJsonProtocol { 15 | 16 | implicit val system: ActorSystem = ActorSystem("HostLevel") 17 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 18 | import system.dispatcher 19 | 20 | val poolFlow = Http().cachedHostConnectionPool[Int]("www.google.com") 21 | 22 | Source(1 to 10) 23 | .map(i => (HttpRequest(), i)) 24 | .via(poolFlow) 25 | .map { 26 | case (Success(response), value) => 27 | // VERY IMPORTANT 28 | response.discardEntityBytes() 29 | s"Request $value has received response: $response" 30 | case (Failure(ex), value) => 31 | s"Request $value has failed: $ex" 32 | } 33 | // .runWith(Sink.foreach[String](println)) 34 | 35 | 36 | import PaymentSystemDomain._ 37 | val creditCards = List( 38 | CreditCard("4242-4242-4242-4242", "424", "tx-test-account"), 39 | CreditCard("1234-1234-1234-1234", "123", "tx-daniels-account"), 40 | CreditCard("1234-1234-4321-4321", "321", "my-awesome-account") 41 | ) 42 | 43 | val paymentRequests = creditCards.map(creditCard => PaymentRequest(creditCard, "rtjvm-store-account", 99)) 44 | val serverHttpRequests = paymentRequests.map(paymentRequest => 45 | ( 46 | HttpRequest( 47 | HttpMethods.POST, 48 | uri = Uri("/api/payments"), 49 | entity = HttpEntity( 50 | ContentTypes.`application/json`, 51 | paymentRequest.toJson.prettyPrint 52 | ) 53 | ), 54 | UUID.randomUUID().toString 55 | ) 56 | ) 57 | 58 | Source(serverHttpRequests) 59 | .via(Http().cachedHostConnectionPool[String]("localhost", 8080)) 60 | .runForeach { // (Try[HttpResponse], String) 61 | case (Success(response@HttpResponse(StatusCodes.Forbidden, _, _, _)), orderId) => 62 | println(s"The order ID $orderId was not allowed to proceed: $response") 63 | case (Success(response), orderId) => 64 | println(s"The order ID $orderId was successful and returned the response: $response") 65 | // do something with the order ID: dispatch it, send a notification to the customer, etc 66 | case (Failure(ex), orderId) => 67 | println(s"The order ID $orderId could not be completed: $ex") 68 | } 69 | 70 | // high-volume, low-latency requests 71 | } 72 | -------------------------------------------------------------------------------- /src/main/scala/part4_client/PaymentSystem.scala: -------------------------------------------------------------------------------- 1 | package part4_client 2 | 3 | import akka.pattern.ask 4 | 5 | import scala.concurrent.duration._ 6 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props} 7 | import akka.http.scaladsl.Http 8 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 9 | import akka.http.scaladsl.model.StatusCodes 10 | import akka.stream.ActorMaterializer 11 | import akka.http.scaladsl.server.Directives._ 12 | import akka.util.Timeout 13 | import spray.json._ 14 | 15 | 16 | case class CreditCard(serialNumber: String, securityCode: String, account: String) 17 | 18 | object PaymentSystemDomain { 19 | case class PaymentRequest(creditCard: CreditCard, receiverAccount: String, amount: Double) 20 | case object PaymentAccepted 21 | case object PaymentRejected 22 | } 23 | 24 | trait PaymentJsonProtocol extends DefaultJsonProtocol { 25 | implicit val creditCardFormat = jsonFormat3(CreditCard) 26 | implicit val paymentRequestFormat = jsonFormat3(PaymentSystemDomain.PaymentRequest) 27 | } 28 | 29 | class PaymentValidator extends Actor with ActorLogging { 30 | import PaymentSystemDomain._ 31 | 32 | override def receive: Receive = { 33 | case PaymentRequest(CreditCard(serialNumber, _, senderAccount), receiverAccount, amount) => 34 | log.info(s"$senderAccount is trying to send $amount dollars to $receiverAccount") 35 | if (serialNumber == "1234-1234-1234-1234") sender() ! PaymentRejected 36 | else sender() ! PaymentAccepted 37 | } 38 | } 39 | 40 | object PaymentSystem extends App with PaymentJsonProtocol with SprayJsonSupport { 41 | 42 | // microservice for payments 43 | implicit val system: ActorSystem = ActorSystem("PaymentSystem") 44 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 45 | import system.dispatcher 46 | import PaymentSystemDomain._ 47 | 48 | val paymentValidator = system.actorOf(Props[PaymentValidator], "paymentValidator") 49 | implicit val timeout: Timeout = Timeout(2.seconds) 50 | 51 | val paymentRoute = 52 | path("api" / "payments") { 53 | post { 54 | entity(as[PaymentRequest]) { paymentRequest => 55 | val validationResponseFuture = (paymentValidator ? paymentRequest).map { 56 | case PaymentRejected => StatusCodes.Forbidden 57 | case PaymentAccepted => StatusCodes.OK 58 | case _ => StatusCodes.BadRequest 59 | } 60 | 61 | complete(validationResponseFuture) 62 | } 63 | } 64 | } 65 | 66 | Http().bindAndHandle(paymentRoute, "localhost", 8080) 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/scala/part4_client/RequestLevel.scala: -------------------------------------------------------------------------------- 1 | package part4_client 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model._ 6 | import akka.stream.ActorMaterializer 7 | import akka.stream.scaladsl.Source 8 | 9 | import scala.util.{Failure, Success} 10 | import spray.json._ 11 | 12 | object RequestLevel extends App with PaymentJsonProtocol { 13 | 14 | implicit val system: ActorSystem = ActorSystem("RequestLevelAPI") 15 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 16 | import system.dispatcher 17 | 18 | val responseFuture = Http().singleRequest(HttpRequest(uri = "http://www.google.com")) 19 | 20 | responseFuture.onComplete { 21 | case Success(response) => 22 | // VERY IMPORTANT 23 | response.discardEntityBytes() 24 | println(s"The request was successful and returned: $response") 25 | case Failure(ex) => 26 | println(s"The request failed with: $ex") 27 | } 28 | 29 | import PaymentSystemDomain._ 30 | 31 | val creditCards = List( 32 | CreditCard("4242-4242-4242-4242", "424", "tx-test-account"), 33 | CreditCard("1234-1234-1234-1234", "123", "tx-daniels-account"), 34 | CreditCard("1234-1234-4321-4321", "321", "my-awesome-account") 35 | ) 36 | 37 | val paymentRequests = creditCards.map(creditCard => PaymentRequest(creditCard, "rtjvm-store-account", 99)) 38 | val serverHttpRequests = paymentRequests.map(paymentRequest => 39 | HttpRequest( 40 | HttpMethods.POST, 41 | uri = "http://localhost:8080/api/payments", 42 | entity = HttpEntity( 43 | ContentTypes.`application/json`, 44 | paymentRequest.toJson.prettyPrint 45 | ) 46 | ) 47 | ) 48 | 49 | Source(serverHttpRequests) 50 | .mapAsyncUnordered(10)(request => Http().singleRequest(request)) 51 | .runForeach(println) 52 | 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/playground/Playground.scala: -------------------------------------------------------------------------------- 1 | package playground 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model._ 6 | import akka.stream.ActorMaterializer 7 | import akka.http.scaladsl.server.Directives._ 8 | import scala.io.StdIn 9 | 10 | object Playground extends App { 11 | 12 | implicit val system: ActorSystem = ActorSystem("AkkaHttpPlayground") 13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6 14 | 15 | import system.dispatcher 16 | 17 | val simpleRoute = 18 | pathEndOrSingleSlash { 19 | complete(HttpEntity( 20 | ContentTypes.`text/html(UTF-8)`, 21 | """ 22 | | 23 | | 24 | | Rock the JVM with Akka HTTP! 25 | | 26 | | 27 | """.stripMargin 28 | )) 29 | } 30 | 31 | val bindingFuture = Http().bindAndHandle(simpleRoute, "localhost", 8080) 32 | // wait for a new line, then terminate the server 33 | StdIn.readLine() 34 | bindingFuture 35 | .flatMap(_.unbind()) 36 | .onComplete(_ => system.terminate()) 37 | } 38 | --------------------------------------------------------------------------------