Remove Akka-HTTP sources from akka/akka, moving to akka/akka-http! (#21690)
This commit is contained in:
parent
09a6d2ede1
commit
a6a5556a8f
1155 changed files with 20 additions and 96517 deletions
|
|
@ -1,219 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.Done
|
||||
import akka.actor.{ ActorLogging, ActorSystem }
|
||||
import akka.http.scaladsl.model.HttpEntity.Strict
|
||||
import akka.http.scaladsl.model.HttpMessage.DiscardedEntity
|
||||
import akka.stream.{ IOResult, Materializer }
|
||||
import akka.stream.scaladsl.{ Framing, Sink }
|
||||
import akka.util.ByteString
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
import scala.concurrent.{ ExecutionContextExecutor, Future }
|
||||
|
||||
class HttpClientExampleSpec extends WordSpec with Matchers with CompileOnlySpec {
|
||||
|
||||
"manual-entity-consume-example-1" in compileOnlySpec {
|
||||
//#manual-entity-consume-example-1
|
||||
import java.io.File
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.Framing
|
||||
import akka.stream.scaladsl.FileIO
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val dispatcher = system.dispatcher
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val response: HttpResponse = ???
|
||||
|
||||
response.entity.dataBytes
|
||||
.via(Framing.delimiter(ByteString("\n"), maximumFrameLength = 256))
|
||||
.map(transformEachLine)
|
||||
.runWith(FileIO.toPath(new File("/tmp/example.out").toPath))
|
||||
|
||||
def transformEachLine(line: ByteString): ByteString = ???
|
||||
|
||||
//#manual-entity-consume-example-1
|
||||
}
|
||||
|
||||
"manual-entity-consume-example-2" in compileOnlySpec {
|
||||
//#manual-entity-consume-example-2
|
||||
import java.io.File
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.model._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val dispatcher = system.dispatcher
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
case class ExamplePerson(name: String)
|
||||
def parse(line: ByteString): ExamplePerson = ???
|
||||
|
||||
val response: HttpResponse = ???
|
||||
|
||||
// toStrict to enforce all data be loaded into memory from the connection
|
||||
val strictEntity: Future[HttpEntity.Strict] = response.entity.toStrict(3.seconds)
|
||||
|
||||
// while API remains the same to consume dataBytes, now they're in memory already:
|
||||
val transformedData: Future[ExamplePerson] =
|
||||
strictEntity flatMap { e =>
|
||||
e.dataBytes
|
||||
.runFold(ByteString.empty) { case (acc, b) => acc ++ b }
|
||||
.map(parse)
|
||||
}
|
||||
|
||||
//#manual-entity-consume-example-2
|
||||
}
|
||||
|
||||
"manual-entity-discard-example-1" in compileOnlySpec {
|
||||
//#manual-entity-discard-example-1
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val dispatcher = system.dispatcher
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val response1: HttpResponse = ??? // obtained from an HTTP call (see examples below)
|
||||
|
||||
val discarded: DiscardedEntity = response1.discardEntityBytes()
|
||||
discarded.future.onComplete { case done => println("Entity discarded completely!") }
|
||||
|
||||
//#manual-entity-discard-example-1
|
||||
}
|
||||
"manual-entity-discard-example-2" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val dispatcher = system.dispatcher
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
//#manual-entity-discard-example-2
|
||||
val response1: HttpResponse = ??? // obtained from an HTTP call (see examples below)
|
||||
|
||||
val discardingComplete: Future[Done] = response1.entity.dataBytes.runWith(Sink.ignore)
|
||||
discardingComplete.onComplete { case done => println("Entity discarded completely!") }
|
||||
//#manual-entity-discard-example-2
|
||||
}
|
||||
|
||||
"outgoing-connection-example" in compileOnlySpec {
|
||||
//#outgoing-connection-example
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
object WebClient {
|
||||
def main(args: Array[String]): Unit = {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val connectionFlow: Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] =
|
||||
Http().outgoingConnection("akka.io")
|
||||
val responseFuture: Future[HttpResponse] =
|
||||
Source.single(HttpRequest(uri = "/"))
|
||||
.via(connectionFlow)
|
||||
.runWith(Sink.head)
|
||||
|
||||
responseFuture.andThen {
|
||||
case Success(_) => println("request succeded")
|
||||
case Failure(_) => println("request failed")
|
||||
}.andThen {
|
||||
case _ => system.terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
//#outgoing-connection-example
|
||||
}
|
||||
|
||||
"host-level-example" in compileOnlySpec {
|
||||
//#host-level-example
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.Try
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// construct a pool client flow with context type `Int`
|
||||
val poolClientFlow = Http().cachedHostConnectionPool[Int]("akka.io")
|
||||
val responseFuture: Future[(Try[HttpResponse], Int)] =
|
||||
Source.single(HttpRequest(uri = "/") -> 42)
|
||||
.via(poolClientFlow)
|
||||
.runWith(Sink.head)
|
||||
//#host-level-example
|
||||
}
|
||||
|
||||
"single-request-example" in compileOnlySpec {
|
||||
//#single-request-example
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val responseFuture: Future[HttpResponse] =
|
||||
Http().singleRequest(HttpRequest(uri = "http://akka.io"))
|
||||
//#single-request-example
|
||||
}
|
||||
|
||||
"single-request-in-actor-example" in compileOnlySpec {
|
||||
//#single-request-in-actor-example
|
||||
import akka.actor.Actor
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.ActorMaterializerSettings
|
||||
|
||||
class Myself extends Actor
|
||||
with ActorLogging {
|
||||
|
||||
import akka.pattern.pipe
|
||||
import context.dispatcher
|
||||
|
||||
final implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(context.system))
|
||||
|
||||
val http = Http(context.system)
|
||||
|
||||
override def preStart() = {
|
||||
http.singleRequest(HttpRequest(uri = "http://akka.io"))
|
||||
.pipeTo(self)
|
||||
}
|
||||
|
||||
def receive = {
|
||||
case HttpResponse(StatusCodes.OK, headers, entity, _) =>
|
||||
log.info("Got response, body: " + entity.dataBytes.runFold(ByteString(""))(_ ++ _))
|
||||
case resp @ HttpResponse(code, _, _, _) =>
|
||||
log.info("Request failed, response code: " + code)
|
||||
resp.discardEntityBytes()
|
||||
}
|
||||
|
||||
}
|
||||
//#single-request-in-actor-example
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,682 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.scaladsl.model.{ RequestEntity, StatusCodes }
|
||||
import akka.stream.scaladsl.Sink
|
||||
import akka.testkit.TestActors
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
import scala.language.postfixOps
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
|
||||
class HttpServerExampleSpec extends WordSpec with Matchers
|
||||
with CompileOnlySpec {
|
||||
|
||||
// never actually called
|
||||
val log: LoggingAdapter = null
|
||||
|
||||
"binding-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] =
|
||||
Http().bind(interface = "localhost", port = 8080)
|
||||
val bindingFuture: Future[Http.ServerBinding] =
|
||||
serverSource.to(Sink.foreach { connection => // foreach materializes the source
|
||||
println("Accepted new connection from " + connection.remoteAddress)
|
||||
// ... and then actually handle the connection
|
||||
}).run()
|
||||
}
|
||||
|
||||
"binding-failure-high-level-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object WebServer {
|
||||
def main(args: Array[String]) {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future onFailure in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val handler = get {
|
||||
complete("Hello world!")
|
||||
}
|
||||
|
||||
// let's say the OS won't allow us to bind to 80.
|
||||
val (host, port) = ("localhost", 80)
|
||||
val bindingFuture: Future[ServerBinding] =
|
||||
Http().bindAndHandle(handler, host, port)
|
||||
|
||||
bindingFuture.onFailure {
|
||||
case ex: Exception =>
|
||||
log.error(ex, "Failed to bind to {}:{}!", host, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mock values:
|
||||
val handleConnections = {
|
||||
import akka.stream.scaladsl.Sink
|
||||
Sink.ignore.mapMaterializedValue(_ => Future.failed(new Exception("")))
|
||||
}
|
||||
|
||||
"binding-failure-handling" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future onFailure in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
// let's say the OS won't allow us to bind to 80.
|
||||
val (host, port) = ("localhost", 80)
|
||||
val serverSource = Http().bind(host, port)
|
||||
|
||||
val bindingFuture: Future[ServerBinding] = serverSource
|
||||
.to(handleConnections) // Sink[Http.IncomingConnection, _]
|
||||
.run()
|
||||
|
||||
bindingFuture.onFailure {
|
||||
case ex: Exception =>
|
||||
log.error(ex, "Failed to bind to {}:{}!", host, port)
|
||||
}
|
||||
}
|
||||
|
||||
object MyExampleMonitoringActor {
|
||||
def props = TestActors.echoActorProps
|
||||
}
|
||||
|
||||
"incoming-connections-source-failure-handling" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
import Http._
|
||||
val (host, port) = ("localhost", 8080)
|
||||
val serverSource = Http().bind(host, port)
|
||||
|
||||
val failureMonitor: ActorRef = system.actorOf(MyExampleMonitoringActor.props)
|
||||
|
||||
val reactToTopLevelFailures = Flow[IncomingConnection]
|
||||
.watchTermination()((_, termination) => termination.onFailure {
|
||||
case cause => failureMonitor ! cause
|
||||
})
|
||||
|
||||
serverSource
|
||||
.via(reactToTopLevelFailures)
|
||||
.to(handleConnections) // Sink[Http.IncomingConnection, _]
|
||||
.run()
|
||||
}
|
||||
|
||||
"connection-stream-failure-handling" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val (host, port) = ("localhost", 8080)
|
||||
val serverSource = Http().bind(host, port)
|
||||
|
||||
val reactToConnectionFailure = Flow[HttpRequest]
|
||||
.recover[HttpRequest] {
|
||||
case ex =>
|
||||
// handle the failure somehow
|
||||
throw ex
|
||||
}
|
||||
|
||||
val httpEcho = Flow[HttpRequest]
|
||||
.via(reactToConnectionFailure)
|
||||
.map { request =>
|
||||
// simple streaming (!) "echo" response:
|
||||
HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, request.entity.dataBytes))
|
||||
}
|
||||
|
||||
serverSource
|
||||
.runForeach { con =>
|
||||
con.handleWith(httpEcho)
|
||||
}
|
||||
}
|
||||
|
||||
"full-server-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.Sink
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val serverSource = Http().bind(interface = "localhost", port = 8080)
|
||||
|
||||
val requestHandler: HttpRequest => HttpResponse = {
|
||||
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
|
||||
HttpResponse(entity = HttpEntity(
|
||||
ContentTypes.`text/html(UTF-8)`,
|
||||
"<html><body>Hello world!</body></html>"))
|
||||
|
||||
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
|
||||
HttpResponse(entity = "PONG!")
|
||||
|
||||
case HttpRequest(GET, Uri.Path("/crash"), _, _, _) =>
|
||||
sys.error("BOOM!")
|
||||
|
||||
case r: HttpRequest =>
|
||||
r.discardEntityBytes() // important to drain incoming HTTP Entity stream
|
||||
HttpResponse(404, entity = "Unknown resource!")
|
||||
}
|
||||
|
||||
val bindingFuture: Future[Http.ServerBinding] =
|
||||
serverSource.to(Sink.foreach { connection =>
|
||||
println("Accepted new connection from " + connection.remoteAddress)
|
||||
|
||||
connection handleWithSyncHandler requestHandler
|
||||
// this is equivalent to
|
||||
// connection handleWith { Flow[HttpRequest] map requestHandler }
|
||||
}).run()
|
||||
}
|
||||
|
||||
"low-level-server-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import scala.io.StdIn
|
||||
|
||||
object WebServer {
|
||||
|
||||
def main(args: Array[String]) {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future map/flatmap in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val requestHandler: HttpRequest => HttpResponse = {
|
||||
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
|
||||
HttpResponse(entity = HttpEntity(
|
||||
ContentTypes.`text/html(UTF-8)`,
|
||||
"<html><body>Hello world!</body></html>"))
|
||||
|
||||
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
|
||||
HttpResponse(entity = "PONG!")
|
||||
|
||||
case HttpRequest(GET, Uri.Path("/crash"), _, _, _) =>
|
||||
sys.error("BOOM!")
|
||||
|
||||
case r: HttpRequest =>
|
||||
r.discardEntityBytes() // important to drain incoming HTTP Entity stream
|
||||
HttpResponse(404, entity = "Unknown resource!")
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandleSync(requestHandler, "localhost", 8080)
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
StdIn.readLine() // let it run until user presses return
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// format: OFF
|
||||
|
||||
"high-level-server-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
import scala.io.StdIn
|
||||
|
||||
object WebServer {
|
||||
def main(args: Array[String]) {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val route =
|
||||
get {
|
||||
pathSingleSlash {
|
||||
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`,"<html><body>Hello world!</body></html>"))
|
||||
} ~
|
||||
path("ping") {
|
||||
complete("PONG!")
|
||||
} ~
|
||||
path("crash") {
|
||||
sys.error("BOOM!")
|
||||
}
|
||||
}
|
||||
|
||||
// `route` will be implicitly converted to `Flow` using `RouteResult.route2HandlerFlow`
|
||||
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
StdIn.readLine() // let it run until user presses return
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"minimal-routing-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
import scala.io.StdIn
|
||||
|
||||
object WebServer {
|
||||
def main(args: Array[String]) {
|
||||
|
||||
implicit val system = ActorSystem("my-system")
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val route =
|
||||
path("hello") {
|
||||
get {
|
||||
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
|
||||
}
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
|
||||
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
StdIn.readLine() // let it run until user presses return
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"long-routing-example" in compileOnlySpec {
|
||||
//#long-routing-example
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.http.scaladsl.coding.Deflate
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshaller
|
||||
import akka.http.scaladsl.model.StatusCodes.MovedPermanently
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller
|
||||
import akka.pattern.ask
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.util.Timeout
|
||||
|
||||
// types used by the API routes
|
||||
type Money = Double // only for demo purposes, don't try this at home!
|
||||
type TransactionResult = String
|
||||
case class User(name: String)
|
||||
case class Order(email: String, amount: Money)
|
||||
case class Update(order: Order)
|
||||
case class OrderItem(i: Int, os: Option[String], s: String)
|
||||
|
||||
// marshalling would usually be derived automatically using libraries
|
||||
implicit val orderUM: FromRequestUnmarshaller[Order] = ???
|
||||
implicit val orderM: ToResponseMarshaller[Order] = ???
|
||||
implicit val orderSeqM: ToResponseMarshaller[Seq[Order]] = ???
|
||||
implicit val timeout: Timeout = ??? // for actor asks
|
||||
implicit val ec: ExecutionContext = ???
|
||||
implicit val mat: ActorMaterializer = ???
|
||||
implicit val sys: ActorSystem = ???
|
||||
|
||||
// backend entry points
|
||||
def myAuthenticator: Authenticator[User] = ???
|
||||
def retrieveOrdersFromDB: Seq[Order] = ???
|
||||
def myDbActor: ActorRef = ???
|
||||
def processOrderRequest(id: Int, complete: Order => Unit): Unit = ???
|
||||
|
||||
val route = {
|
||||
path("orders") {
|
||||
authenticateBasic(realm = "admin area", myAuthenticator) { user =>
|
||||
get {
|
||||
encodeResponseWith(Deflate) {
|
||||
complete {
|
||||
// marshal custom object with in-scope marshaller
|
||||
retrieveOrdersFromDB
|
||||
}
|
||||
}
|
||||
} ~
|
||||
post {
|
||||
// decompress gzipped or deflated requests if required
|
||||
decodeRequest {
|
||||
// unmarshal with in-scope unmarshaller
|
||||
entity(as[Order]) { order =>
|
||||
complete {
|
||||
// ... write order to DB
|
||||
"Order received"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
// extract URI path element as Int
|
||||
pathPrefix("order" / IntNumber) { orderId =>
|
||||
pathEnd {
|
||||
(put | parameter('method ! "put")) {
|
||||
// form extraction from multipart or www-url-encoded forms
|
||||
formFields(('email, 'total.as[Money])).as(Order) { order =>
|
||||
complete {
|
||||
// complete with serialized Future result
|
||||
(myDbActor ? Update(order)).mapTo[TransactionResult]
|
||||
}
|
||||
}
|
||||
} ~
|
||||
get {
|
||||
// debugging helper
|
||||
logRequest("GET-ORDER") {
|
||||
// use in-scope marshaller to create completer function
|
||||
completeWith(instanceOf[Order]) { completer =>
|
||||
// custom
|
||||
processOrderRequest(orderId, completer)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("items") {
|
||||
get {
|
||||
// parameters to case class extraction
|
||||
parameters(('size.as[Int], 'color ?, 'dangerous ? "no"))
|
||||
.as(OrderItem) { orderItem =>
|
||||
// ... route using case class instance created from
|
||||
// required and optional query parameters
|
||||
complete("") // hide
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
pathPrefix("documentation") {
|
||||
// optionally compresses the response with Gzip or Deflate
|
||||
// if the client accepts compressed responses
|
||||
encodeResponse {
|
||||
// serve up static content from a JAR resource
|
||||
getFromResourceDirectory("docs")
|
||||
}
|
||||
} ~
|
||||
path("oldApi" / Remaining) { pathRest =>
|
||||
redirect("http://oldapi.example.com/" + pathRest, MovedPermanently)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"stream random numbers" in compileOnlySpec {
|
||||
//#stream-random-numbers
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.scaladsl._
|
||||
import akka.util.ByteString
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.{HttpEntity, ContentTypes}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
import scala.util.Random
|
||||
import scala.io.StdIn
|
||||
|
||||
object WebServer {
|
||||
|
||||
def main(args: Array[String]) {
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
// streams are re-usable so we can define it here
|
||||
// and use it for every request
|
||||
val numbers = Source.fromIterator(() =>
|
||||
Iterator.continually(Random.nextInt()))
|
||||
|
||||
val route =
|
||||
path("random") {
|
||||
get {
|
||||
complete(
|
||||
HttpEntity(
|
||||
ContentTypes.`text/plain(UTF-8)`,
|
||||
// transform each number to a chunk of bytes
|
||||
numbers.map(n => ByteString(s"$n\n"))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
StdIn.readLine() // let it run until user presses return
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
}
|
||||
}
|
||||
//#stream-random-numbers
|
||||
}
|
||||
|
||||
|
||||
object Auction {
|
||||
import akka.actor.Props
|
||||
def props: Props = ???
|
||||
}
|
||||
|
||||
"interact with an actor" in compileOnlySpec {
|
||||
//#actor-interaction
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import akka.pattern.ask
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.util.Timeout
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
import scala.concurrent.duration._
|
||||
import scala.io.StdIn
|
||||
|
||||
object WebServer {
|
||||
|
||||
case class Bid(userId: String, bid: Int)
|
||||
case object GetBids
|
||||
case class Bids(bids: List[Bid])
|
||||
|
||||
// these are from spray-json
|
||||
implicit val bidFormat = jsonFormat2(Bid)
|
||||
implicit val bidsFormat = jsonFormat1(Bids)
|
||||
|
||||
def main(args: Array[String]) {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val auction = system.actorOf(Auction.props, "auction")
|
||||
|
||||
val route =
|
||||
path("auction") {
|
||||
put {
|
||||
parameter("bid".as[Int], "user") { (bid, user) =>
|
||||
// place a bid, fire-and-forget
|
||||
auction ! Bid(user, bid)
|
||||
complete((StatusCodes.Accepted, "bid placed"))
|
||||
}
|
||||
}
|
||||
get {
|
||||
implicit val timeout: Timeout = 5.seconds
|
||||
|
||||
// query the actor for the current auction state
|
||||
val bids: Future[Bids] = (auction ? GetBids).mapTo[Bids]
|
||||
complete(bids)
|
||||
}
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
StdIn.readLine() // let it run until user presses return
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
|
||||
}
|
||||
}
|
||||
//#actor-interaction
|
||||
}
|
||||
|
||||
"consume entity using entity directive" in compileOnlySpec {
|
||||
//#consume-entity-directive
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import akka.stream.ActorMaterializer
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
final case class Bid(userId: String, bid: Int)
|
||||
|
||||
// these are from spray-json
|
||||
implicit val bidFormat = jsonFormat2(Bid)
|
||||
|
||||
val route =
|
||||
path("bid") {
|
||||
put {
|
||||
entity(as[Bid]) { bid =>
|
||||
// incoming entity is fully consumed and converted into a Bid
|
||||
complete("The bid was: " + bid)
|
||||
}
|
||||
}
|
||||
}
|
||||
//#consume-entity-directive
|
||||
}
|
||||
|
||||
"consume entity using raw dataBytes to file" in compileOnlySpec {
|
||||
//#consume-raw-dataBytes
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.scaladsl.FileIO
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
import java.io.File
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val route =
|
||||
(put & path("lines")) {
|
||||
withoutSizeLimit {
|
||||
extractDataBytes { bytes =>
|
||||
val finishedWriting = bytes.runWith(FileIO.toPath(new File("/tmp/example.out").toPath))
|
||||
|
||||
// we only want to respond once the incoming data has been handled:
|
||||
onComplete(finishedWriting) { ioResult =>
|
||||
complete("Finished writing data: " + ioResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//#consume-raw-dataBytes
|
||||
}
|
||||
|
||||
"drain entity using request#discardEntityBytes" in compileOnlySpec {
|
||||
//#discard-discardEntityBytes
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.scaladsl.FileIO
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.model.HttpRequest
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val route =
|
||||
(put & path("lines")) {
|
||||
withoutSizeLimit {
|
||||
extractRequest { r: HttpRequest =>
|
||||
val finishedWriting = r.discardEntityBytes().future
|
||||
|
||||
// we only want to respond once the incoming data has been handled:
|
||||
onComplete(finishedWriting) { done =>
|
||||
complete("Drained all data from connection... (" + done + ")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//#discard-discardEntityBytes
|
||||
}
|
||||
|
||||
"discard entity manually" in compileOnlySpec {
|
||||
//#discard-close-connections
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.scaladsl.Sink
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.model.headers.Connection
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future flatMap/onComplete in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val route =
|
||||
(put & path("lines")) {
|
||||
withoutSizeLimit {
|
||||
extractDataBytes { data =>
|
||||
// Closing connections, method 1 (eager):
|
||||
// we deem this request as illegal, and close the connection right away:
|
||||
data.runWith(Sink.cancelled) // "brutally" closes the connection
|
||||
|
||||
// Closing connections, method 2 (graceful):
|
||||
// consider draining connection and replying with `Connection: Close` header
|
||||
// if you want the client to close after this request/reply cycle instead:
|
||||
respondWithHeader(Connection("close"))
|
||||
complete(StatusCodes.Forbidden -> "Not allowed!")
|
||||
}
|
||||
}
|
||||
}
|
||||
//#discard-close-connections
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.actor.{ ActorLogging, ActorSystem }
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.util.ByteString
|
||||
import com.typesafe.sslconfig.akka.AkkaSSLConfig
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class HttpsExamplesSpec extends WordSpec with Matchers with CompileOnlySpec {
|
||||
|
||||
"disable SNI for connection" in compileOnlySpec {
|
||||
val unsafeHost = "example.com"
|
||||
//#disable-sni-connection
|
||||
implicit val system = ActorSystem()
|
||||
implicit val mat = ActorMaterializer()
|
||||
|
||||
// WARNING: disabling SNI is a very bad idea, please don't unless you have a very good reason to.
|
||||
val badSslConfig = AkkaSSLConfig().mapSettings(s => s.withLoose(s.loose.withDisableSNI(true)))
|
||||
val badCtx = Http().createClientHttpsContext(badSslConfig)
|
||||
Http().outgoingConnectionHttps(unsafeHost, connectionContext = badCtx)
|
||||
//#disable-sni-connection
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.testkit.AkkaSpec
|
||||
|
||||
class MarshalSpec extends AkkaSpec {
|
||||
|
||||
"use marshal" in {
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import akka.http.scaladsl.marshalling.Marshal
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
import system.dispatcher // ExecutionContext
|
||||
|
||||
val string = "Yeah"
|
||||
val entityFuture = Marshal(string).to[MessageEntity]
|
||||
val entity = Await.result(entityFuture, 1.second) // don't block in non-test code!
|
||||
entity.contentType shouldEqual ContentTypes.`text/plain(UTF-8)`
|
||||
|
||||
val errorMsg = "Easy, pal!"
|
||||
val responseFuture = Marshal(420 -> errorMsg).to[HttpResponse]
|
||||
val response = Await.result(responseFuture, 1.second) // don't block in non-test code!
|
||||
response.status shouldEqual StatusCodes.EnhanceYourCalm
|
||||
response.entity.contentType shouldEqual ContentTypes.`text/plain(UTF-8)`
|
||||
|
||||
val request = HttpRequest(headers = List(headers.Accept(MediaTypes.`application/json`)))
|
||||
val responseText = "Plaintext"
|
||||
val respFuture = Marshal(responseText).toResponseFor(request) // with content negotiation!
|
||||
a[Marshal.UnacceptableResponseContentTypeException] should be thrownBy {
|
||||
Await.result(respFuture, 1.second) // client requested JSON, we only have text/plain!
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
//#import-model
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
//#import-model
|
||||
|
||||
import akka.testkit.AkkaSpec
|
||||
import akka.util.ByteString
|
||||
import akka.http.scaladsl.model.headers.BasicHttpCredentials
|
||||
|
||||
class ModelSpec extends AkkaSpec {
|
||||
"construct request" in {
|
||||
//#construct-request
|
||||
import HttpMethods._
|
||||
|
||||
// construct a simple GET request to `homeUri`
|
||||
val homeUri = Uri("/abc")
|
||||
HttpRequest(GET, uri = homeUri)
|
||||
|
||||
// construct simple GET request to "/index" (implicit string to Uri conversion)
|
||||
HttpRequest(GET, uri = "/index")
|
||||
|
||||
// construct simple POST request containing entity
|
||||
val data = ByteString("abc")
|
||||
HttpRequest(POST, uri = "/receive", entity = data)
|
||||
|
||||
// customize every detail of HTTP request
|
||||
import HttpProtocols._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
val userData = ByteString("abc")
|
||||
val authorization = headers.Authorization(BasicHttpCredentials("user", "pass"))
|
||||
HttpRequest(
|
||||
PUT,
|
||||
uri = "/user",
|
||||
entity = HttpEntity(`text/plain` withCharset `UTF-8`, userData),
|
||||
headers = List(authorization),
|
||||
protocol = `HTTP/1.0`)
|
||||
//#construct-request
|
||||
}
|
||||
|
||||
"construct response" in {
|
||||
//#construct-response
|
||||
import StatusCodes._
|
||||
|
||||
// simple OK response without data created using the integer status code
|
||||
HttpResponse(200)
|
||||
|
||||
// 404 response created using the named StatusCode constant
|
||||
HttpResponse(NotFound)
|
||||
|
||||
// 404 response with a body explaining the error
|
||||
HttpResponse(404, entity = "Unfortunately, the resource couldn't be found.")
|
||||
|
||||
// A redirecting response containing an extra header
|
||||
val locationHeader = headers.Location("http://example.com/other")
|
||||
HttpResponse(Found, headers = List(locationHeader))
|
||||
|
||||
//#construct-response
|
||||
}
|
||||
|
||||
"deal with headers" in {
|
||||
//#headers
|
||||
import akka.http.scaladsl.model.headers._
|
||||
|
||||
// create a ``Location`` header
|
||||
val loc = Location("http://example.com/other")
|
||||
|
||||
// create an ``Authorization`` header with HTTP Basic authentication data
|
||||
val auth = Authorization(BasicHttpCredentials("joe", "josepp"))
|
||||
|
||||
// custom type
|
||||
case class User(name: String, pass: String)
|
||||
|
||||
// a method that extracts basic HTTP credentials from a request
|
||||
def credentialsOfRequest(req: HttpRequest): Option[User] =
|
||||
for {
|
||||
Authorization(BasicHttpCredentials(user, pass)) <- req.header[Authorization]
|
||||
} yield User(user, pass)
|
||||
//#headers
|
||||
|
||||
credentialsOfRequest(HttpRequest(headers = List(auth))) should be(Some(User("joe", "josepp")))
|
||||
credentialsOfRequest(HttpRequest()) should be(None)
|
||||
credentialsOfRequest(HttpRequest(headers = List(Authorization(GenericHttpCredentials("Other", Map.empty[String, String]))))) should be(None)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import akka.http.scaladsl.server.Directives
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class SprayJsonCompactMarshalSpec extends WordSpec with Matchers {
|
||||
|
||||
"spray-json example" in {
|
||||
//#example
|
||||
import spray.json._
|
||||
|
||||
// domain model
|
||||
final case class CompactPrintedItem(name: String, id: Long)
|
||||
|
||||
trait CompactJsonFormatSupport extends DefaultJsonProtocol with SprayJsonSupport {
|
||||
implicit val printer = CompactPrinter
|
||||
implicit val compactPrintedItemFormat = jsonFormat2(CompactPrintedItem)
|
||||
}
|
||||
|
||||
// use it wherever json (un)marshalling is needed
|
||||
class MyJsonService extends Directives with CompactJsonFormatSupport {
|
||||
|
||||
// format: OFF
|
||||
val route =
|
||||
get {
|
||||
pathSingleSlash {
|
||||
complete {
|
||||
// should complete with spray.json.JsValue = {"name":"akka","id":42}
|
||||
CompactPrintedItem("akka", 42) // will render as JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
// format: ON
|
||||
//#
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class SprayJsonExampleSpec extends WordSpec with Matchers {
|
||||
|
||||
def compileOnlySpec(body: => Unit) = ()
|
||||
|
||||
"spray-json example" in compileOnlySpec {
|
||||
//#minimal-spray-json-example
|
||||
import akka.http.scaladsl.server.Directives
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import spray.json._
|
||||
|
||||
// domain model
|
||||
final case class Item(name: String, id: Long)
|
||||
final case class Order(items: List[Item])
|
||||
|
||||
// collect your json format instances into a support trait:
|
||||
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
implicit val itemFormat = jsonFormat2(Item)
|
||||
implicit val orderFormat = jsonFormat1(Order) // contains List[Item]
|
||||
}
|
||||
|
||||
// use it wherever json (un)marshalling is needed
|
||||
class MyJsonService extends Directives with JsonSupport {
|
||||
|
||||
// format: OFF
|
||||
val route =
|
||||
get {
|
||||
pathSingleSlash {
|
||||
complete(Item("thing", 42)) // will render as JSON
|
||||
}
|
||||
} ~
|
||||
post {
|
||||
entity(as[Order]) { order => // will unmarshal JSON to Order
|
||||
val itemsCount = order.items.size
|
||||
val itemNames = order.items.map(_.name).mkString(", ")
|
||||
complete(s"Ordered $itemsCount items: $itemNames")
|
||||
}
|
||||
}
|
||||
// format: ON
|
||||
//#
|
||||
}
|
||||
}
|
||||
|
||||
"second-spray-json-example" in compileOnlySpec {
|
||||
//#second-spray-json-example
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.Done
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
|
||||
import scala.io.StdIn
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object WebServer {
|
||||
|
||||
// domain model
|
||||
final case class Item(name: String, id: Long)
|
||||
final case class Order(items: List[Item])
|
||||
|
||||
// formats for unmarshalling and marshalling
|
||||
implicit val itemFormat = jsonFormat2(Item)
|
||||
implicit val orderFormat = jsonFormat1(Order)
|
||||
|
||||
// (fake) async database query api
|
||||
def fetchItem(itemId: Long): Future[Option[Item]] = ???
|
||||
def saveOrder(order: Order): Future[Done] = ???
|
||||
|
||||
def main(args: Array[String]) {
|
||||
|
||||
// needed to run the route
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
// needed for the future map/flatmap in the end
|
||||
implicit val executionContext = system.dispatcher
|
||||
|
||||
val route: Route =
|
||||
get {
|
||||
pathPrefix("item" / LongNumber) { id =>
|
||||
// there might be no item for a given id
|
||||
val maybeItem: Future[Option[Item]] = fetchItem(id)
|
||||
|
||||
onSuccess(maybeItem) {
|
||||
case Some(item) => complete(item)
|
||||
case None => complete(StatusCodes.NotFound)
|
||||
}
|
||||
}
|
||||
} ~
|
||||
post {
|
||||
path("create-order") {
|
||||
entity(as[Order]) { order =>
|
||||
val saved: Future[Done] = saveOrder(order)
|
||||
onComplete(saved) { done =>
|
||||
complete("order created")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
StdIn.readLine() // let it run until user presses return
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ ⇒ system.terminate()) // and shutdown when done
|
||||
|
||||
}
|
||||
}
|
||||
//#second-spray-json-example
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.stream.{ Materializer, ActorMaterializer }
|
||||
import akka.testkit.AkkaSpec
|
||||
|
||||
class UnmarshalSpec extends AkkaSpec {
|
||||
|
||||
"use unmarshal" in {
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import system.dispatcher // ExecutionContext
|
||||
implicit val materializer: Materializer = ActorMaterializer()
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
|
||||
val intFuture = Unmarshal("42").to[Int]
|
||||
val int = Await.result(intFuture, 1.second) // don't block in non-test code!
|
||||
int shouldEqual 42
|
||||
|
||||
val boolFuture = Unmarshal("off").to[Boolean]
|
||||
val bool = Await.result(boolFuture, 1.second) // don't block in non-test code!
|
||||
bool shouldBe false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package docs.http.scaladsl
|
||||
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class WebSocketClientExampleSpec extends WordSpec with Matchers with CompileOnlySpec {
|
||||
|
||||
"singleWebSocket-request-example" in compileOnlySpec {
|
||||
//#single-WebSocket-request
|
||||
import akka.actor.ActorSystem
|
||||
import akka.{ Done, NotUsed }
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.ws._
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object SingleWebSocketRequest {
|
||||
def main(args: Array[String]) = {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
// print each incoming strict text message
|
||||
val printSink: Sink[Message, Future[Done]] =
|
||||
Sink.foreach {
|
||||
case message: TextMessage.Strict =>
|
||||
println(message.text)
|
||||
}
|
||||
|
||||
val helloSource: Source[Message, NotUsed] =
|
||||
Source.single(TextMessage("hello world!"))
|
||||
|
||||
// the Future[Done] is the materialized value of Sink.foreach
|
||||
// and it is completed when the stream completes
|
||||
val flow: Flow[Message, Message, Future[Done]] =
|
||||
Flow.fromSinkAndSourceMat(printSink, helloSource)(Keep.left)
|
||||
|
||||
// upgradeResponse is a Future[WebSocketUpgradeResponse] that
|
||||
// completes or fails when the connection succeeds or fails
|
||||
// and closed is a Future[Done] representing the stream completion from above
|
||||
val (upgradeResponse, closed) =
|
||||
Http().singleWebSocketRequest(WebSocketRequest("ws://echo.websocket.org"), flow)
|
||||
|
||||
val connected = upgradeResponse.map { upgrade =>
|
||||
// just like a regular http request we can access response status which is available via upgrade.response.status
|
||||
// status code 101 (Switching Protocols) indicates that server support WebSockets
|
||||
if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
|
||||
Done
|
||||
} else {
|
||||
throw new RuntimeException(s"Connection failed: ${upgrade.response.status}")
|
||||
}
|
||||
}
|
||||
|
||||
// in a real application you would not side effect here
|
||||
// and handle errors more carefully
|
||||
connected.onComplete(println)
|
||||
closed.foreach(_ => println("closed"))
|
||||
}
|
||||
}
|
||||
//#single-WebSocket-request
|
||||
}
|
||||
|
||||
"half-closed-WebSocket-closing-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.{ Done, NotUsed }
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model.ws._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
//#half-closed-WebSocket-closing-example
|
||||
|
||||
// we may expect to be able to to just tail
|
||||
// the server websocket output like this
|
||||
val flow: Flow[Message, Message, NotUsed] =
|
||||
Flow.fromSinkAndSource(
|
||||
Sink.foreach(println),
|
||||
Source.empty)
|
||||
|
||||
Http().singleWebSocketRequest(
|
||||
WebSocketRequest("ws://example.com:8080/some/path"),
|
||||
flow)
|
||||
|
||||
//#half-closed-WebSocket-closing-example
|
||||
}
|
||||
|
||||
"half-closed-WebSocket-working-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model.ws._
|
||||
|
||||
import scala.concurrent.Promise
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
//#half-closed-WebSocket-working-example
|
||||
|
||||
// using Source.maybe materializes into a promise
|
||||
// which will allow us to complete the source later
|
||||
val flow: Flow[Message, Message, Promise[Option[Message]]] =
|
||||
Flow.fromSinkAndSourceMat(
|
||||
Sink.foreach[Message](println),
|
||||
Source.maybe[Message])(Keep.right)
|
||||
|
||||
val (upgradeResponse, promise) =
|
||||
Http().singleWebSocketRequest(
|
||||
WebSocketRequest("ws://example.com:8080/some/path"),
|
||||
flow)
|
||||
|
||||
// at some later time we want to disconnect
|
||||
promise.success(None)
|
||||
//#half-closed-WebSocket-working-example
|
||||
}
|
||||
|
||||
"half-closed-WebSocket-finite-working-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.{ Done, NotUsed }
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model.ws._
|
||||
|
||||
import scala.concurrent.Promise
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
//#half-closed-WebSocket-finite-working-example
|
||||
|
||||
// using emit "one" and "two" and then keep the connection open
|
||||
val flow: Flow[Message, Message, Promise[Option[Message]]] =
|
||||
Flow.fromSinkAndSourceMat(
|
||||
Sink.foreach[Message](println),
|
||||
Source(List(TextMessage("one"), TextMessage("two")))
|
||||
.concatMat(Source.maybe[Message])(Keep.right))(Keep.right)
|
||||
|
||||
val (upgradeResponse, promise) =
|
||||
Http().singleWebSocketRequest(
|
||||
WebSocketRequest("ws://example.com:8080/some/path"),
|
||||
flow)
|
||||
|
||||
// at some later time we want to disconnect
|
||||
promise.success(None)
|
||||
//#half-closed-WebSocket-finite-working-example
|
||||
}
|
||||
|
||||
"authorized-singleWebSocket-request-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.NotUsed
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model.headers.{ Authorization, BasicHttpCredentials }
|
||||
import akka.http.scaladsl.model.ws._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import collection.immutable.Seq
|
||||
|
||||
val flow: Flow[Message, Message, NotUsed] = ???
|
||||
|
||||
//#authorized-single-WebSocket-request
|
||||
val (upgradeResponse, _) =
|
||||
Http().singleWebSocketRequest(
|
||||
WebSocketRequest(
|
||||
"ws://example.com:8080/some/path",
|
||||
extraHeaders = Seq(Authorization(
|
||||
BasicHttpCredentials("johan", "correcthorsebatterystaple")))),
|
||||
flow)
|
||||
//#authorized-single-WebSocket-request
|
||||
}
|
||||
|
||||
"WebSocketClient-flow-example" in compileOnlySpec {
|
||||
//#WebSocket-client-flow
|
||||
import akka.actor.ActorSystem
|
||||
import akka.Done
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.ws._
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object WebSocketClientFlow {
|
||||
def main(args: Array[String]) = {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
// Future[Done] is the materialized value of Sink.foreach,
|
||||
// emitted when the stream completes
|
||||
val incoming: Sink[Message, Future[Done]] =
|
||||
Sink.foreach[Message] {
|
||||
case message: TextMessage.Strict =>
|
||||
println(message.text)
|
||||
}
|
||||
|
||||
// send this as a message over the WebSocket
|
||||
val outgoing = Source.single(TextMessage("hello world!"))
|
||||
|
||||
// flow to use (note: not re-usable!)
|
||||
val webSocketFlow = Http().webSocketClientFlow(WebSocketRequest("ws://echo.websocket.org"))
|
||||
|
||||
// the materialized value is a tuple with
|
||||
// upgradeResponse is a Future[WebSocketUpgradeResponse] that
|
||||
// completes or fails when the connection succeeds or fails
|
||||
// and closed is a Future[Done] with the stream completion from the incoming sink
|
||||
val (upgradeResponse, closed) =
|
||||
outgoing
|
||||
.viaMat(webSocketFlow)(Keep.right) // keep the materialized Future[WebSocketUpgradeResponse]
|
||||
.toMat(incoming)(Keep.both) // also keep the Future[Done]
|
||||
.run()
|
||||
|
||||
// just like a regular http request we can access response status which is available via upgrade.response.status
|
||||
// status code 101 (Switching Protocols) indicates that server support WebSockets
|
||||
val connected = upgradeResponse.flatMap { upgrade =>
|
||||
if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
|
||||
Future.successful(Done)
|
||||
} else {
|
||||
throw new RuntimeException(s"Connection failed: ${upgrade.response.status}")
|
||||
}
|
||||
}
|
||||
|
||||
// in a real application you would not side effect here
|
||||
connected.onComplete(println)
|
||||
closed.foreach(_ => println("closed"))
|
||||
}
|
||||
}
|
||||
//#WebSocket-client-flow
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.server.{ Directives, Route }
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.WordSpec
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class BlockingInHttpExamplesSpec extends WordSpec with CompileOnlySpec
|
||||
with Directives {
|
||||
|
||||
compileOnlySpec {
|
||||
val system: ActorSystem = ???
|
||||
|
||||
//#blocking-example-in-default-dispatcher
|
||||
// BAD (due to blocking in Future, on default dispatcher)
|
||||
implicit val defaultDispatcher = system.dispatcher
|
||||
|
||||
val routes: Route = post {
|
||||
complete {
|
||||
Future { // uses defaultDispatcher
|
||||
Thread.sleep(5000) // will block on default dispatcher,
|
||||
System.currentTimeMillis().toString // Starving the routing infrastructure
|
||||
}
|
||||
}
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
compileOnlySpec {
|
||||
val system: ActorSystem = ???
|
||||
|
||||
//#blocking-example-in-dedicated-dispatcher
|
||||
// GOOD (the blocking is now isolated onto a dedicated dispatcher):
|
||||
implicit val blockingDispatcher = system.dispatchers.lookup("my-blocking-dispatcher")
|
||||
|
||||
val routes: Route = post {
|
||||
complete {
|
||||
Future { // uses the good "blocking dispatcher" that we configured,
|
||||
// instead of the default dispatcher- the blocking is isolated.
|
||||
Thread.sleep(5000)
|
||||
System.currentTimeMillis().toString
|
||||
}
|
||||
}
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
|
||||
|
||||
/*
|
||||
import org.scalatest.Inside
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
class CaseClassExtractionExamplesSpec extends RoutingSpec with Inside {
|
||||
// FIXME: investigate why it doesn't work without this import
|
||||
import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet
|
||||
|
||||
// format: OFF
|
||||
|
||||
"example-1" in {
|
||||
case class Color(red: Int, green: Int, blue: Int)
|
||||
|
||||
val route =
|
||||
path("color") {
|
||||
parameters('red.as[Int], 'green.as[Int], 'blue.as[Int]) { (red, green, blue) =>
|
||||
val color = Color(red, green, blue)
|
||||
// ... route working with the `color` instance
|
||||
null // hide
|
||||
}
|
||||
}
|
||||
Get("/color?red=1&green=2&blue=3") ~> route ~> check { responseAs[String] shouldEqual "Color(1,2,3)" } // hide
|
||||
}
|
||||
|
||||
"example-2" in {
|
||||
case class Color(red: Int, green: Int, blue: Int)
|
||||
|
||||
val route =
|
||||
path("color") {
|
||||
parameters('red.as[Int], 'green.as[Int], 'blue.as[Int]).as(Color) { color =>
|
||||
// ... route working with the `color` instance
|
||||
null // hide
|
||||
}
|
||||
}
|
||||
Get("/color?red=1&green=2&blue=3") ~> route ~> check { responseAs[String] shouldEqual "Color(1,2,3)" } // hide
|
||||
}
|
||||
|
||||
"example-3" in {
|
||||
case class Color(name: String, red: Int, green: Int, blue: Int)
|
||||
|
||||
val route =
|
||||
(path("color" / Segment) & parameters('r.as[Int], 'g.as[Int], 'b.as[Int]))
|
||||
.as(Color) { color =>
|
||||
// ... route working with the `color` instance
|
||||
null // hide
|
||||
}
|
||||
Get("/color/abc?r=1&g=2&b=3") ~> route ~> check { responseAs[String] shouldEqual "Color(abc,1,2,3)" } // hide
|
||||
}
|
||||
|
||||
//# example-4
|
||||
case class Color(name: String, red: Int, green: Int, blue: Int) {
|
||||
require(!name.isEmpty, "color name must not be empty")
|
||||
require(0 <= red && red <= 255, "red color component must be between 0 and 255")
|
||||
require(0 <= green && green <= 255, "green color component must be between 0 and 255")
|
||||
require(0 <= blue && blue <= 255, "blue color component must be between 0 and 255")
|
||||
}
|
||||
//#
|
||||
|
||||
"example 4 test" in {
|
||||
val route =
|
||||
(path("color" / Segment) &
|
||||
parameters('r.as[Int], 'g.as[Int], 'b.as[Int])).as(Color) { color =>
|
||||
doSomethingWith(color) // route working with the Color instance
|
||||
}
|
||||
Get("/color/abc?r=1&g=2&b=3") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Color(abc,1,2,3)"
|
||||
}
|
||||
Get("/color/abc?r=1&g=2&b=345") ~> route ~> check {
|
||||
inside(rejection) {
|
||||
case ValidationRejection("requirement failed: blue color component must be between 0 and 255", _) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def doSomethingWith(x: Any) = complete(x.toString)
|
||||
}*/
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
|
||||
class DirectiveExamplesSpec extends RoutingSpec {
|
||||
|
||||
// format: OFF
|
||||
|
||||
"example-1" in {
|
||||
val route: Route =
|
||||
path("order" / IntNumber) { id =>
|
||||
get {
|
||||
complete {
|
||||
"Received GET request for order " + id
|
||||
}
|
||||
} ~
|
||||
put {
|
||||
complete {
|
||||
"Received PUT request for order " + id
|
||||
}
|
||||
}
|
||||
}
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
"example-2" in {
|
||||
def innerRoute(id: Int): Route =
|
||||
get {
|
||||
complete {
|
||||
"Received GET request for order " + id
|
||||
}
|
||||
} ~
|
||||
put {
|
||||
complete {
|
||||
"Received PUT request for order " + id
|
||||
}
|
||||
}
|
||||
|
||||
val route: Route = path("order" / IntNumber) { id => innerRoute(id) }
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
"example-3" in {
|
||||
val route =
|
||||
path("order" / IntNumber) { id =>
|
||||
(get | put) { ctx =>
|
||||
ctx.complete(s"Received ${ctx.request.method.name} request for order $id")
|
||||
}
|
||||
}
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
"example-4" in {
|
||||
val route =
|
||||
path("order" / IntNumber) { id =>
|
||||
(get | put) {
|
||||
extractMethod { m =>
|
||||
complete(s"Received ${m.name} request for order $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
"example-5" in {
|
||||
val getOrPut = get | put
|
||||
val route =
|
||||
path("order" / IntNumber) { id =>
|
||||
getOrPut {
|
||||
extractMethod { m =>
|
||||
complete(s"Received ${m.name} request for order $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
"example-6" in {
|
||||
val getOrPut = get | put
|
||||
val route =
|
||||
(path("order" / IntNumber) & getOrPut & extractMethod) { (id, m) =>
|
||||
complete(s"Received ${m.name} request for order $id")
|
||||
}
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
"example-7" in {
|
||||
val orderGetOrPutWithMethod =
|
||||
path("order" / IntNumber) & (get | put) & extractMethod
|
||||
val route =
|
||||
orderGetOrPutWithMethod { (id, m) =>
|
||||
complete(s"Received ${m.name} request for order $id")
|
||||
}
|
||||
verify(route) // hide
|
||||
}
|
||||
|
||||
def verify(route: Route) = {
|
||||
Get("/order/42") ~> route ~> check { responseAs[String] shouldEqual "Received GET request for order 42" }
|
||||
Put("/order/42") ~> route ~> check { responseAs[String] shouldEqual "Received PUT request for order 42" }
|
||||
Get("/") ~> route ~> check { handled shouldEqual false }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
// format: OFF
|
||||
|
||||
object MyExplicitExceptionHandler {
|
||||
|
||||
//#explicit-handler-example
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server._
|
||||
import StatusCodes._
|
||||
import Directives._
|
||||
|
||||
val myExceptionHandler = ExceptionHandler {
|
||||
case _: ArithmeticException =>
|
||||
extractUri { uri =>
|
||||
println(s"Request to $uri could not be handled normally")
|
||||
complete(HttpResponse(InternalServerError, entity = "Bad numbers, bad result!!!"))
|
||||
}
|
||||
}
|
||||
|
||||
object MyApp extends App {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val route: Route =
|
||||
handleExceptions(myExceptionHandler) {
|
||||
// ... some route structure
|
||||
null // hide
|
||||
}
|
||||
|
||||
Http().bindAndHandle(route, "localhost", 8080)
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
object MyImplicitExceptionHandler {
|
||||
|
||||
//#implicit-handler-example
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server._
|
||||
import StatusCodes._
|
||||
import Directives._
|
||||
|
||||
implicit def myExceptionHandler: ExceptionHandler =
|
||||
ExceptionHandler {
|
||||
case _: ArithmeticException =>
|
||||
extractUri { uri =>
|
||||
println(s"Request to $uri could not be handled normally")
|
||||
complete(HttpResponse(InternalServerError, entity = "Bad numbers, bad result!!!"))
|
||||
}
|
||||
}
|
||||
|
||||
object MyApp extends App {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val route: Route =
|
||||
// ... some route structure
|
||||
null // hide
|
||||
|
||||
Http().bindAndHandle(route, "localhost", 8080)
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
class ExceptionHandlerExamplesSpec extends RoutingSpec {
|
||||
|
||||
"test explicit example" in {
|
||||
// tests:
|
||||
Get() ~> handleExceptions(MyExplicitExceptionHandler.myExceptionHandler) {
|
||||
_.complete((1 / 0).toString)
|
||||
} ~> check {
|
||||
responseAs[String] === "Bad numbers, bad result!!!"
|
||||
}
|
||||
}
|
||||
|
||||
"test implicit example" in {
|
||||
import akka.http.scaladsl.server._
|
||||
import MyImplicitExceptionHandler.myExceptionHandler
|
||||
// tests:
|
||||
Get() ~> Route.seal(ctx => ctx.complete((1 / 0).toString)) ~> check {
|
||||
responseAs[String] === "Bad numbers, bad result!!!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import java.io.File
|
||||
|
||||
import akka.Done
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.model.Multipart.FormData.BodyPart
|
||||
import akka.stream.scaladsl.Framing
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model.Multipart
|
||||
import akka.util.ByteString
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.Future
|
||||
|
||||
class FileUploadExamplesSpec extends RoutingSpec {
|
||||
|
||||
case class Video(file: File, title: String, author: String)
|
||||
object db {
|
||||
def create(video: Video): Future[Unit] = Future.successful(Unit)
|
||||
}
|
||||
|
||||
"simple-upload" in {
|
||||
val uploadVideo =
|
||||
path("video") {
|
||||
entity(as[Multipart.FormData]) { formData =>
|
||||
|
||||
// collect all parts of the multipart as it arrives into a map
|
||||
val allPartsF: Future[Map[String, Any]] = formData.parts.mapAsync[(String, Any)](1) {
|
||||
|
||||
case b: BodyPart if b.name == "file" =>
|
||||
// stream into a file as the chunks of it arrives and return a future
|
||||
// file to where it got stored
|
||||
val file = File.createTempFile("upload", "tmp")
|
||||
b.entity.dataBytes.runWith(FileIO.toPath(file.toPath)).map(_ =>
|
||||
(b.name -> file))
|
||||
|
||||
case b: BodyPart =>
|
||||
// collect form field values
|
||||
b.toStrict(2.seconds).map(strict =>
|
||||
(b.name -> strict.entity.data.utf8String))
|
||||
|
||||
}.runFold(Map.empty[String, Any])((map, tuple) => map + tuple)
|
||||
|
||||
val done = allPartsF.map { allParts =>
|
||||
// You would have some better validation/unmarshalling here
|
||||
db.create(Video(
|
||||
file = allParts("file").asInstanceOf[File],
|
||||
title = allParts("title").asInstanceOf[String],
|
||||
author = allParts("author").asInstanceOf[String]))
|
||||
}
|
||||
|
||||
// when processing have finished create a response for the user
|
||||
onSuccess(allPartsF) { allParts =>
|
||||
complete {
|
||||
"ok!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MetadataActor {
|
||||
case class Entry(id: Long, values: Seq[String])
|
||||
}
|
||||
val metadataActor: ActorRef = system.deadLetters
|
||||
|
||||
"stream-csv-upload" in {
|
||||
val splitLines = Framing.delimiter(ByteString("\n"), 256)
|
||||
|
||||
val csvUploads =
|
||||
path("metadata" / LongNumber) { id =>
|
||||
entity(as[Multipart.FormData]) { formData =>
|
||||
val done: Future[Done] = formData.parts.mapAsync(1) {
|
||||
case b: BodyPart if b.filename.exists(_.endsWith(".csv")) =>
|
||||
b.entity.dataBytes
|
||||
.via(splitLines)
|
||||
.map(_.utf8String.split(",").toVector)
|
||||
.runForeach(csv =>
|
||||
metadataActor ! MetadataActor.Entry(id, csv))
|
||||
case _ => Future.successful(Done)
|
||||
}.runWith(Sink.ignore)
|
||||
|
||||
// when processing have finished create a response for the user
|
||||
onSuccess(done) { _ =>
|
||||
complete {
|
||||
"ok!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
// format: OFF
|
||||
|
||||
//# source-quote
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.server._
|
||||
import Directives._
|
||||
|
||||
class FullTestKitExampleSpec extends WordSpec with Matchers with ScalatestRouteTest {
|
||||
|
||||
val smallRoute =
|
||||
get {
|
||||
pathSingleSlash {
|
||||
complete {
|
||||
"Captain on the bridge!"
|
||||
}
|
||||
} ~
|
||||
path("ping") {
|
||||
complete("PONG!")
|
||||
}
|
||||
}
|
||||
|
||||
"The service" should {
|
||||
|
||||
"return a greeting for GET requests to the root path" in {
|
||||
// tests:
|
||||
Get() ~> smallRoute ~> check {
|
||||
responseAs[String] shouldEqual "Captain on the bridge!"
|
||||
}
|
||||
}
|
||||
|
||||
"return a 'PONG!' response for GET requests to /ping" in {
|
||||
// tests:
|
||||
Get("/ping") ~> smallRoute ~> check {
|
||||
responseAs[String] shouldEqual "PONG!"
|
||||
}
|
||||
}
|
||||
|
||||
"leave GET requests to other paths unhandled" in {
|
||||
// tests:
|
||||
Get("/kermit") ~> smallRoute ~> check {
|
||||
handled shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"return a MethodNotAllowed error for PUT requests to the root path" in {
|
||||
// tests:
|
||||
Put() ~> Route.seal(smallRoute) ~> check {
|
||||
status === StatusCodes.MethodNotAllowed
|
||||
responseAs[String] shouldEqual "HTTP method not allowed, supported methods: GET"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//#
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
//#imports
|
||||
import java.io.InputStream
|
||||
import java.security.{ SecureRandom, KeyStore }
|
||||
import javax.net.ssl.{ SSLContext, TrustManagerFactory, KeyManagerFactory }
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.server.{ RouteResult, Route, Directives }
|
||||
import akka.http.scaladsl.{ ConnectionContext, HttpsConnectionContext, Http }
|
||||
import akka.stream.ActorMaterializer
|
||||
import com.typesafe.sslconfig.akka.AkkaSSLConfig
|
||||
//#
|
||||
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
abstract class HttpsServerExampleSpec extends WordSpec with Matchers
|
||||
with Directives with CompileOnlySpec {
|
||||
|
||||
class HowToObtainSSLConfig {
|
||||
//#akka-ssl-config
|
||||
implicit val system = ActorSystem()
|
||||
val sslConfig = AkkaSSLConfig()
|
||||
//#
|
||||
}
|
||||
|
||||
"low level api" in compileOnlySpec {
|
||||
//#low-level-default
|
||||
implicit val system = ActorSystem()
|
||||
implicit val mat = ActorMaterializer()
|
||||
implicit val dispatcher = system.dispatcher
|
||||
|
||||
// Manual HTTPS configuration
|
||||
|
||||
val password: Array[Char] = ??? // do not store passwords in code, read them from somewhere safe!
|
||||
|
||||
val ks: KeyStore = KeyStore.getInstance("PKCS12")
|
||||
val keystore: InputStream = getClass.getClassLoader.getResourceAsStream("server.p12")
|
||||
|
||||
require(keystore != null, "Keystore required!")
|
||||
ks.load(keystore, password)
|
||||
|
||||
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
|
||||
keyManagerFactory.init(ks, password)
|
||||
|
||||
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
|
||||
tmf.init(ks)
|
||||
|
||||
val sslContext: SSLContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
|
||||
val https: HttpsConnectionContext = ConnectionContext.https(sslContext)
|
||||
//#
|
||||
|
||||
//#both-https-and-http
|
||||
// you can run both HTTP and HTTPS in the same application as follows:
|
||||
val commonRoutes: Route = get { complete("Hello world!") }
|
||||
Http().bindAndHandle(commonRoutes, "127.0.0.1", 443, connectionContext = https)
|
||||
Http().bindAndHandle(commonRoutes, "127.0.0.1", 80)
|
||||
//#
|
||||
|
||||
//#bind-low-level-context
|
||||
Http().bind("127.0.0.1", connectionContext = https)
|
||||
|
||||
// or using the high level routing DSL:
|
||||
val routes: Route = get { complete("Hello world!") }
|
||||
Http().bindAndHandle(routes, "127.0.0.1", 8080, connectionContext = https)
|
||||
//#
|
||||
|
||||
//#set-low-level-context-default
|
||||
// sets default context to HTTPS – all Http() bound servers for this ActorSystem will use HTTPS from now on
|
||||
Http().setDefaultServerHttpContext(https)
|
||||
Http().bindAndHandle(routes, "127.0.0.1", 9090, connectionContext = https)
|
||||
//#
|
||||
|
||||
system.terminate()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
// format: OFF
|
||||
|
||||
object MyRejectionHandler {
|
||||
|
||||
//#custom-handler-example
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server._
|
||||
import StatusCodes._
|
||||
import Directives._
|
||||
|
||||
implicit def myRejectionHandler =
|
||||
RejectionHandler.newBuilder()
|
||||
.handle { case MissingCookieRejection(cookieName) =>
|
||||
complete(HttpResponse(BadRequest, entity = "No cookies, no service!!!"))
|
||||
}
|
||||
.handle { case AuthorizationFailedRejection =>
|
||||
complete((Forbidden, "You're out of your depth!"))
|
||||
}
|
||||
.handle { case ValidationRejection(msg, _) =>
|
||||
complete((InternalServerError, "That wasn't valid! " + msg))
|
||||
}
|
||||
.handleAll[MethodRejection] { methodRejections =>
|
||||
val names = methodRejections.map(_.supported.name)
|
||||
complete((MethodNotAllowed, s"Can't do that! Supported: ${names mkString " or "}!"))
|
||||
}
|
||||
.handleNotFound { complete((NotFound, "Not here!")) }
|
||||
.result()
|
||||
|
||||
object MyApp extends App {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val route: Route =
|
||||
// ... some route structure
|
||||
null // hide
|
||||
|
||||
Http().bindAndHandle(route, "localhost", 8080)
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
class RejectionHandlerExamplesSpec extends RoutingSpec {
|
||||
import MyRejectionHandler._
|
||||
|
||||
"example-1" in {
|
||||
import akka.http.scaladsl.coding.Gzip
|
||||
|
||||
val route =
|
||||
path("order") {
|
||||
get {
|
||||
complete("Received GET")
|
||||
} ~
|
||||
post {
|
||||
decodeRequestWith(Gzip) {
|
||||
complete("Received compressed POST")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"test custom handler example" in {
|
||||
import akka.http.scaladsl.server._
|
||||
val route = Route.seal(reject(MissingCookieRejection("abc")))
|
||||
|
||||
// tests:
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] === "No cookies, no service!!!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.server.Directives
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
abstract class RoutingSpec extends WordSpec with Matchers
|
||||
with Directives with ScalatestRouteTest
|
||||
with CompileOnlySpec
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.model.ws.BinaryMessage
|
||||
import akka.stream.scaladsl.Sink
|
||||
import docs.CompileOnlySpec
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class WebSocketExampleSpec extends WordSpec with Matchers with CompileOnlySpec {
|
||||
"core-example" in compileOnlySpec {
|
||||
//#websocket-example-using-core
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.{ Source, Flow }
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.ws.UpgradeToWebSocket
|
||||
import akka.http.scaladsl.model.ws.{ TextMessage, Message }
|
||||
import akka.http.scaladsl.model.{ HttpResponse, Uri, HttpRequest }
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
//#websocket-handler
|
||||
// The Greeter WebSocket Service expects a "name" per message and
|
||||
// returns a greeting message for that name
|
||||
val greeterWebSocketService =
|
||||
Flow[Message]
|
||||
.mapConcat {
|
||||
// we match but don't actually consume the text message here,
|
||||
// rather we simply stream it back as the tail of the response
|
||||
// this means we might start sending the response even before the
|
||||
// end of the incoming message has been received
|
||||
case tm: TextMessage => TextMessage(Source.single("Hello ") ++ tm.textStream) :: Nil
|
||||
case bm: BinaryMessage =>
|
||||
// ignore binary messages but drain content to avoid the stream being clogged
|
||||
bm.dataStream.runWith(Sink.ignore)
|
||||
Nil
|
||||
}
|
||||
//#websocket-handler
|
||||
|
||||
//#websocket-request-handling
|
||||
val requestHandler: HttpRequest => HttpResponse = {
|
||||
case req @ HttpRequest(GET, Uri.Path("/greeter"), _, _, _) =>
|
||||
req.header[UpgradeToWebSocket] match {
|
||||
case Some(upgrade) => upgrade.handleMessages(greeterWebSocketService)
|
||||
case None => HttpResponse(400, entity = "Not a valid websocket request!")
|
||||
}
|
||||
case r: HttpRequest =>
|
||||
r.discardEntityBytes() // important to drain incoming HTTP Entity stream
|
||||
HttpResponse(404, entity = "Unknown resource!")
|
||||
}
|
||||
//#websocket-request-handling
|
||||
|
||||
val bindingFuture =
|
||||
Http().bindAndHandleSync(requestHandler, interface = "localhost", port = 8080)
|
||||
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
Console.readLine()
|
||||
|
||||
import system.dispatcher // for the future transformations
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
}
|
||||
"routing-example" in compileOnlySpec {
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.{ Source, Flow }
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.ws.{ TextMessage, Message }
|
||||
import akka.http.scaladsl.server.Directives
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
import Directives._
|
||||
|
||||
// The Greeter WebSocket Service expects a "name" per message and
|
||||
// returns a greeting message for that name
|
||||
val greeterWebSocketService =
|
||||
Flow[Message]
|
||||
.collect {
|
||||
case tm: TextMessage => TextMessage(Source.single("Hello ") ++ tm.textStream)
|
||||
// ignore binary messages
|
||||
// TODO #20096 in case a Streamed message comes in, we should runWith(Sink.ignore) its data
|
||||
}
|
||||
|
||||
//#websocket-routing
|
||||
val route =
|
||||
path("greeter") {
|
||||
get {
|
||||
handleWebSocketMessages(greeterWebSocketService)
|
||||
}
|
||||
}
|
||||
//#websocket-routing
|
||||
|
||||
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
|
||||
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
Console.readLine()
|
||||
|
||||
import system.dispatcher // for the future transformations
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ => system.terminate()) // and shutdown when done
|
||||
}
|
||||
}
|
||||
|
|
@ -1,878 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.event.Logging
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.{ Server, RawHeader }
|
||||
import akka.http.scaladsl.server.RouteResult.{ Complete, Rejected }
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.settings.RoutingSettings
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.{ FileIO, Sink, Source }
|
||||
import akka.util.ByteString
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
class BasicDirectivesExamplesSpec extends RoutingSpec {
|
||||
"0extract" in {
|
||||
//#0extract
|
||||
val uriLength = extract(_.request.uri.toString.length)
|
||||
val route =
|
||||
uriLength { len =>
|
||||
complete(s"The length of the request URI is $len")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/abcdef") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The length of the request URI is 25"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"0extractLog" in {
|
||||
//#0extractLog
|
||||
val route =
|
||||
extractLog { log =>
|
||||
log.debug("I'm logging things in much detail..!")
|
||||
complete("It's amazing!")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/abcdef") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "It's amazing!"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"withMaterializer-0" in {
|
||||
//#withMaterializer-0
|
||||
val special = ActorMaterializer(namePrefix = Some("special"))
|
||||
|
||||
def sample() =
|
||||
path("sample") {
|
||||
extractMaterializer { mat =>
|
||||
complete {
|
||||
// explicitly use the materializer:
|
||||
Source.single(s"Materialized by ${mat.##}!")
|
||||
.runWith(Sink.head)(mat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
pathPrefix("special") {
|
||||
withMaterializer(special) {
|
||||
sample() // `special` materializer will be used
|
||||
}
|
||||
} ~ sample() // default materializer will be used
|
||||
|
||||
// tests:
|
||||
Get("/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Materialized by ${materializer.##}!"
|
||||
}
|
||||
Get("/special/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Materialized by ${special.##}!"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractMaterializer-0" in {
|
||||
//#extractMaterializer-0
|
||||
val route =
|
||||
path("sample") {
|
||||
extractMaterializer { materializer =>
|
||||
complete {
|
||||
// explicitly use the `materializer`:
|
||||
Source.single(s"Materialized by ${materializer.##}!")
|
||||
.runWith(Sink.head)(materializer)
|
||||
}
|
||||
}
|
||||
} // default materializer will be used
|
||||
|
||||
// tests:
|
||||
Get("/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Materialized by ${materializer.##}!"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"withExecutionContext-0" in compileOnlySpec {
|
||||
//#withExecutionContext-0
|
||||
val special = system.dispatchers.lookup("special")
|
||||
|
||||
def sample() =
|
||||
path("sample") {
|
||||
extractExecutionContext { implicit executor =>
|
||||
complete {
|
||||
Future(s"Run on ${executor.##}!") // uses the `executor` ExecutionContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
pathPrefix("special") {
|
||||
withExecutionContext(special) {
|
||||
sample() // `special` execution context will be used
|
||||
}
|
||||
} ~ sample() // default execution context will be used
|
||||
|
||||
// tests:
|
||||
Get("/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Run on ${system.dispatcher.##}!"
|
||||
}
|
||||
Get("/special/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Run on ${special.##}!"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractExecutionContext-0" in compileOnlySpec {
|
||||
//#extractExecutionContext-0
|
||||
def sample() =
|
||||
path("sample") {
|
||||
extractExecutionContext { implicit executor =>
|
||||
complete {
|
||||
Future(s"Run on ${executor.##}!") // uses the `executor` ExecutionContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
pathPrefix("special") {
|
||||
sample() // default execution context will be used
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Run on ${system.dispatcher.##}!"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"0withLog" in {
|
||||
//#0withLog
|
||||
val special = Logging(system, "SpecialRoutes")
|
||||
|
||||
def sample() =
|
||||
path("sample") {
|
||||
extractLog { implicit log =>
|
||||
complete {
|
||||
val msg = s"Logging using $log!"
|
||||
log.debug(msg)
|
||||
msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
pathPrefix("special") {
|
||||
withLog(special) {
|
||||
sample() // `special` logging adapter will be used
|
||||
}
|
||||
} ~ sample() // default logging adapter will be used
|
||||
|
||||
// tests:
|
||||
Get("/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Logging using ${system.log}!"
|
||||
}
|
||||
Get("/special/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Logging using $special!"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"withSettings-0" in compileOnlySpec {
|
||||
//#withSettings-0
|
||||
val special = RoutingSettings(system).withFileIODispatcher("special-io-dispatcher")
|
||||
|
||||
def sample() =
|
||||
path("sample") {
|
||||
complete {
|
||||
// internally uses the configured fileIODispatcher:
|
||||
val source = FileIO.fromPath(Paths.get("example.json"))
|
||||
HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, source))
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
get {
|
||||
pathPrefix("special") {
|
||||
withSettings(special) {
|
||||
sample() // `special` file-io-dispatcher will be used to read the file
|
||||
}
|
||||
} ~ sample() // default file-io-dispatcher will be used to read the file
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/special/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"{}"
|
||||
}
|
||||
Get("/sample") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "{}"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"textract" in {
|
||||
//#textract
|
||||
val pathAndQuery = textract { ctx =>
|
||||
val uri = ctx.request.uri
|
||||
(uri.path, uri.query())
|
||||
}
|
||||
val route =
|
||||
pathAndQuery { (p, query) =>
|
||||
complete(s"The path is $p and the query is $query")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/abcdef?ghi=12") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The path is /abcdef and the query is ghi=12"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"tprovide" in {
|
||||
//#tprovide
|
||||
def provideStringAndLength(value: String) = tprovide((value, value.length))
|
||||
val route =
|
||||
provideStringAndLength("test") { (value, len) =>
|
||||
complete(s"Value is $value and its length is $len")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Value is test and its length is 4"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"0mapResponse" in {
|
||||
//#0mapResponse
|
||||
def overwriteResultStatus(response: HttpResponse): HttpResponse =
|
||||
response.copy(status = StatusCodes.BadGateway)
|
||||
val route = mapResponse(overwriteResultStatus)(complete("abc"))
|
||||
|
||||
// tests:
|
||||
Get("/abcdef?ghi=12") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.BadGateway
|
||||
}
|
||||
//#
|
||||
}
|
||||
"1mapResponse-advanced-json" in {
|
||||
//#1mapResponse-advanced
|
||||
trait ApiRoutes {
|
||||
protected def system: ActorSystem
|
||||
|
||||
private val log = Logging(system, "ApiRoutes")
|
||||
|
||||
private val NullJsonEntity = HttpEntity(ContentTypes.`application/json`, "{}")
|
||||
|
||||
private def nonSuccessToEmptyJsonEntity(response: HttpResponse): HttpResponse =
|
||||
response.status match {
|
||||
case code if code.isSuccess => response
|
||||
case code =>
|
||||
log.warning("Dropping response entity since response status code was: {}", code)
|
||||
response.copy(entity = NullJsonEntity)
|
||||
}
|
||||
|
||||
/** Wrapper for all of our JSON API routes */
|
||||
def apiRoute(innerRoutes: => Route): Route =
|
||||
mapResponse(nonSuccessToEmptyJsonEntity)(innerRoutes)
|
||||
}
|
||||
//#
|
||||
|
||||
import StatusCodes._
|
||||
val __system = system
|
||||
val routes = new ApiRoutes {
|
||||
override protected def system = __system
|
||||
}
|
||||
import routes.apiRoute
|
||||
|
||||
//#1mapResponse-advanced
|
||||
val route: Route =
|
||||
apiRoute {
|
||||
get {
|
||||
complete(InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "{}"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRouteResult" in {
|
||||
//#mapRouteResult
|
||||
// this directive is a joke, don't do that :-)
|
||||
val makeEverythingOk = mapRouteResult {
|
||||
case Complete(response) =>
|
||||
// "Everything is OK!"
|
||||
Complete(response.copy(status = 200))
|
||||
case r => r
|
||||
}
|
||||
|
||||
val route =
|
||||
makeEverythingOk {
|
||||
// will actually render as 200 OK (!)
|
||||
complete(StatusCodes.Accepted)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRouteResultFuture" in {
|
||||
//#mapRouteResultFuture
|
||||
val tryRecoverAddServer = mapRouteResultFuture { fr =>
|
||||
fr recover {
|
||||
case ex: IllegalArgumentException =>
|
||||
Complete(HttpResponse(StatusCodes.InternalServerError))
|
||||
} map {
|
||||
case Complete(res) => Complete(res.addHeader(Server("MyServer 1.0")))
|
||||
case rest => rest
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
tryRecoverAddServer {
|
||||
complete("Hello world!")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
header[Server] shouldEqual Some(Server("MyServer 1.0"))
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapResponseEntity" in {
|
||||
//#mapResponseEntity
|
||||
def prefixEntity(entity: ResponseEntity): ResponseEntity = entity match {
|
||||
case HttpEntity.Strict(contentType, data) =>
|
||||
HttpEntity.Strict(contentType, ByteString("test") ++ data)
|
||||
case _ => throw new IllegalStateException("Unexpected entity type")
|
||||
}
|
||||
|
||||
val prefixWithTest: Directive0 = mapResponseEntity(prefixEntity)
|
||||
val route = prefixWithTest(complete("abc"))
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "testabc"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapResponseHeaders" in {
|
||||
//#mapResponseHeaders
|
||||
// adds all request headers to the response
|
||||
val echoRequestHeaders = extract(_.request.headers).flatMap(respondWithHeaders)
|
||||
|
||||
val removeIdHeader = mapResponseHeaders(_.filterNot(_.lowercaseName == "id"))
|
||||
val route =
|
||||
removeIdHeader {
|
||||
echoRequestHeaders {
|
||||
complete("test")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> RawHeader("id", "12345") ~> RawHeader("id2", "67890") ~> route ~> check {
|
||||
header("id") shouldEqual None
|
||||
header("id2").get.value shouldEqual "67890"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapInnerRoute" in {
|
||||
//#mapInnerRoute
|
||||
val completeWithInnerException =
|
||||
mapInnerRoute { route => ctx =>
|
||||
try {
|
||||
route(ctx)
|
||||
} catch {
|
||||
case NonFatal(e) => ctx.complete(s"Got ${e.getClass.getSimpleName} '${e.getMessage}'")
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
completeWithInnerException {
|
||||
complete(throw new IllegalArgumentException("BLIP! BLOP! Everything broke"))
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Got IllegalArgumentException 'BLIP! BLOP! Everything broke'"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRejections" in {
|
||||
//#mapRejections
|
||||
// ignore any rejections and replace them by AuthorizationFailedRejection
|
||||
val replaceByAuthorizationFailed = mapRejections(_ => List(AuthorizationFailedRejection))
|
||||
val route =
|
||||
replaceByAuthorizationFailed {
|
||||
path("abc")(complete("abc"))
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejection shouldEqual AuthorizationFailedRejection
|
||||
}
|
||||
|
||||
Get("/abc") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
//#
|
||||
}
|
||||
"recoverRejections" in {
|
||||
//#recoverRejections
|
||||
val authRejectionsToNothingToSeeHere = recoverRejections { rejections =>
|
||||
if (rejections.exists(_.isInstanceOf[AuthenticationFailedRejection]))
|
||||
Complete(HttpResponse(entity = "Nothing to see here, move along."))
|
||||
else if (rejections == Nil) // see "Empty Rejections" for more details
|
||||
Complete(HttpResponse(StatusCodes.NotFound, entity = "Literally nothing to see here."))
|
||||
else
|
||||
Rejected(rejections)
|
||||
}
|
||||
val neverAuth: Authenticator[String] = creds => None
|
||||
val alwaysAuth: Authenticator[String] = creds => Some("id")
|
||||
|
||||
val route =
|
||||
authRejectionsToNothingToSeeHere {
|
||||
pathPrefix("auth") {
|
||||
path("never") {
|
||||
authenticateBasic("my-realm", neverAuth) { user =>
|
||||
complete("Welcome to the bat-cave!")
|
||||
}
|
||||
} ~
|
||||
path("always") {
|
||||
authenticateBasic("my-realm", alwaysAuth) { user =>
|
||||
complete("Welcome to the secret place!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/auth/never") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Nothing to see here, move along."
|
||||
}
|
||||
Get("/auth/always") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Welcome to the secret place!"
|
||||
}
|
||||
Get("/auth/does_not_exist") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[String] shouldEqual "Literally nothing to see here."
|
||||
}
|
||||
//#
|
||||
}
|
||||
"recoverRejectionsWith" in {
|
||||
//#recoverRejectionsWith
|
||||
val authRejectionsToNothingToSeeHere = recoverRejectionsWith { rejections =>
|
||||
Future {
|
||||
// imagine checking rejections takes a longer time:
|
||||
if (rejections.exists(_.isInstanceOf[AuthenticationFailedRejection]))
|
||||
Complete(HttpResponse(entity = "Nothing to see here, move along."))
|
||||
else
|
||||
Rejected(rejections)
|
||||
}
|
||||
}
|
||||
val neverAuth: Authenticator[String] = creds => None
|
||||
|
||||
val route =
|
||||
authRejectionsToNothingToSeeHere {
|
||||
pathPrefix("auth") {
|
||||
path("never") {
|
||||
authenticateBasic("my-realm", neverAuth) { user =>
|
||||
complete("Welcome to the bat-cave!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/auth/never") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Nothing to see here, move along."
|
||||
}
|
||||
//#
|
||||
}
|
||||
"0mapRequest" in {
|
||||
//#0mapRequest
|
||||
def transformToPostRequest(req: HttpRequest): HttpRequest = req.copy(method = HttpMethods.POST)
|
||||
val route =
|
||||
mapRequest(transformToPostRequest) {
|
||||
extractRequest { req =>
|
||||
complete(s"The request method was ${req.method.name}")
|
||||
}
|
||||
}
|
||||
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The request method was POST"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRequestContext" in {
|
||||
//#mapRequestContext
|
||||
val replaceRequest =
|
||||
mapRequestContext(_.withRequest(HttpRequest(HttpMethods.POST)))
|
||||
|
||||
val route =
|
||||
replaceRequest {
|
||||
extractRequest { req =>
|
||||
complete(req.method.value)
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/abc/def/ghi") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "POST"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"0mapRouteResult" in {
|
||||
//#0mapRouteResult
|
||||
val rejectAll = // not particularly useful directive
|
||||
mapRouteResult {
|
||||
case _ => Rejected(List(AuthorizationFailedRejection))
|
||||
}
|
||||
val route =
|
||||
rejectAll {
|
||||
complete("abc")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejections.nonEmpty shouldEqual true
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRouteResultPF" in {
|
||||
//#mapRouteResultPF
|
||||
case object MyCustomRejection extends Rejection
|
||||
val rejectRejections = // not particularly useful directive
|
||||
mapRouteResultPF {
|
||||
case Rejected(_) => Rejected(List(AuthorizationFailedRejection))
|
||||
}
|
||||
val route =
|
||||
rejectRejections {
|
||||
reject(MyCustomRejection)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejection shouldEqual AuthorizationFailedRejection
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRouteResultWithPF-0" in {
|
||||
//#mapRouteResultWithPF-0
|
||||
case object MyCustomRejection extends Rejection
|
||||
val rejectRejections = // not particularly useful directive
|
||||
mapRouteResultWithPF {
|
||||
case Rejected(_) => Future(Rejected(List(AuthorizationFailedRejection)))
|
||||
}
|
||||
val route =
|
||||
rejectRejections {
|
||||
reject(MyCustomRejection)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejection shouldEqual AuthorizationFailedRejection
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapRouteResultWith-0" in {
|
||||
//#mapRouteResultWith-0
|
||||
case object MyCustomRejection extends Rejection
|
||||
val rejectRejections = // not particularly useful directive
|
||||
mapRouteResultWith {
|
||||
case Rejected(_) => Future(Rejected(List(AuthorizationFailedRejection)))
|
||||
case res => Future(res)
|
||||
}
|
||||
val route =
|
||||
rejectRejections {
|
||||
reject(MyCustomRejection)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejection shouldEqual AuthorizationFailedRejection
|
||||
}
|
||||
//#
|
||||
}
|
||||
"pass" in {
|
||||
//#pass
|
||||
val route = pass(complete("abc"))
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "abc"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"0provide" in {
|
||||
//#0provide
|
||||
def providePrefixedString(value: String): Directive1[String] = provide("prefix:" + value)
|
||||
val route =
|
||||
providePrefixedString("test") { value =>
|
||||
complete(value)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "prefix:test"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"cancelRejections-filter-example" in {
|
||||
//#cancelRejections-filter-example
|
||||
def isMethodRejection: Rejection => Boolean = {
|
||||
case MethodRejection(_) => true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
val route =
|
||||
cancelRejections(isMethodRejection) {
|
||||
post {
|
||||
complete("Result")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejections shouldEqual Nil
|
||||
handled shouldEqual false
|
||||
}
|
||||
//#
|
||||
}
|
||||
"cancelRejection-example" in {
|
||||
//#cancelRejection-example
|
||||
val route =
|
||||
cancelRejection(MethodRejection(HttpMethods.POST)) {
|
||||
post {
|
||||
complete("Result")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
rejections shouldEqual Nil
|
||||
handled shouldEqual false
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractRequest-example" in {
|
||||
//#extractRequest-example
|
||||
val route =
|
||||
extractRequest { request =>
|
||||
complete(s"Request method is ${request.method.name} and content-type is ${request.entity.contentType}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", "text") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request method is POST and content-type is text/plain; charset=UTF-8"
|
||||
}
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request method is GET and content-type is none/none"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractSettings-examples" in {
|
||||
//#extractSettings-examples
|
||||
val route =
|
||||
extractSettings { settings: RoutingSettings =>
|
||||
complete(s"RoutingSettings.renderVanityFooter = ${settings.renderVanityFooter}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "RoutingSettings.renderVanityFooter = true"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapSettings-examples" in {
|
||||
//#mapSettings-examples
|
||||
val tunedSettings = mapSettings { settings =>
|
||||
settings.withFileGetConditional(false)
|
||||
}
|
||||
|
||||
val route =
|
||||
tunedSettings {
|
||||
extractSettings { settings: RoutingSettings =>
|
||||
complete(s"RoutingSettings.fileGetConditional = ${settings.fileGetConditional}")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"RoutingSettings.fileGetConditional = false"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractRequestContext-example" in {
|
||||
//#extractRequestContext-example
|
||||
val route =
|
||||
extractRequestContext { ctx =>
|
||||
ctx.log.debug("Using access to additional context availablethings, like the logger.")
|
||||
val request = ctx.request
|
||||
complete(s"Request method is ${request.method.name} and content-type is ${request.entity.contentType}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", "text") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request method is POST and content-type is text/plain; charset=UTF-8"
|
||||
}
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request method is GET and content-type is none/none"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractUri-example" in {
|
||||
//#extractUri-example
|
||||
val route =
|
||||
extractUri { uri =>
|
||||
complete(s"Full URI: $uri")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
// tests are executed with the host assumed to be "example.com"
|
||||
responseAs[String] shouldEqual "Full URI: http://example.com/"
|
||||
}
|
||||
Get("/test") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Full URI: http://example.com/test"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapUnmatchedPath-example" in {
|
||||
//#mapUnmatchedPath-example
|
||||
def ignore456(path: Uri.Path) = path match {
|
||||
case s @ Uri.Path.Segment(head, tail) if head.startsWith("456") =>
|
||||
val newHead = head.drop(3)
|
||||
if (newHead.isEmpty) tail
|
||||
else s.copy(head = head.drop(3))
|
||||
case _ => path
|
||||
}
|
||||
val ignoring456 = mapUnmatchedPath(ignore456)
|
||||
|
||||
val route =
|
||||
pathPrefix("123") {
|
||||
ignoring456 {
|
||||
path("abc") {
|
||||
complete("Content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/123/abc") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Content"
|
||||
}
|
||||
Get("/123456/abc") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Content"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractUnmatchedPath-example" in {
|
||||
//#extractUnmatchedPath-example
|
||||
val route =
|
||||
pathPrefix("abc") {
|
||||
extractUnmatchedPath { remaining =>
|
||||
complete(s"Unmatched: '$remaining'")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/abc") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Unmatched: ''"
|
||||
}
|
||||
Get("/abc/456") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Unmatched: '/456'"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractRequestEntity-example" in {
|
||||
//#extractRequestEntity-example
|
||||
val route =
|
||||
extractRequestEntity { entity =>
|
||||
complete(s"Request entity content-type is ${entity.contentType}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
val httpEntity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, "req")
|
||||
Post("/abc", httpEntity) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request entity content-type is text/plain; charset=UTF-8"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractDataBytes-example" in {
|
||||
//#extractDataBytes-example
|
||||
val route =
|
||||
extractDataBytes { data ⇒
|
||||
val sum = data.runFold(0) { (acc, i) ⇒ acc + i.utf8String.toInt }
|
||||
onSuccess(sum) { s ⇒
|
||||
complete(HttpResponse(entity = HttpEntity(s.toString)))
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
val dataBytes = Source.fromIterator(() ⇒ Iterator.range(1, 10).map(x ⇒ ByteString(x.toString)))
|
||||
Post("/abc", HttpEntity(ContentTypes.`text/plain(UTF-8)`, data = dataBytes)) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "45"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"extractStrictEntity-example" in {
|
||||
//#extractStrictEntity-example
|
||||
import scala.concurrent.duration._
|
||||
val route = extractStrictEntity(3.seconds) { entity =>
|
||||
complete(entity.data.utf8String)
|
||||
}
|
||||
|
||||
// tests:
|
||||
val dataBytes = Source.fromIterator(() ⇒ Iterator.range(1, 10).map(x ⇒ ByteString(x.toString)))
|
||||
Post("/", HttpEntity(ContentTypes.`text/plain(UTF-8)`, data = dataBytes)) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "123456789"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"toStrictEntity-example" in {
|
||||
//#toStrictEntity-example
|
||||
import scala.concurrent.duration._
|
||||
val route = toStrictEntity(3.seconds) {
|
||||
extractRequest { req =>
|
||||
req.entity match {
|
||||
case strict: HttpEntity.Strict =>
|
||||
complete(s"Request entity is strict, data=${strict.data.utf8String}")
|
||||
case _ =>
|
||||
complete("Ooops, request entity is not strict!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
val dataBytes = Source.fromIterator(() ⇒ Iterator.range(1, 10).map(x ⇒ ByteString(x.toString)))
|
||||
Post("/", HttpEntity(ContentTypes.`text/plain(UTF-8)`, data = dataBytes)) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request entity is strict, data=123456789"
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
"extractActorSystem-example" in {
|
||||
//#extractActorSystem-example
|
||||
val route = extractActorSystem { actorSystem =>
|
||||
complete(s"Actor System extracted, hash=${actorSystem.hashCode()}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual s"Actor System extracted, hash=${system.hashCode()}"
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.coding._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import akka.http.scaladsl.model.{ HttpResponse, StatusCodes }
|
||||
import akka.http.scaladsl.model.headers.{ HttpEncodings, HttpEncoding, `Accept-Encoding`, `Content-Encoding` }
|
||||
import akka.http.scaladsl.model.headers.HttpEncodings._
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.matchers.Matcher
|
||||
|
||||
class CodingDirectivesExamplesSpec extends RoutingSpec {
|
||||
"responseEncodingAccepted" in {
|
||||
val route = responseEncodingAccepted(gzip) { complete("content") }
|
||||
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "content"
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> route ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
"encodeResponse" in {
|
||||
val route = encodeResponse { complete("content") }
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
response should haveContentEncoding(identity)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> route ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> route ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(identity) ~> route ~> check {
|
||||
response should haveContentEncoding(identity)
|
||||
}
|
||||
}
|
||||
"encodeResponseWith" in {
|
||||
val route = encodeResponseWith(Gzip) { complete("content") }
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> route ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> route ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(identity) ~> route ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
|
||||
val helloGzipped = compress("Hello", Gzip)
|
||||
val helloDeflated = compress("Hello", Deflate)
|
||||
"decodeRequest" in {
|
||||
val route =
|
||||
decodeRequest {
|
||||
entity(as[String]) { content: String =>
|
||||
complete(s"Request content: '$content'")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request content: 'Hello'"
|
||||
}
|
||||
Post("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request content: 'Hello'"
|
||||
}
|
||||
Post("/", "hello uncompressed") ~> `Content-Encoding`(identity) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request content: 'hello uncompressed'"
|
||||
}
|
||||
}
|
||||
"decodeRequestWith-0" in {
|
||||
val route =
|
||||
decodeRequestWith(Gzip) {
|
||||
entity(as[String]) { content: String =>
|
||||
complete(s"Request content: '$content'")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request content: 'Hello'"
|
||||
}
|
||||
Post("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> route ~> check {
|
||||
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
|
||||
}
|
||||
Post("/", "hello") ~> `Content-Encoding`(identity) ~> route ~> check {
|
||||
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
"decodeRequestWith-1" in {
|
||||
val route =
|
||||
decodeRequestWith(Gzip, NoCoding) {
|
||||
entity(as[String]) { content: String =>
|
||||
complete(s"Request content: '$content'")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request content: 'Hello'"
|
||||
}
|
||||
Post("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> route ~> check {
|
||||
rejections shouldEqual List(UnsupportedRequestEncodingRejection(gzip), UnsupportedRequestEncodingRejection(identity))
|
||||
}
|
||||
Post("/", "hello uncompressed") ~> `Content-Encoding`(identity) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Request content: 'hello uncompressed'"
|
||||
}
|
||||
}
|
||||
|
||||
def haveContentEncoding(encoding: HttpEncoding): Matcher[HttpResponse] =
|
||||
be(encoding) compose { (_: HttpResponse).header[`Content-Encoding`].map(_.encodings.head).getOrElse(HttpEncodings.identity) }
|
||||
|
||||
def compress(input: String, encoder: Encoder): ByteString = encoder.encode(ByteString(input))
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.model.headers.{ HttpCookie, Cookie, `Set-Cookie` }
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import akka.http.scaladsl.model.DateTime
|
||||
|
||||
class CookieDirectivesExamplesSpec extends RoutingSpec {
|
||||
"cookie" in {
|
||||
val route =
|
||||
cookie("userName") { nameCookie =>
|
||||
complete(s"The logged in user is '${nameCookie.value}'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> Cookie("userName" -> "paul") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The logged in user is 'paul'"
|
||||
}
|
||||
// missing cookie
|
||||
Get("/") ~> route ~> check {
|
||||
rejection shouldEqual MissingCookieRejection("userName")
|
||||
}
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] shouldEqual "Request is missing required cookie 'userName'"
|
||||
}
|
||||
}
|
||||
"optionalCookie" in {
|
||||
val route =
|
||||
optionalCookie("userName") {
|
||||
case Some(nameCookie) => complete(s"The logged in user is '${nameCookie.value}'")
|
||||
case None => complete("No user logged in")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> Cookie("userName" -> "paul") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The logged in user is 'paul'"
|
||||
}
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "No user logged in"
|
||||
}
|
||||
}
|
||||
"deleteCookie" in {
|
||||
val route =
|
||||
deleteCookie("userName") {
|
||||
complete("The user was logged out")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The user was logged out"
|
||||
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("userName", value = "deleted", expires = Some(DateTime.MinValue))))
|
||||
}
|
||||
}
|
||||
"setCookie" in {
|
||||
val route =
|
||||
setCookie(HttpCookie("userName", value = "paul")) {
|
||||
complete("The user was logged in")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The user was logged in"
|
||||
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("userName", value = "paul")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.server.{ Directive1, Directive }
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class CustomDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"labeling" in {
|
||||
val getOrPut = get | put
|
||||
|
||||
// tests:
|
||||
val route = getOrPut { complete("ok") }
|
||||
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "ok"
|
||||
}
|
||||
|
||||
Put("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "ok"
|
||||
}
|
||||
}
|
||||
|
||||
"map-0" in {
|
||||
val textParam: Directive1[String] =
|
||||
parameter("text".as[String])
|
||||
|
||||
val lengthDirective: Directive1[Int] =
|
||||
textParam.map(text => text.length)
|
||||
|
||||
// tests:
|
||||
Get("/?text=abcdefg") ~> lengthDirective(x => complete(x.toString)) ~> check {
|
||||
responseAs[String] === "7"
|
||||
}
|
||||
}
|
||||
|
||||
"tmap-1" in {
|
||||
val twoIntParameters: Directive[(Int, Int)] =
|
||||
parameters(("a".as[Int], "b".as[Int]))
|
||||
|
||||
val myDirective: Directive1[String] =
|
||||
twoIntParameters.tmap {
|
||||
case (a, b) => (a + b).toString
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?a=2&b=5") ~> myDirective(x => complete(x)) ~> check {
|
||||
responseAs[String] === "7"
|
||||
}
|
||||
}
|
||||
|
||||
"flatMap-0" in {
|
||||
val intParameter: Directive1[Int] = parameter("a".as[Int])
|
||||
|
||||
val myDirective: Directive1[Int] =
|
||||
intParameter.flatMap {
|
||||
case a if a > 0 => provide(2 * a)
|
||||
case _ => reject
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?a=21") ~> myDirective(i => complete(i.toString)) ~> check {
|
||||
responseAs[String] === "42"
|
||||
}
|
||||
Get("/?a=-18") ~> myDirective(i => complete(i.toString)) ~> check {
|
||||
handled === false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.{ Http, TestUtils }
|
||||
import akka.http.scaladsl.client.RequestBuilding
|
||||
import akka.http.scaladsl.model.HttpProtocols._
|
||||
import akka.http.scaladsl.model.RequestEntityAcceptance.Expected
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.Directives
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.testkit.AkkaSpec
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.concurrent.ScalaFutures
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class CustomHttpMethodSpec extends AkkaSpec with ScalaFutures
|
||||
with Directives with RequestBuilding {
|
||||
|
||||
implicit val mat = ActorMaterializer()
|
||||
|
||||
"Http" should {
|
||||
"allow registering custom method" in {
|
||||
import system.dispatcher
|
||||
val (_, host, port) = TestUtils.temporaryServerHostnameAndPort()
|
||||
|
||||
//#application-custom
|
||||
import akka.http.scaladsl.settings.{ ParserSettings, ServerSettings }
|
||||
|
||||
// define custom media type:
|
||||
val BOLT = HttpMethod.custom("BOLT", safe = false,
|
||||
idempotent = true, requestEntityAcceptance = Expected)
|
||||
|
||||
// add custom method to parser settings:
|
||||
val parserSettings = ParserSettings(system).withCustomMethods(BOLT)
|
||||
val serverSettings = ServerSettings(system).withParserSettings(parserSettings)
|
||||
|
||||
val routes = extractMethod { method ⇒
|
||||
complete(s"This is a ${method.name} method request.")
|
||||
}
|
||||
val binding = Http().bindAndHandle(routes, host, port, settings = serverSettings)
|
||||
//#application-custom
|
||||
|
||||
val request = HttpRequest(BOLT, s"http://$host:$port/", protocol = `HTTP/1.0`)
|
||||
val response = Http().singleRequest(request).futureValue
|
||||
|
||||
response.status should ===(StatusCodes.OK)
|
||||
val responseBody = response.toStrict(1.second).futureValue.entity.dataBytes.runFold(ByteString.empty)(_ ++ _).futureValue.utf8String
|
||||
responseBody should ===("This is a BOLT method request.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.event.{ LoggingAdapter, Logging }
|
||||
import akka.event.Logging.LogLevel
|
||||
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse }
|
||||
import akka.http.scaladsl.server.RouteResult
|
||||
import akka.http.scaladsl.server.RouteResult.{ Rejected, Complete }
|
||||
import akka.http.scaladsl.server.directives.{ DebuggingDirectives, LogEntry, LoggingMagnet }
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class DebuggingDirectivesExamplesSpec extends RoutingSpec {
|
||||
"logRequest-0" in {
|
||||
// different possibilities of using logRequest
|
||||
|
||||
// The first alternatives use an implicitly available LoggingContext for logging
|
||||
// marks with "get-user", log with debug level, HttpRequest.toString
|
||||
DebuggingDirectives.logRequest("get-user")
|
||||
|
||||
// marks with "get-user", log with info level, HttpRequest.toString
|
||||
DebuggingDirectives.logRequest(("get-user", Logging.InfoLevel))
|
||||
|
||||
// logs just the request method at debug level
|
||||
def requestMethod(req: HttpRequest): String = req.method.name
|
||||
DebuggingDirectives.logRequest(requestMethod _)
|
||||
|
||||
// logs just the request method at info level
|
||||
def requestMethodAsInfo(req: HttpRequest): LogEntry = LogEntry(req.method.name, Logging.InfoLevel)
|
||||
DebuggingDirectives.logRequest(requestMethodAsInfo _)
|
||||
|
||||
// This one doesn't use the implicit LoggingContext but uses `println` for logging
|
||||
def printRequestMethod(req: HttpRequest): Unit = println(req.method.name)
|
||||
val logRequestPrintln = DebuggingDirectives.logRequest(LoggingMagnet(_ => printRequestMethod))
|
||||
|
||||
// tests:
|
||||
Get("/") ~> logRequestPrintln(complete("logged")) ~> check {
|
||||
responseAs[String] shouldEqual "logged"
|
||||
}
|
||||
}
|
||||
"logRequestResult" in {
|
||||
// different possibilities of using logRequestResponse
|
||||
|
||||
// The first alternatives use an implicitly available LoggingContext for logging
|
||||
// marks with "get-user", log with debug level, HttpRequest.toString, HttpResponse.toString
|
||||
DebuggingDirectives.logRequestResult("get-user")
|
||||
|
||||
// marks with "get-user", log with info level, HttpRequest.toString, HttpResponse.toString
|
||||
DebuggingDirectives.logRequestResult(("get-user", Logging.InfoLevel))
|
||||
|
||||
// logs just the request method and response status at info level
|
||||
def requestMethodAndResponseStatusAsInfo(req: HttpRequest): RouteResult => Option[LogEntry] = {
|
||||
case RouteResult.Complete(res) => Some(LogEntry(req.method.name + ": " + res.status, Logging.InfoLevel))
|
||||
case _ => None // no log entries for rejections
|
||||
}
|
||||
DebuggingDirectives.logRequestResult(requestMethodAndResponseStatusAsInfo _)
|
||||
|
||||
// This one doesn't use the implicit LoggingContext but uses `println` for logging
|
||||
def printRequestMethodAndResponseStatus(req: HttpRequest)(res: RouteResult): Unit =
|
||||
println(requestMethodAndResponseStatusAsInfo(req)(res).map(_.obj.toString).getOrElse(""))
|
||||
val logRequestResultPrintln = DebuggingDirectives.logRequestResult(LoggingMagnet(_ => printRequestMethodAndResponseStatus))
|
||||
|
||||
// tests:
|
||||
Get("/") ~> logRequestResultPrintln(complete("logged")) ~> check {
|
||||
responseAs[String] shouldEqual "logged"
|
||||
}
|
||||
}
|
||||
"logResult" in {
|
||||
// different possibilities of using logResponse
|
||||
|
||||
// The first alternatives use an implicitly available LoggingContext for logging
|
||||
// marks with "get-user", log with debug level, HttpResponse.toString
|
||||
DebuggingDirectives.logResult("get-user")
|
||||
|
||||
// marks with "get-user", log with info level, HttpResponse.toString
|
||||
DebuggingDirectives.logResult(("get-user", Logging.InfoLevel))
|
||||
|
||||
// logs just the response status at debug level
|
||||
def responseStatus(res: RouteResult): String = res match {
|
||||
case RouteResult.Complete(x) => x.status.toString
|
||||
case RouteResult.Rejected(rejections) => "Rejected: " + rejections.mkString(", ")
|
||||
}
|
||||
DebuggingDirectives.logResult(responseStatus _)
|
||||
|
||||
// logs just the response status at info level
|
||||
def responseStatusAsInfo(res: RouteResult): LogEntry = LogEntry(responseStatus(res), Logging.InfoLevel)
|
||||
DebuggingDirectives.logResult(responseStatusAsInfo _)
|
||||
|
||||
// This one doesn't use the implicit LoggingContext but uses `println` for logging
|
||||
def printResponseStatus(res: RouteResult): Unit = println(responseStatus(res))
|
||||
val logResultPrintln = DebuggingDirectives.logResult(LoggingMagnet(_ => printResponseStatus))
|
||||
|
||||
// tests:
|
||||
Get("/") ~> logResultPrintln(complete("logged")) ~> check {
|
||||
responseAs[String] shouldEqual "logged"
|
||||
}
|
||||
}
|
||||
"logRequestResultWithResponseTime" in {
|
||||
|
||||
def akkaResponseTimeLoggingFunction(
|
||||
loggingAdapter: LoggingAdapter,
|
||||
requestTimestamp: Long,
|
||||
level: LogLevel = Logging.InfoLevel)(req: HttpRequest)(res: Any): Unit = {
|
||||
val entry = res match {
|
||||
case Complete(resp) =>
|
||||
val responseTimestamp: Long = System.nanoTime
|
||||
val elapsedTime: Long = (responseTimestamp - requestTimestamp) / 1000000
|
||||
val loggingString = s"""Logged Request:${req.method}:${req.uri}:${resp.status}:${elapsedTime}"""
|
||||
LogEntry(loggingString, level)
|
||||
case Rejected(reason) =>
|
||||
LogEntry(s"Rejected Reason: ${reason.mkString(",")}", level)
|
||||
}
|
||||
entry.logTo(loggingAdapter)
|
||||
}
|
||||
def printResponseTime(log: LoggingAdapter) = {
|
||||
val requestTimestamp = System.nanoTime
|
||||
akkaResponseTimeLoggingFunction(log, requestTimestamp)(_)
|
||||
}
|
||||
|
||||
val logResponseTime = DebuggingDirectives.logRequestResult(LoggingMagnet(printResponseTime(_)))
|
||||
|
||||
Get("/") ~> logResponseTime(complete("logged")) ~> check {
|
||||
responseAs[String] shouldEqual "logged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.server._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class ExecutionDirectivesExamplesSpec extends RoutingSpec {
|
||||
"handleExceptions" in {
|
||||
val divByZeroHandler = ExceptionHandler {
|
||||
case _: ArithmeticException => complete((StatusCodes.BadRequest, "You've got your arithmetic wrong, fool!"))
|
||||
}
|
||||
val route =
|
||||
path("divide" / IntNumber / IntNumber) { (a, b) =>
|
||||
handleExceptions(divByZeroHandler) {
|
||||
complete(s"The result is ${a / b}")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/divide/10/5") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The result is 2"
|
||||
}
|
||||
Get("/divide/10/0") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[String] shouldEqual "You've got your arithmetic wrong, fool!"
|
||||
}
|
||||
}
|
||||
"handleRejections" in {
|
||||
val totallyMissingHandler = RejectionHandler.newBuilder()
|
||||
.handleNotFound { complete((StatusCodes.NotFound, "Oh man, what you are looking for is long gone.")) }
|
||||
.handle { case ValidationRejection(msg, _) => complete((StatusCodes.InternalServerError, msg)) }
|
||||
.result()
|
||||
val route =
|
||||
pathPrefix("handled") {
|
||||
handleRejections(totallyMissingHandler) {
|
||||
path("existing")(complete("This path exists")) ~
|
||||
path("boom")(reject(new ValidationRejection("This didn't work.")))
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/handled/existing") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This path exists"
|
||||
}
|
||||
Get("/missing") ~> Route.seal(route) /* applies default handler */ ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[String] shouldEqual "The requested resource could not be found."
|
||||
}
|
||||
Get("/handled/missing") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[String] shouldEqual "Oh man, what you are looking for is long gone."
|
||||
}
|
||||
Get("/handled/boom") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "This didn't work."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToEntityMarshaller
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.server.directives.DirectoryListing
|
||||
import akka.http.scaladsl.server.directives.FileAndResourceDirectives.DirectoryRenderer
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class FileAndResourceDirectivesExamplesSpec extends RoutingSpec {
|
||||
"getFromFile-examples" in compileOnlySpec {
|
||||
import akka.http.scaladsl.server.directives._
|
||||
import ContentTypeResolver.Default
|
||||
|
||||
val route =
|
||||
path("logs" / Segment) { name =>
|
||||
getFromFile(s"$name.log") // uses implicit ContentTypeResolver
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/logs/example") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
"getFromResource-examples" in compileOnlySpec {
|
||||
import akka.http.scaladsl.server.directives._
|
||||
import ContentTypeResolver.Default
|
||||
|
||||
val route =
|
||||
path("logs" / Segment) { name =>
|
||||
getFromResource(s"$name.log") // uses implicit ContentTypeResolver
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/logs/example") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
"listDirectoryContents-examples" in compileOnlySpec {
|
||||
val route =
|
||||
path("tmp") {
|
||||
listDirectoryContents("/tmp")
|
||||
} ~
|
||||
path("custom") {
|
||||
// implement your custom renderer here
|
||||
val renderer = new DirectoryRenderer {
|
||||
override def marshaller(renderVanityFooter: Boolean): ToEntityMarshaller[DirectoryListing] = ???
|
||||
}
|
||||
listDirectoryContents("/tmp")(renderer)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/logs/example") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
"getFromBrowseableDirectory-examples" in compileOnlySpec {
|
||||
val route =
|
||||
path("tmp") {
|
||||
getFromBrowseableDirectory("/tmp")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/tmp") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
"getFromBrowseableDirectories-examples" in compileOnlySpec {
|
||||
val route =
|
||||
path("tmp") {
|
||||
getFromBrowseableDirectories("/main", "/backups")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/tmp") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
"getFromDirectory-examples" in compileOnlySpec {
|
||||
val route =
|
||||
pathPrefix("tmp") {
|
||||
getFromDirectory("/tmp")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/tmp/example") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
"getFromResourceDirectory-examples" in compileOnlySpec {
|
||||
val route =
|
||||
pathPrefix("examples") {
|
||||
getFromResourceDirectory("/examples")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/examples/example-1") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2015-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.testkit.RouteTestTimeout
|
||||
import akka.stream.scaladsl.Framing
|
||||
import akka.util.ByteString
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import akka.testkit.TestDuration
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class FileUploadDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
override def testConfigSource = "akka.actor.default-mailbox.mailbox-type = \"akka.dispatch.UnboundedMailbox\""
|
||||
|
||||
// test touches disk, so give it some time
|
||||
implicit val routeTimeout = RouteTestTimeout(3.seconds.dilated)
|
||||
|
||||
"uploadedFile" in {
|
||||
|
||||
val route =
|
||||
uploadedFile("csv") {
|
||||
case (metadata, file) =>
|
||||
// do something with the file and file metadata ...
|
||||
file.delete()
|
||||
complete(StatusCodes.OK)
|
||||
}
|
||||
|
||||
// tests:
|
||||
val multipartForm =
|
||||
Multipart.FormData(
|
||||
Multipart.FormData.BodyPart.Strict(
|
||||
"csv",
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "1,5,7\n11,13,17"),
|
||||
Map("filename" -> "data.csv")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"fileUpload" in {
|
||||
|
||||
// adding integers as a service ;)
|
||||
val route =
|
||||
extractRequestContext { ctx =>
|
||||
implicit val materializer = ctx.materializer
|
||||
implicit val ec = ctx.executionContext
|
||||
|
||||
fileUpload("csv") {
|
||||
case (metadata, byteSource) =>
|
||||
|
||||
val sumF: Future[Int] =
|
||||
// sum the numbers as they arrive so that we can
|
||||
// accept any size of file
|
||||
byteSource.via(Framing.delimiter(ByteString("\n"), 1024))
|
||||
.mapConcat(_.utf8String.split(",").toVector)
|
||||
.map(_.toInt)
|
||||
.runFold(0) { (acc, n) => acc + n }
|
||||
|
||||
onSuccess(sumF) { sum => complete(s"Sum: $sum") }
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
val multipartForm =
|
||||
Multipart.FormData(Multipart.FormData.BodyPart.Strict(
|
||||
"csv",
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "2,3,5\n7,11,13,17,23\n29,31,37\n"),
|
||||
Map("filename" -> "primes.csv")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Sum: 178"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.model._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class FormFieldDirectivesExamplesSpec extends RoutingSpec {
|
||||
"formFields" in {
|
||||
val route =
|
||||
formFields('color, 'age.as[Int]) { (color, age) =>
|
||||
complete(s"The color is '$color' and the age ten years ago was ${age - 10}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", FormData("color" -> "blue", "age" -> "68")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and the age ten years ago was 58"
|
||||
}
|
||||
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[String] shouldEqual "Request is missing required form field 'color'"
|
||||
}
|
||||
}
|
||||
"formField" in {
|
||||
val route =
|
||||
formField('color) { color =>
|
||||
complete(s"The color is '$color'")
|
||||
} ~
|
||||
formField('id.as[Int]) { id =>
|
||||
complete(s"The id is '$id'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", FormData("color" -> "blue")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue'"
|
||||
}
|
||||
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[String] shouldEqual "Request is missing required form field 'color'"
|
||||
}
|
||||
}
|
||||
"formFieldMap" in {
|
||||
val route =
|
||||
formFieldMap { fields =>
|
||||
def formFieldString(formField: (String, String)): String =
|
||||
s"""${formField._1} = '${formField._2}'"""
|
||||
complete(s"The form fields are ${fields.map(formFieldString).mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", FormData("color" -> "blue", "count" -> "42")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The form fields are color = 'blue', count = '42'"
|
||||
}
|
||||
Post("/", FormData("x" -> "1", "x" -> "5")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The form fields are x = '5'"
|
||||
}
|
||||
}
|
||||
"formFieldMultiMap" in {
|
||||
val route =
|
||||
formFieldMultiMap { fields =>
|
||||
complete("There are " +
|
||||
s"form fields ${fields.map(x => x._1 + " -> " + x._2.size).mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", FormData("color" -> "blue", "count" -> "42")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "There are form fields color -> 1, count -> 1"
|
||||
}
|
||||
Post("/", FormData("x" -> "23", "x" -> "4", "x" -> "89")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "There are form fields x -> 3"
|
||||
}
|
||||
}
|
||||
"formFieldSeq" in {
|
||||
val route =
|
||||
formFieldSeq { fields =>
|
||||
def formFieldString(formField: (String, String)): String =
|
||||
s"""${formField._1} = '${formField._2}'"""
|
||||
complete(s"The form fields are ${fields.map(formFieldString).mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", FormData("color" -> "blue", "count" -> "42")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The form fields are color = 'blue', count = '42'"
|
||||
}
|
||||
Post("/", FormData("x" -> "23", "x" -> "4", "x" -> "89")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The form fields are x = '23', x = '4', x = '89'"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{ Failure, Success }
|
||||
import akka.http.scaladsl.server.{ CircuitBreakerOpenRejection, ExceptionHandler, Route }
|
||||
import akka.util.Timeout
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
import akka.pattern.CircuitBreaker
|
||||
|
||||
// format: OFF
|
||||
|
||||
class FutureDirectivesExamplesSpec extends RoutingSpec {
|
||||
object TestException extends Throwable
|
||||
|
||||
implicit val myExceptionHandler =
|
||||
ExceptionHandler {
|
||||
case TestException => ctx =>
|
||||
ctx.complete((InternalServerError, "Unsuccessful future!"))
|
||||
}
|
||||
|
||||
implicit val responseTimeout = Timeout(2, TimeUnit.SECONDS)
|
||||
|
||||
"onComplete" in {
|
||||
def divide(a: Int, b: Int): Future[Int] = Future {
|
||||
a / b
|
||||
}
|
||||
|
||||
val route =
|
||||
path("divide" / IntNumber / IntNumber) { (a, b) =>
|
||||
onComplete(divide(a, b)) {
|
||||
case Success(value) => complete(s"The result was $value")
|
||||
case Failure(ex) => complete((InternalServerError, s"An error occurred: ${ex.getMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/divide/10/2") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The result was 5"
|
||||
}
|
||||
|
||||
Get("/divide/10/0") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual InternalServerError
|
||||
responseAs[String] shouldEqual "An error occurred: / by zero"
|
||||
}
|
||||
}
|
||||
|
||||
"onCompleteWithBreaker" in {
|
||||
def divide(a: Int, b: Int): Future[Int] = Future {
|
||||
a / b
|
||||
}
|
||||
|
||||
val resetTimeout = 1.second
|
||||
val breaker = new CircuitBreaker(system.scheduler,
|
||||
maxFailures = 1,
|
||||
callTimeout = 5.seconds,
|
||||
resetTimeout
|
||||
)
|
||||
|
||||
val route =
|
||||
path("divide" / IntNumber / IntNumber) { (a, b) =>
|
||||
onCompleteWithBreaker(breaker)(divide(a, b)) {
|
||||
case Success(value) => complete(s"The result was $value")
|
||||
case Failure(ex) => complete((InternalServerError, s"An error occurred: ${ex.getMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/divide/10/2") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The result was 5"
|
||||
}
|
||||
|
||||
Get("/divide/10/0") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual InternalServerError
|
||||
responseAs[String] shouldEqual "An error occurred: / by zero"
|
||||
} // opens the circuit breaker
|
||||
|
||||
Get("/divide/10/2") ~> route ~> check {
|
||||
rejection shouldBe a[CircuitBreakerOpenRejection]
|
||||
}
|
||||
|
||||
Thread.sleep(resetTimeout.toMillis + 200)
|
||||
|
||||
Get("/divide/10/2") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The result was 5"
|
||||
}
|
||||
}
|
||||
|
||||
"onSuccess" in {
|
||||
val route =
|
||||
path("success") {
|
||||
onSuccess(Future { "Ok" }) { extraction =>
|
||||
complete(extraction)
|
||||
}
|
||||
} ~
|
||||
path("failure") {
|
||||
onSuccess(Future.failed[String](TestException)) { extraction =>
|
||||
complete(extraction)
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/success") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Ok"
|
||||
}
|
||||
|
||||
Get("/failure") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual InternalServerError
|
||||
responseAs[String] shouldEqual "Unsuccessful future!"
|
||||
}
|
||||
}
|
||||
|
||||
"completeOrRecoverWith" in {
|
||||
val route =
|
||||
path("success") {
|
||||
completeOrRecoverWith(Future { "Ok" }) { extraction =>
|
||||
failWith(extraction) // not executed.
|
||||
}
|
||||
} ~
|
||||
path("failure") {
|
||||
completeOrRecoverWith(Future.failed[String](TestException)) { extraction =>
|
||||
failWith(extraction)
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/success") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Ok"
|
||||
}
|
||||
|
||||
Get("/failure") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual InternalServerError
|
||||
responseAs[String] shouldEqual "Unsuccessful future!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.{ InvalidOriginRejection, MissingHeaderRejection, Route }
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import org.scalatest.Inside
|
||||
|
||||
class HeaderDirectivesExamplesSpec extends RoutingSpec with Inside {
|
||||
"headerValueByName-0" in {
|
||||
val route =
|
||||
headerValueByName("X-User-Id") { userId =>
|
||||
complete(s"The user is $userId")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> RawHeader("X-User-Id", "Joe42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The user is Joe42"
|
||||
}
|
||||
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "Request is missing required HTTP header 'X-User-Id'"
|
||||
}
|
||||
}
|
||||
"headerValue-0" in {
|
||||
def extractHostPort: HttpHeader => Option[Int] = {
|
||||
case h: `Host` => Some(h.port)
|
||||
case x => None
|
||||
}
|
||||
|
||||
val route =
|
||||
headerValue(extractHostPort) { port =>
|
||||
complete(s"The port was $port")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> Host("example.com", 5043) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The port was 5043"
|
||||
}
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual NotFound
|
||||
responseAs[String] shouldEqual "The requested resource could not be found."
|
||||
}
|
||||
}
|
||||
"optionalHeaderValue-0" in {
|
||||
def extractHostPort: HttpHeader => Option[Int] = {
|
||||
case h: `Host` => Some(h.port)
|
||||
case x => None
|
||||
}
|
||||
|
||||
val route =
|
||||
optionalHeaderValue(extractHostPort) {
|
||||
case Some(port) => complete(s"The port was $port")
|
||||
case None => complete(s"The port was not provided explicitly")
|
||||
} ~ // can also be written as:
|
||||
optionalHeaderValue(extractHostPort) { port =>
|
||||
complete {
|
||||
port match {
|
||||
case Some(p) => s"The port was $p"
|
||||
case _ => "The port was not provided explicitly"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> Host("example.com", 5043) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The port was 5043"
|
||||
}
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] shouldEqual "The port was not provided explicitly"
|
||||
}
|
||||
}
|
||||
"optionalHeaderValueByName-0" in {
|
||||
val route =
|
||||
optionalHeaderValueByName("X-User-Id") {
|
||||
case Some(userId) => complete(s"The user is $userId")
|
||||
case None => complete(s"No user was provided")
|
||||
} ~ // can also be written as:
|
||||
optionalHeaderValueByName("port") { port =>
|
||||
complete {
|
||||
port match {
|
||||
case Some(p) => s"The user is $p"
|
||||
case _ => "No user was provided"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> RawHeader("X-User-Id", "Joe42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The user is Joe42"
|
||||
}
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] shouldEqual "No user was provided"
|
||||
}
|
||||
}
|
||||
"headerValuePF-0" in {
|
||||
def extractHostPort: PartialFunction[HttpHeader, Int] = {
|
||||
case h: `Host` => h.port
|
||||
}
|
||||
|
||||
val route =
|
||||
headerValuePF(extractHostPort) { port =>
|
||||
complete(s"The port was $port")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> Host("example.com", 5043) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The port was 5043"
|
||||
}
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual NotFound
|
||||
responseAs[String] shouldEqual "The requested resource could not be found."
|
||||
}
|
||||
}
|
||||
"optionalHeaderValuePF-0" in {
|
||||
def extractHostPort: PartialFunction[HttpHeader, Int] = {
|
||||
case h: `Host` => h.port
|
||||
}
|
||||
|
||||
val route =
|
||||
optionalHeaderValuePF(extractHostPort) {
|
||||
case Some(port) => complete(s"The port was $port")
|
||||
case None => complete(s"The port was not provided explicitly")
|
||||
} ~ // can also be written as:
|
||||
optionalHeaderValuePF(extractHostPort) { port =>
|
||||
complete {
|
||||
port match {
|
||||
case Some(p) => s"The port was $p"
|
||||
case _ => "The port was not provided explicitly"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> Host("example.com", 5043) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The port was 5043"
|
||||
}
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] shouldEqual "The port was not provided explicitly"
|
||||
}
|
||||
}
|
||||
"headerValueByType-0" in {
|
||||
val route =
|
||||
headerValueByType[Origin]() { origin =>
|
||||
complete(s"The first origin was ${origin.origins.head}")
|
||||
}
|
||||
|
||||
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
|
||||
|
||||
// tests:
|
||||
// extract a header if the type is matching
|
||||
Get("abc") ~> originHeader ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The first origin was http://localhost:8080"
|
||||
}
|
||||
|
||||
// reject a request if no header of the given type is present
|
||||
Get("abc") ~> route ~> check {
|
||||
inside(rejection) { case MissingHeaderRejection("Origin") => }
|
||||
}
|
||||
}
|
||||
"optionalHeaderValueByType-0" in {
|
||||
val route =
|
||||
optionalHeaderValueByType[Origin]() {
|
||||
case Some(origin) => complete(s"The first origin was ${origin.origins.head}")
|
||||
case None => complete("No Origin header found.")
|
||||
}
|
||||
|
||||
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
|
||||
|
||||
// tests:
|
||||
// extract Some(header) if the type is matching
|
||||
Get("abc") ~> originHeader ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The first origin was http://localhost:8080"
|
||||
}
|
||||
|
||||
// extract None if no header of the given type is present
|
||||
Get("abc") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "No Origin header found."
|
||||
}
|
||||
}
|
||||
"checkSameOrigin-0" in {
|
||||
val correctOrigin = HttpOrigin("http://localhost:8080")
|
||||
val route = checkSameOrigin(HttpOriginRange(correctOrigin)) {
|
||||
complete("Result")
|
||||
}
|
||||
|
||||
// tests:
|
||||
// handle request with correct origin headers
|
||||
Get("abc") ~> Origin(correctOrigin) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Result"
|
||||
}
|
||||
|
||||
// reject request with missed origin header
|
||||
Get("abc") ~> route ~> check {
|
||||
inside(rejection) {
|
||||
case MissingHeaderRejection(headerName) ⇒ headerName shouldEqual Origin.name
|
||||
}
|
||||
}
|
||||
|
||||
// rejects request with invalid origin headers
|
||||
val invalidHttpOrigin = HttpOrigin("http://invalid.com")
|
||||
val invalidOriginHeader = Origin(invalidHttpOrigin)
|
||||
Get("abc") ~> invalidOriginHeader ~> route ~> check {
|
||||
inside(rejection) {
|
||||
case InvalidOriginRejection(allowedOrigins) ⇒
|
||||
allowedOrigins shouldEqual Seq(correctOrigin)
|
||||
}
|
||||
}
|
||||
Get("abc") ~> invalidOriginHeader ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.Forbidden
|
||||
responseAs[String] should include(s"${correctOrigin.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
|
||||
class HostDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"extractHost" in {
|
||||
val route =
|
||||
extractHost { hn =>
|
||||
complete(s"Hostname: $hn")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get() ~> Host("company.com", 9090) ~> route ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "Hostname: company.com"
|
||||
}
|
||||
}
|
||||
|
||||
"list-of-hosts" in {
|
||||
val route =
|
||||
host("api.company.com", "rest.company.com") {
|
||||
complete("Ok")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get() ~> Host("rest.company.com") ~> route ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "Ok"
|
||||
}
|
||||
|
||||
Get() ~> Host("notallowed.company.com") ~> route ~> check {
|
||||
handled shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"predicate" in {
|
||||
val shortOnly: String => Boolean = (hostname) => hostname.length < 10
|
||||
|
||||
val route =
|
||||
host(shortOnly) {
|
||||
complete("Ok")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get() ~> Host("short.com") ~> route ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "Ok"
|
||||
}
|
||||
|
||||
Get() ~> Host("verylonghostname.com") ~> route ~> check {
|
||||
handled shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"using-regex" in {
|
||||
val route =
|
||||
host("api|rest".r) { prefix =>
|
||||
complete(s"Extracted prefix: $prefix")
|
||||
} ~
|
||||
host("public.(my|your)company.com".r) { captured =>
|
||||
complete(s"You came through $captured company")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get() ~> Host("api.company.com") ~> route ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "Extracted prefix: api"
|
||||
}
|
||||
|
||||
Get() ~> Host("public.mycompany.com") ~> route ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "You came through my company"
|
||||
}
|
||||
}
|
||||
|
||||
"failing-regex" in {
|
||||
an[IllegalArgumentException] should be thrownBy {
|
||||
host("server-([0-9]).company.(com|net|org)".r) { target =>
|
||||
complete("Will never complete :'(")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.NotUsed
|
||||
import akka.http.scaladsl.common.{ EntityStreamingSupport, JsonEntityStreamingSupport }
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.Accept
|
||||
import akka.http.scaladsl.server.{ UnacceptedResponseContentTypeRejection, UnsupportedRequestContentTypeRejection }
|
||||
import akka.stream.scaladsl.{ Flow, Source }
|
||||
import akka.util.ByteString
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class JsonStreamingExamplesSpec extends RoutingSpec {
|
||||
|
||||
//#models
|
||||
case class Tweet(uid: Int, txt: String)
|
||||
case class Measurement(id: String, value: Int)
|
||||
//#
|
||||
|
||||
val tweets = List(
|
||||
Tweet(1, "#Akka rocks!"),
|
||||
Tweet(2, "Streaming is so hot right now!"),
|
||||
Tweet(3, "You cannot enter the same river twice."))
|
||||
def getTweets = Source(tweets)
|
||||
|
||||
//#formats
|
||||
object MyJsonProtocol
|
||||
extends akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
with spray.json.DefaultJsonProtocol {
|
||||
|
||||
implicit val tweetFormat = jsonFormat2(Tweet.apply)
|
||||
implicit val measurementFormat = jsonFormat2(Measurement.apply)
|
||||
}
|
||||
//#
|
||||
|
||||
"spray-json-response-streaming" in {
|
||||
// [1] import "my protocol", for marshalling Tweet objects:
|
||||
import MyJsonProtocol._
|
||||
|
||||
// [2] pick a Source rendering support trait:
|
||||
// Note that the default support renders the Source as JSON Array
|
||||
implicit val jsonStreamingSupport: JsonEntityStreamingSupport = EntityStreamingSupport.json()
|
||||
|
||||
val route =
|
||||
path("tweets") {
|
||||
// [3] simply complete a request with a source of tweets:
|
||||
val tweets: Source[Tweet, NotUsed] = getTweets
|
||||
complete(tweets)
|
||||
}
|
||||
|
||||
// tests ------------------------------------------------------------
|
||||
val AcceptJson = Accept(MediaRange(MediaTypes.`application/json`))
|
||||
val AcceptXml = Accept(MediaRange(MediaTypes.`text/xml`))
|
||||
|
||||
Get("/tweets").withHeaders(AcceptJson) ~> route ~> check {
|
||||
responseAs[String] shouldEqual
|
||||
"""[""" +
|
||||
"""{"uid":1,"txt":"#Akka rocks!"},""" +
|
||||
"""{"uid":2,"txt":"Streaming is so hot right now!"},""" +
|
||||
"""{"uid":3,"txt":"You cannot enter the same river twice."}""" +
|
||||
"""]"""
|
||||
}
|
||||
|
||||
// endpoint can only marshal Json, so it will *reject* requests for application/xml:
|
||||
Get("/tweets").withHeaders(AcceptXml) ~> route ~> check {
|
||||
handled should ===(false)
|
||||
rejection should ===(UnacceptedResponseContentTypeRejection(Set(ContentTypes.`application/json`)))
|
||||
}
|
||||
}
|
||||
|
||||
"line-by-line-json-response-streaming" in {
|
||||
import MyJsonProtocol._
|
||||
|
||||
// Configure the EntityStreamingSupport to render the elements as:
|
||||
// {"example":42}
|
||||
// {"example":43}
|
||||
// ...
|
||||
// {"example":1000}
|
||||
val start = ByteString.empty
|
||||
val sep = ByteString("\n")
|
||||
val end = ByteString.empty
|
||||
|
||||
implicit val jsonStreamingSupport = EntityStreamingSupport.json()
|
||||
.withFramingRenderer(Flow[ByteString].intersperse(start, sep, end))
|
||||
|
||||
val route =
|
||||
path("tweets") {
|
||||
// [3] simply complete a request with a source of tweets:
|
||||
val tweets: Source[Tweet, NotUsed] = getTweets
|
||||
complete(tweets)
|
||||
}
|
||||
|
||||
// tests ------------------------------------------------------------
|
||||
val AcceptJson = Accept(MediaRange(MediaTypes.`application/json`))
|
||||
|
||||
Get("/tweets").withHeaders(AcceptJson) ~> route ~> check {
|
||||
responseAs[String] shouldEqual
|
||||
"""{"uid":1,"txt":"#Akka rocks!"}""" + "\n" +
|
||||
"""{"uid":2,"txt":"Streaming is so hot right now!"}""" + "\n" +
|
||||
"""{"uid":3,"txt":"You cannot enter the same river twice."}"""
|
||||
}
|
||||
}
|
||||
|
||||
"csv-example" in {
|
||||
// [1] provide a marshaller to ByteString
|
||||
implicit val tweetAsCsv = Marshaller.strict[Tweet, ByteString] { t =>
|
||||
Marshalling.WithFixedContentType(ContentTypes.`text/csv(UTF-8)`, () => {
|
||||
val txt = t.txt.replaceAll(",", ".")
|
||||
val uid = t.uid
|
||||
ByteString(List(uid, txt).mkString(","))
|
||||
})
|
||||
}
|
||||
|
||||
// [2] enable csv streaming:
|
||||
implicit val csvStreaming = EntityStreamingSupport.csv()
|
||||
|
||||
val route =
|
||||
path("tweets") {
|
||||
val tweets: Source[Tweet, NotUsed] = getTweets
|
||||
complete(tweets)
|
||||
}
|
||||
|
||||
// tests ------------------------------------------------------------
|
||||
val AcceptCsv = Accept(MediaRange(MediaTypes.`text/csv`))
|
||||
|
||||
Get("/tweets").withHeaders(AcceptCsv) ~> route ~> check {
|
||||
responseAs[String] shouldEqual
|
||||
"1,#Akka rocks!" + "\n" +
|
||||
"2,Streaming is so hot right now!" + "\n" +
|
||||
"3,You cannot enter the same river twice."
|
||||
}
|
||||
}
|
||||
|
||||
"response-streaming-modes" in {
|
||||
|
||||
{
|
||||
//#async-rendering
|
||||
import MyJsonProtocol._
|
||||
implicit val jsonStreamingSupport: JsonEntityStreamingSupport =
|
||||
EntityStreamingSupport.json()
|
||||
.withParallelMarshalling(parallelism = 8, unordered = false)
|
||||
|
||||
path("tweets") {
|
||||
val tweets: Source[Tweet, NotUsed] = getTweets
|
||||
complete(tweets)
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
//#async-unordered-rendering
|
||||
import MyJsonProtocol._
|
||||
implicit val jsonStreamingSupport: JsonEntityStreamingSupport =
|
||||
EntityStreamingSupport.json()
|
||||
.withParallelMarshalling(parallelism = 8, unordered = true)
|
||||
|
||||
path("tweets" / "unordered") {
|
||||
val tweets: Source[Tweet, NotUsed] = getTweets
|
||||
complete(tweets)
|
||||
}
|
||||
//#
|
||||
}
|
||||
}
|
||||
|
||||
"spray-json-request-streaming" in {
|
||||
// [1] import "my protocol", for unmarshalling Measurement objects:
|
||||
import MyJsonProtocol._
|
||||
|
||||
// [2] enable Json Streaming
|
||||
implicit val jsonStreamingSupport = EntityStreamingSupport.json()
|
||||
|
||||
// prepare your persisting logic here
|
||||
val persistMetrics = Flow[Measurement]
|
||||
|
||||
val route =
|
||||
path("metrics") {
|
||||
// [3] extract Source[Measurement, _]
|
||||
entity(asSourceOf[Measurement]) { measurements =>
|
||||
// alternative syntax:
|
||||
// entity(as[Source[Measurement, NotUsed]]) { measurements =>
|
||||
val measurementsSubmitted: Future[Int] =
|
||||
measurements
|
||||
.via(persistMetrics)
|
||||
.runFold(0) { (cnt, _) => cnt + 1 }
|
||||
|
||||
complete {
|
||||
measurementsSubmitted.map(n => Map("msg" -> s"""Total metrics received: $n"""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests ------------------------------------------------------------
|
||||
// uploading an array or newline separated values works out of the box
|
||||
val data = HttpEntity(
|
||||
ContentTypes.`application/json`,
|
||||
"""
|
||||
|{"id":"temp","value":32}
|
||||
|{"id":"temp","value":31}
|
||||
|
|
||||
""".stripMargin)
|
||||
|
||||
Post("/metrics", entity = data) ~> route ~> check {
|
||||
status should ===(StatusCodes.OK)
|
||||
responseAs[String] should ===("""{"msg":"Total metrics received: 2"}""")
|
||||
}
|
||||
|
||||
// the FramingWithContentType will reject any content type that it does not understand:
|
||||
val xmlData = HttpEntity(
|
||||
ContentTypes.`text/xml(UTF-8)`,
|
||||
"""|<data id="temp" value="32"/>
|
||||
|<data id="temp" value="31"/>""".stripMargin)
|
||||
|
||||
Post("/metrics", entity = xmlData) ~> route ~> check {
|
||||
handled should ===(false)
|
||||
rejection should ===(UnsupportedRequestContentTypeRejection(Set(ContentTypes.`application/json`)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import akka.http.scaladsl.model.MediaTypes.`application/json`
|
||||
import akka.http.scaladsl.model._
|
||||
import spray.json.{ JsValue, DefaultJsonProtocol }
|
||||
|
||||
//# person-case-class
|
||||
case class Person(name: String, favoriteNumber: Int)
|
||||
|
||||
//# person-json-support
|
||||
object PersonJsonSupport extends DefaultJsonProtocol with SprayJsonSupport {
|
||||
implicit val PortofolioFormats = jsonFormat2(Person)
|
||||
}
|
||||
//#
|
||||
|
||||
class MarshallingDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"example-entity-with-json" in {
|
||||
import PersonJsonSupport._
|
||||
|
||||
val route = post {
|
||||
entity(as[Person]) { person =>
|
||||
complete(s"Person: ${person.name} - favorite number: ${person.favoriteNumber}")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", HttpEntity(`application/json`, """{ "name": "Jane", "favoriteNumber" : 42 }""")) ~>
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "Person: Jane - favorite number: 42"
|
||||
}
|
||||
}
|
||||
|
||||
"example-entity-with-raw-json" in {
|
||||
import PersonJsonSupport._
|
||||
|
||||
val route = post {
|
||||
entity(as[JsValue]) { json =>
|
||||
complete(s"Person: ${json.asJsObject.fields("name")} - favorite number: ${json.asJsObject.fields("favoriteNumber")}")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", HttpEntity(`application/json`, """{ "name": "Jane", "favoriteNumber" : 42 }""")) ~>
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual """Person: "Jane" - favorite number: 42"""
|
||||
}
|
||||
}
|
||||
|
||||
"example-completeWith-with-json" in {
|
||||
import PersonJsonSupport._
|
||||
|
||||
val findPerson = (f: Person => Unit) => {
|
||||
|
||||
//... some processing logic...
|
||||
|
||||
//complete the request
|
||||
f(Person("Jane", 42))
|
||||
}
|
||||
|
||||
val route = get {
|
||||
completeWith(instanceOf[Person]) { completionFunction => findPerson(completionFunction) }
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
mediaType shouldEqual `application/json`
|
||||
responseAs[String] should include(""""name":"Jane"""")
|
||||
responseAs[String] should include(""""favoriteNumber":42""")
|
||||
}
|
||||
}
|
||||
|
||||
"example-handleWith-with-json" in {
|
||||
import PersonJsonSupport._
|
||||
|
||||
val updatePerson = (person: Person) => {
|
||||
|
||||
//... some processing logic...
|
||||
|
||||
//return the person
|
||||
person
|
||||
}
|
||||
|
||||
val route = post {
|
||||
handleWith(updatePerson)
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", HttpEntity(`application/json`, """{ "name": "Jane", "favoriteNumber" : 42 }""")) ~>
|
||||
route ~> check {
|
||||
mediaType shouldEqual `application/json`
|
||||
responseAs[String] should include(""""name":"Jane"""")
|
||||
responseAs[String] should include(""""favoriteNumber":42""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class MethodDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"delete-method" in {
|
||||
val route = delete { complete("This is a DELETE request.") }
|
||||
|
||||
// tests:
|
||||
Delete("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a DELETE request."
|
||||
}
|
||||
}
|
||||
|
||||
"get-method" in {
|
||||
val route = get { complete("This is a GET request.") }
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a GET request."
|
||||
}
|
||||
}
|
||||
|
||||
"head-method" in {
|
||||
val route = head { complete("This is a HEAD request.") }
|
||||
|
||||
// tests:
|
||||
Head("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a HEAD request."
|
||||
}
|
||||
}
|
||||
|
||||
"options-method" in {
|
||||
val route = options { complete("This is an OPTIONS request.") }
|
||||
|
||||
// tests:
|
||||
Options("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is an OPTIONS request."
|
||||
}
|
||||
}
|
||||
|
||||
"patch-method" in {
|
||||
val route = patch { complete("This is a PATCH request.") }
|
||||
|
||||
// tests:
|
||||
Patch("/", "patch content") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a PATCH request."
|
||||
}
|
||||
}
|
||||
|
||||
"post-method" in {
|
||||
val route = post { complete("This is a POST request.") }
|
||||
|
||||
// tests:
|
||||
Post("/", "post content") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a POST request."
|
||||
}
|
||||
}
|
||||
|
||||
"put-method" in {
|
||||
val route = put { complete("This is a PUT request.") }
|
||||
|
||||
// tests:
|
||||
Put("/", "put content") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a PUT request."
|
||||
}
|
||||
}
|
||||
|
||||
"method-example" in {
|
||||
val route = method(HttpMethods.PUT) { complete("This is a PUT request.") }
|
||||
|
||||
// tests:
|
||||
Put("/", "put content") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a PUT request."
|
||||
}
|
||||
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.MethodNotAllowed
|
||||
responseAs[String] shouldEqual "HTTP method not allowed, supported methods: PUT"
|
||||
}
|
||||
}
|
||||
|
||||
"extractMethod-example" in {
|
||||
val route =
|
||||
get {
|
||||
complete("This is a GET request.")
|
||||
} ~
|
||||
extractMethod { method =>
|
||||
complete(s"This ${method.name} request, clearly is not a GET!")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This is a GET request."
|
||||
}
|
||||
|
||||
Put("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This PUT request, clearly is not a GET!"
|
||||
}
|
||||
Head("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This HEAD request, clearly is not a GET!"
|
||||
}
|
||||
}
|
||||
|
||||
"overrideMethodWithParameter-0" in {
|
||||
val route =
|
||||
overrideMethodWithParameter("method") {
|
||||
get {
|
||||
complete("This looks like a GET request.")
|
||||
} ~
|
||||
post {
|
||||
complete("This looks like a POST request.")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?method=POST") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This looks like a POST request."
|
||||
}
|
||||
Post("/?method=get") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "This looks like a GET request."
|
||||
}
|
||||
|
||||
Get("/?method=hallo") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.NotImplemented
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server._
|
||||
import headers._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import java.net.InetAddress
|
||||
|
||||
class MiscDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"extractClientIP-example" in {
|
||||
val route = extractClientIP { ip =>
|
||||
complete("Client's ip is " + ip.toOption.map(_.getHostAddress).getOrElse("unknown"))
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/").withHeaders(`Remote-Address`(RemoteAddress(InetAddress.getByName("192.168.3.12")))) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Client's ip is 192.168.3.12"
|
||||
}
|
||||
}
|
||||
|
||||
"rejectEmptyResponse-example" in {
|
||||
val route = rejectEmptyResponse {
|
||||
path("even" / IntNumber) { i =>
|
||||
complete {
|
||||
// returns Some(evenNumberDescription) or None
|
||||
Option(i).filter(_ % 2 == 0).map { num =>
|
||||
s"Number $num is even."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/even/23") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
}
|
||||
Get("/even/28") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Number 28 is even."
|
||||
}
|
||||
}
|
||||
|
||||
"requestEntityEmptyPresent-example" in {
|
||||
val route =
|
||||
requestEntityEmpty {
|
||||
complete("request entity empty")
|
||||
} ~
|
||||
requestEntityPresent {
|
||||
complete("request entity present")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Post("/", "text") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] shouldEqual "request entity present"
|
||||
}
|
||||
Post("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "request entity empty"
|
||||
}
|
||||
}
|
||||
|
||||
"selectPreferredLanguage-example" in {
|
||||
val request = Get() ~> `Accept-Language`(
|
||||
Language("en-US"),
|
||||
Language("en") withQValue 0.7f,
|
||||
LanguageRange.`*` withQValue 0.1f,
|
||||
Language("de") withQValue 0.5f)
|
||||
|
||||
request ~> {
|
||||
selectPreferredLanguage("en", "en-US") { lang =>
|
||||
complete(lang.toString)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "en-US" }
|
||||
|
||||
request ~> {
|
||||
selectPreferredLanguage("de-DE", "hu") { lang =>
|
||||
complete(lang.toString)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "de-DE" }
|
||||
}
|
||||
|
||||
"validate-example" in {
|
||||
val route =
|
||||
extractUri { uri =>
|
||||
validate(uri.path.toString.size < 5, s"Path too long: '${uri.path.toString}'") {
|
||||
complete(s"Full URI: $uri")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/234") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Full URI: http://example.com/234"
|
||||
}
|
||||
Get("/abcdefghijkl") ~> route ~> check {
|
||||
rejection shouldEqual ValidationRejection("Path too long: '/abcdefghijkl'", None)
|
||||
}
|
||||
}
|
||||
|
||||
"withSizeLimit-example" in {
|
||||
val route = withSizeLimit(500) {
|
||||
entity(as[String]) { _ ⇒
|
||||
complete(HttpResponse())
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
def entityOfSize(size: Int) =
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "0" * size)
|
||||
|
||||
Post("/abc", entityOfSize(500)) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
|
||||
Post("/abc", entityOfSize(501)) ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"withSizeLimit-execution-moment-example" in {
|
||||
val route = withSizeLimit(500) {
|
||||
complete(HttpResponse())
|
||||
}
|
||||
|
||||
// tests:
|
||||
def entityOfSize(size: Int) =
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "0" * size)
|
||||
|
||||
Post("/abc", entityOfSize(500)) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
|
||||
Post("/abc", entityOfSize(501)) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
"withSizeLimit-nested-example" in {
|
||||
val route =
|
||||
withSizeLimit(500) {
|
||||
withSizeLimit(800) {
|
||||
entity(as[String]) { _ ⇒
|
||||
complete(HttpResponse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
def entityOfSize(size: Int) =
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "0" * size)
|
||||
Post("/abc", entityOfSize(800)) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
|
||||
Post("/abc", entityOfSize(801)) ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
}
|
||||
}
|
||||
|
||||
"withoutSizeLimit-example" in {
|
||||
val route =
|
||||
withoutSizeLimit {
|
||||
entity(as[String]) { _ ⇒
|
||||
complete(HttpResponse())
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
def entityOfSize(size: Int) =
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "0" * size)
|
||||
|
||||
// will work even if you have configured akka.http.parsing.max-content-length = 500
|
||||
Post("/abc", entityOfSize(501)) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.unmarshalling.PredefinedFromStringUnmarshallers
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class ParameterDirectivesExamplesSpec extends RoutingSpec with PredefinedFromStringUnmarshallers {
|
||||
"example-1" in {
|
||||
val route =
|
||||
parameter('color) { color =>
|
||||
complete(s"The color is '$color'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue'"
|
||||
}
|
||||
|
||||
Get("/") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[String] shouldEqual "Request is missing required query parameter 'color'"
|
||||
}
|
||||
}
|
||||
"required-1" in {
|
||||
val route =
|
||||
parameters('color, 'backgroundColor) { (color, backgroundColor) =>
|
||||
complete(s"The color is '$color' and the background is '$backgroundColor'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&backgroundColor=red") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and the background is 'red'"
|
||||
}
|
||||
Get("/?color=blue") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[String] shouldEqual "Request is missing required query parameter 'backgroundColor'"
|
||||
}
|
||||
}
|
||||
"optional" in {
|
||||
val route =
|
||||
parameters('color, 'backgroundColor.?) { (color, backgroundColor) =>
|
||||
val backgroundStr = backgroundColor.getOrElse("<undefined>")
|
||||
complete(s"The color is '$color' and the background is '$backgroundStr'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&backgroundColor=red") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and the background is 'red'"
|
||||
}
|
||||
Get("/?color=blue") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and the background is '<undefined>'"
|
||||
}
|
||||
}
|
||||
"optional-with-default" in {
|
||||
val route =
|
||||
parameters('color, 'backgroundColor ? "white") { (color, backgroundColor) =>
|
||||
complete(s"The color is '$color' and the background is '$backgroundColor'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&backgroundColor=red") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and the background is 'red'"
|
||||
}
|
||||
Get("/?color=blue") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and the background is 'white'"
|
||||
}
|
||||
}
|
||||
"required-value" in {
|
||||
val route =
|
||||
parameters('color, 'action ! "true") { (color) =>
|
||||
complete(s"The color is '$color'.")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&action=true") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue'."
|
||||
}
|
||||
|
||||
Get("/?color=blue&action=false") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[String] shouldEqual "The requested resource could not be found."
|
||||
}
|
||||
}
|
||||
"mapped-value" in {
|
||||
val route =
|
||||
parameters('color, 'count.as[Int]) { (color, count) =>
|
||||
complete(s"The color is '$color' and you have $count of it.")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&count=42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The color is 'blue' and you have 42 of it."
|
||||
}
|
||||
|
||||
Get("/?color=blue&count=blub") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[String] shouldEqual "The query parameter 'count' was malformed:\n'blub' is not a valid 32-bit signed integer value"
|
||||
}
|
||||
}
|
||||
"repeated" in {
|
||||
val route =
|
||||
parameters('color, 'city.*) { (color, cities) =>
|
||||
cities.toList match {
|
||||
case Nil => complete(s"The color is '$color' and there are no cities.")
|
||||
case city :: Nil => complete(s"The color is '$color' and the city is $city.")
|
||||
case multiple => complete(s"The color is '$color' and the cities are ${multiple.mkString(", ")}.")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue") ~> route ~> check {
|
||||
responseAs[String] === "The color is 'blue' and there are no cities."
|
||||
}
|
||||
|
||||
Get("/?color=blue&city=Chicago") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] === "The color is 'blue' and the city is Chicago."
|
||||
}
|
||||
|
||||
Get("/?color=blue&city=Chicago&city=Boston") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] === "The color is 'blue' and the cities are Chicago, Boston."
|
||||
}
|
||||
}
|
||||
"mapped-repeated" in {
|
||||
val route =
|
||||
parameters('color, 'distance.as[Int].*) { (color, cities) =>
|
||||
cities.toList match {
|
||||
case Nil => complete(s"The color is '$color' and there are no distances.")
|
||||
case distance :: Nil => complete(s"The color is '$color' and the distance is $distance.")
|
||||
case multiple => complete(s"The color is '$color' and the distances are ${multiple.mkString(", ")}.")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue") ~> route ~> check {
|
||||
responseAs[String] === "The color is 'blue' and there are no distances."
|
||||
}
|
||||
|
||||
Get("/?color=blue&distance=5") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] === "The color is 'blue' and the distance is 5."
|
||||
}
|
||||
|
||||
Get("/?color=blue&distance=5&distance=14") ~> Route.seal(route) ~> check {
|
||||
responseAs[String] === "The color is 'blue' and the distances are 5, 14."
|
||||
}
|
||||
}
|
||||
"parameterMap" in {
|
||||
val route =
|
||||
parameterMap { params =>
|
||||
def paramString(param: (String, String)): String = s"""${param._1} = '${param._2}'"""
|
||||
complete(s"The parameters are ${params.map(paramString).mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&count=42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are color = 'blue', count = '42'"
|
||||
}
|
||||
Get("/?x=1&x=2") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are x = '2'"
|
||||
}
|
||||
}
|
||||
"parameterMultiMap" in {
|
||||
val route =
|
||||
parameterMultiMap { params =>
|
||||
complete(s"There are parameters ${params.map(x => x._1 + " -> " + x._2.size).mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&count=42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "There are parameters color -> 1, count -> 1"
|
||||
}
|
||||
Get("/?x=23&x=42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "There are parameters x -> 2"
|
||||
}
|
||||
}
|
||||
"parameterSeq" in {
|
||||
val route =
|
||||
parameterSeq { params =>
|
||||
def paramString(param: (String, String)): String = s"""${param._1} = '${param._2}'"""
|
||||
complete(s"The parameters are ${params.map(paramString).mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?color=blue&count=42") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are color = 'blue', count = '42'"
|
||||
}
|
||||
Get("/?x=1&x=2") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are x = '1', x = '2'"
|
||||
}
|
||||
}
|
||||
"csv" in {
|
||||
val route =
|
||||
parameter("names".as(CsvSeq[String])) { names =>
|
||||
complete(s"The parameters are ${names.mkString(", ")}")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/?names=") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are "
|
||||
}
|
||||
Get("/?names=Caplin") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are Caplin"
|
||||
}
|
||||
Get("/?names=Caplin,John") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The parameters are Caplin, John"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class PathDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
//# path-matcher
|
||||
val matcher: PathMatcher1[Option[Int]] =
|
||||
"foo" / "bar" / "X" ~ IntNumber.? / ("edit" | "create")
|
||||
//#
|
||||
|
||||
//# path-dsl
|
||||
// matches /foo/
|
||||
path("foo"./)
|
||||
|
||||
// matches e.g. /foo/123 and extracts "123" as a String
|
||||
path("foo" / """\d+""".r)
|
||||
|
||||
// matches e.g. /foo/bar123 and extracts "123" as a String
|
||||
path("foo" / """bar(\d+)""".r)
|
||||
|
||||
// similar to `path(Segments)`
|
||||
path(Segment.repeat(10, separator = Slash))
|
||||
|
||||
// matches e.g. /i42 or /hCAFE and extracts an Int
|
||||
path("i" ~ IntNumber | "h" ~ HexIntNumber)
|
||||
|
||||
// identical to path("foo" ~ (PathEnd | Slash))
|
||||
path("foo" ~ Slash.?)
|
||||
|
||||
// matches /red or /green or /blue and extracts 1, 2 or 3 respectively
|
||||
path(Map("red" -> 1, "green" -> 2, "blue" -> 3))
|
||||
|
||||
// matches anything starting with "/foo" except for /foobar
|
||||
pathPrefix("foo" ~ !"bar")
|
||||
//#
|
||||
|
||||
//# pathPrefixTest-, rawPathPrefix-, rawPathPrefixTest-, pathSuffix-, pathSuffixTest-
|
||||
val completeWithUnmatchedPath =
|
||||
extractUnmatchedPath { p =>
|
||||
complete(p.toString)
|
||||
}
|
||||
|
||||
//#
|
||||
|
||||
"path-example" in {
|
||||
val route =
|
||||
path("foo") {
|
||||
complete("/foo")
|
||||
} ~
|
||||
path("foo" / "bar") {
|
||||
complete("/foo/bar")
|
||||
} ~
|
||||
pathPrefix("ball") {
|
||||
pathEnd {
|
||||
complete("/ball")
|
||||
} ~
|
||||
path(IntNumber) { int =>
|
||||
complete(if (int % 2 == 0) "even ball" else "odd ball")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
|
||||
Get("/foo") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo"
|
||||
}
|
||||
|
||||
Get("/foo/bar") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo/bar"
|
||||
}
|
||||
|
||||
Get("/ball/1337") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "odd ball"
|
||||
}
|
||||
}
|
||||
|
||||
"pathEnd-" in {
|
||||
val route =
|
||||
pathPrefix("foo") {
|
||||
pathEnd {
|
||||
complete("/foo")
|
||||
} ~
|
||||
path("bar") {
|
||||
complete("/foo/bar")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo"
|
||||
}
|
||||
|
||||
Get("/foo/") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
|
||||
Get("/foo/bar") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo/bar"
|
||||
}
|
||||
}
|
||||
|
||||
"pathEndOrSingleSlash-" in {
|
||||
val route =
|
||||
pathPrefix("foo") {
|
||||
pathEndOrSingleSlash {
|
||||
complete("/foo")
|
||||
} ~
|
||||
path("bar") {
|
||||
complete("/foo/bar")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo"
|
||||
}
|
||||
|
||||
Get("/foo/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo"
|
||||
}
|
||||
|
||||
Get("/foo/bar") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/foo/bar"
|
||||
}
|
||||
}
|
||||
|
||||
"pathPrefix-" in {
|
||||
val route =
|
||||
pathPrefix("ball") {
|
||||
pathEnd {
|
||||
complete("/ball")
|
||||
} ~
|
||||
path(IntNumber) { int =>
|
||||
complete(if (int % 2 == 0) "even ball" else "odd ball")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
|
||||
Get("/ball") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/ball"
|
||||
}
|
||||
|
||||
Get("/ball/1337") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "odd ball"
|
||||
}
|
||||
}
|
||||
|
||||
"pathPrefixTest-" in {
|
||||
val route =
|
||||
pathPrefixTest("foo" | "bar") {
|
||||
pathPrefix("foo") { completeWithUnmatchedPath } ~
|
||||
pathPrefix("bar") { completeWithUnmatchedPath }
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo/doo") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/doo"
|
||||
}
|
||||
|
||||
Get("/bar/yes") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/yes"
|
||||
}
|
||||
}
|
||||
|
||||
"pathSingleSlash-" in {
|
||||
val route =
|
||||
pathSingleSlash {
|
||||
complete("root")
|
||||
} ~
|
||||
pathPrefix("ball") {
|
||||
pathSingleSlash {
|
||||
complete("/ball/")
|
||||
} ~
|
||||
path(IntNumber) { int =>
|
||||
complete(if (int % 2 == 0) "even ball" else "odd ball")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "root"
|
||||
}
|
||||
|
||||
Get("/ball") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
|
||||
Get("/ball/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/ball/"
|
||||
}
|
||||
|
||||
Get("/ball/1337") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "odd ball"
|
||||
}
|
||||
}
|
||||
|
||||
"pathSuffix-" in {
|
||||
val route =
|
||||
pathPrefix("start") {
|
||||
pathSuffix("end") {
|
||||
completeWithUnmatchedPath
|
||||
} ~
|
||||
pathSuffix("foo" / "bar" ~ "baz") {
|
||||
completeWithUnmatchedPath
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/start/middle/end") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/middle/"
|
||||
}
|
||||
|
||||
Get("/start/something/barbaz/foo") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/something/"
|
||||
}
|
||||
}
|
||||
|
||||
"pathSuffixTest-" in {
|
||||
val route =
|
||||
pathSuffixTest(Slash) {
|
||||
complete("slashed")
|
||||
} ~
|
||||
complete("unslashed")
|
||||
|
||||
// tests:
|
||||
Get("/foo/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "slashed"
|
||||
}
|
||||
Get("/foo") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "unslashed"
|
||||
}
|
||||
}
|
||||
|
||||
"rawPathPrefix-" in {
|
||||
val route =
|
||||
pathPrefix("foo") {
|
||||
rawPathPrefix("bar") { completeWithUnmatchedPath } ~
|
||||
rawPathPrefix("doo") { completeWithUnmatchedPath }
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foobar/baz") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/baz"
|
||||
}
|
||||
|
||||
Get("/foodoo/baz") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "/baz"
|
||||
}
|
||||
}
|
||||
|
||||
"rawPathPrefixTest-" in {
|
||||
val route =
|
||||
pathPrefix("foo") {
|
||||
rawPathPrefixTest("bar") {
|
||||
completeWithUnmatchedPath
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foobar") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "bar"
|
||||
}
|
||||
|
||||
Get("/foobaz") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
}
|
||||
|
||||
"redirectToTrailingSlashIfMissing-0" in {
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
|
||||
val route =
|
||||
redirectToTrailingSlashIfMissing(StatusCodes.MovedPermanently) {
|
||||
path("foo"./) {
|
||||
// We require the explicit trailing slash in the path
|
||||
complete("OK")
|
||||
} ~
|
||||
path("bad-1") {
|
||||
// MISTAKE!
|
||||
// Missing `/` in path, causes this path to never match,
|
||||
// because it is inside a `redirectToTrailingSlashIfMissing`
|
||||
???
|
||||
} ~
|
||||
path("bad-2/") {
|
||||
// MISTAKE!
|
||||
// / should be explicit as path element separator and not *in* the path element
|
||||
// So it should be: "bad-1" /
|
||||
???
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
// Redirected:
|
||||
Get("/foo") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.MovedPermanently
|
||||
|
||||
// results in nice human readable message,
|
||||
// in case the redirect can't be followed automatically:
|
||||
responseAs[String] shouldEqual {
|
||||
"This and all future requests should be directed to " +
|
||||
"<a href=\"http://example.com/foo/\">this URI</a>."
|
||||
}
|
||||
}
|
||||
|
||||
// Properly handled:
|
||||
Get("/foo/") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "OK"
|
||||
}
|
||||
|
||||
// MISTAKE! will never match - reason explained in routes
|
||||
Get("/bad-1/") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
|
||||
// MISTAKE! will never match - reason explained in routes
|
||||
Get("/bad-2/") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
}
|
||||
|
||||
"redirectToNoTrailingSlashIfPresent-0" in {
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
|
||||
val route =
|
||||
redirectToNoTrailingSlashIfPresent(StatusCodes.MovedPermanently) {
|
||||
path("foo") {
|
||||
// We require the explicit trailing slash in the path
|
||||
complete("OK")
|
||||
} ~
|
||||
path("bad"./) {
|
||||
// MISTAKE!
|
||||
// Since inside a `redirectToNoTrailingSlashIfPresent` directive
|
||||
// the matched path here will never contain a trailing slash,
|
||||
// thus this path will never match.
|
||||
//
|
||||
// It should be `path("bad")` instead.
|
||||
???
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
// Redirected:
|
||||
Get("/foo/") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.MovedPermanently
|
||||
|
||||
// results in nice human readable message,
|
||||
// in case the redirect can't be followed automatically:
|
||||
responseAs[String] shouldEqual {
|
||||
"This and all future requests should be directed to " +
|
||||
"<a href=\"http://example.com/foo\">this URI</a>."
|
||||
}
|
||||
}
|
||||
|
||||
// Properly handled:
|
||||
Get("/foo") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "OK"
|
||||
}
|
||||
|
||||
// MISTAKE! will never match - reason explained in routes
|
||||
Get("/bad") ~> route ~> check {
|
||||
handled shouldEqual false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import com.typesafe.config.{ ConfigFactory, Config }
|
||||
import akka.util.ByteString
|
||||
|
||||
import headers._
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class RangeDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
override def testConfig: Config =
|
||||
ConfigFactory.parseString("akka.http.routing.range-coalescing-threshold=2").withFallback(super.testConfig)
|
||||
|
||||
"withRangeSupport" in {
|
||||
val route =
|
||||
withRangeSupport {
|
||||
complete("ABCDEFGH")
|
||||
}
|
||||
|
||||
Get() ~> addHeader(Range(ByteRange(3, 4))) ~> route ~> check {
|
||||
headers should contain(`Content-Range`(ContentRange(3, 4, 8)))
|
||||
status shouldEqual StatusCodes.PartialContent
|
||||
responseAs[String] shouldEqual "DE"
|
||||
}
|
||||
|
||||
// we set "akka.http.routing.range-coalescing-threshold = 2"
|
||||
// above to make sure we get two BodyParts
|
||||
Get() ~> addHeader(Range(ByteRange(0, 1), ByteRange(1, 2), ByteRange(6, 7))) ~> route ~> check {
|
||||
headers.collectFirst { case `Content-Range`(_, _) => true } shouldBe None
|
||||
val responseF = responseAs[Multipart.ByteRanges].parts
|
||||
.runFold[List[Multipart.ByteRanges.BodyPart]](Nil)((acc, curr) => curr :: acc)
|
||||
|
||||
val response = Await.result(responseF, 3.seconds).reverse
|
||||
|
||||
response should have length 2
|
||||
|
||||
val part1 = response(0)
|
||||
part1.contentRange === ContentRange(0, 2, 8)
|
||||
part1.entity should matchPattern {
|
||||
case HttpEntity.Strict(_, bytes) if bytes.utf8String == "ABC" =>
|
||||
}
|
||||
|
||||
val part2 = response(1)
|
||||
part2.contentRange === ContentRange(6, 7, 8)
|
||||
part2.entity should matchPattern {
|
||||
case HttpEntity.Strict(_, bytes) if bytes.utf8String == "GH" =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class RespondWithDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"respondWithHeader-0" in {
|
||||
val route =
|
||||
path("foo") {
|
||||
respondWithHeader(RawHeader("Funky-Muppet", "gonzo")) {
|
||||
complete("beep")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo") ~> route ~> check {
|
||||
header("Funky-Muppet") shouldEqual Some(RawHeader("Funky-Muppet", "gonzo"))
|
||||
responseAs[String] shouldEqual "beep"
|
||||
}
|
||||
}
|
||||
|
||||
"respondWithDefaultHeader-0" in {
|
||||
// custom headers
|
||||
val blippy = RawHeader("X-Fish-Name", "Blippy")
|
||||
val elTonno = RawHeader("X-Fish-Name", "El Tonno")
|
||||
|
||||
// format: OFF
|
||||
// by default always include the Blippy header,
|
||||
// unless a more specific X-Fish-Name is given by the inner route
|
||||
val route =
|
||||
respondWithDefaultHeader(blippy) { // blippy
|
||||
respondWithHeader(elTonno) { // / el tonno
|
||||
path("el-tonno") { // | /
|
||||
complete("¡Ay blippy!") // | |- el tonno
|
||||
} ~ // | |
|
||||
path("los-tonnos") { // | |
|
||||
complete("¡Ay ay blippy!") // | |- el tonno
|
||||
} // | |
|
||||
} ~ // | x
|
||||
complete("Blip!") // |- blippy
|
||||
} // x
|
||||
// format: ON
|
||||
|
||||
// tests:
|
||||
Get("/") ~> route ~> check {
|
||||
header("X-Fish-Name") shouldEqual Some(RawHeader("X-Fish-Name", "Blippy"))
|
||||
responseAs[String] shouldEqual "Blip!"
|
||||
}
|
||||
|
||||
Get("/el-tonno") ~> route ~> check {
|
||||
header("X-Fish-Name") shouldEqual Some(RawHeader("X-Fish-Name", "El Tonno"))
|
||||
responseAs[String] shouldEqual "¡Ay blippy!"
|
||||
}
|
||||
|
||||
Get("/los-tonnos") ~> route ~> check {
|
||||
header("X-Fish-Name") shouldEqual Some(RawHeader("X-Fish-Name", "El Tonno"))
|
||||
responseAs[String] shouldEqual "¡Ay ay blippy!"
|
||||
}
|
||||
}
|
||||
// format: ON
|
||||
|
||||
"respondWithHeaders-0" in {
|
||||
val route =
|
||||
path("foo") {
|
||||
respondWithHeaders(RawHeader("Funky-Muppet", "gonzo"), Origin(HttpOrigin("http://akka.io"))) {
|
||||
complete("beep")
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo") ~> route ~> check {
|
||||
header("Funky-Muppet") shouldEqual Some(RawHeader("Funky-Muppet", "gonzo"))
|
||||
header[Origin] shouldEqual Some(Origin(HttpOrigin("http://akka.io")))
|
||||
responseAs[String] shouldEqual "beep"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.ContentTypes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.`Content-Type`
|
||||
import akka.http.scaladsl.server.{ Route, ValidationRejection }
|
||||
import akka.testkit.EventFilter
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class RouteDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"complete-examples" in {
|
||||
val route =
|
||||
path("a") {
|
||||
complete(HttpResponse(entity = "foo"))
|
||||
} ~
|
||||
path("b") {
|
||||
complete(StatusCodes.OK)
|
||||
} ~
|
||||
path("c") {
|
||||
complete(StatusCodes.Created -> "bar")
|
||||
} ~
|
||||
path("d") {
|
||||
complete(201 -> "bar")
|
||||
} ~
|
||||
path("e") {
|
||||
complete(StatusCodes.Created, List(`Content-Type`(`text/plain(UTF-8)`)), "bar")
|
||||
} ~
|
||||
path("f") {
|
||||
complete(201, List(`Content-Type`(`text/plain(UTF-8)`)), "bar")
|
||||
} ~
|
||||
(path("g") & complete("baz")) // `&` also works with `complete` as the 2nd argument
|
||||
|
||||
// tests:
|
||||
Get("/a") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "foo"
|
||||
}
|
||||
|
||||
Get("/b") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "OK"
|
||||
}
|
||||
|
||||
Get("/c") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Created
|
||||
responseAs[String] shouldEqual "bar"
|
||||
}
|
||||
|
||||
Get("/d") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Created
|
||||
responseAs[String] shouldEqual "bar"
|
||||
}
|
||||
|
||||
Get("/e") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Created
|
||||
header[`Content-Type`] shouldEqual Some(`Content-Type`(`text/plain(UTF-8)`))
|
||||
responseAs[String] shouldEqual "bar"
|
||||
}
|
||||
|
||||
Get("/f") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Created
|
||||
header[`Content-Type`] shouldEqual Some(`Content-Type`(`text/plain(UTF-8)`))
|
||||
responseAs[String] shouldEqual "bar"
|
||||
}
|
||||
|
||||
Get("/g") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "baz"
|
||||
}
|
||||
}
|
||||
|
||||
"reject-examples" in {
|
||||
val route =
|
||||
path("a") {
|
||||
reject // don't handle here, continue on
|
||||
} ~
|
||||
path("a") {
|
||||
complete("foo")
|
||||
} ~
|
||||
path("b") {
|
||||
// trigger a ValidationRejection explicitly
|
||||
// rather than through the `validate` directive
|
||||
reject(ValidationRejection("Restricted!"))
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/a") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "foo"
|
||||
}
|
||||
|
||||
Get("/b") ~> route ~> check {
|
||||
rejection shouldEqual ValidationRejection("Restricted!")
|
||||
}
|
||||
}
|
||||
|
||||
"redirect-examples" in {
|
||||
val route =
|
||||
pathPrefix("foo") {
|
||||
pathSingleSlash {
|
||||
complete("yes")
|
||||
} ~
|
||||
pathEnd {
|
||||
redirect("/foo/", StatusCodes.PermanentRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "yes"
|
||||
}
|
||||
|
||||
Get("/foo") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.PermanentRedirect
|
||||
responseAs[String] shouldEqual """The request, and all future requests should be repeated using <a href="/foo/">this URI</a>."""
|
||||
}
|
||||
}
|
||||
|
||||
"failwith-examples" in EventFilter[RuntimeException](start = "Error during processing of request", occurrences = 1).intercept {
|
||||
val route =
|
||||
path("foo") {
|
||||
failWith(new RuntimeException("Oops."))
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/foo") ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class SchemeDirectivesExamplesSpec extends RoutingSpec {
|
||||
"example-1" in {
|
||||
val route =
|
||||
extractScheme { scheme =>
|
||||
complete(s"The scheme is '${scheme}'")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("https://www.example.com/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The scheme is 'https'"
|
||||
}
|
||||
}
|
||||
|
||||
"example-2" in {
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.Location
|
||||
import StatusCodes.MovedPermanently
|
||||
|
||||
val route =
|
||||
scheme("http") {
|
||||
extract(_.request.uri) { uri =>
|
||||
redirect(uri.copy(scheme = "https"), MovedPermanently)
|
||||
}
|
||||
} ~
|
||||
scheme("https") {
|
||||
complete(s"Safe and secure!")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("http://www.example.com/hello") ~> route ~> check {
|
||||
status shouldEqual MovedPermanently
|
||||
header[Location] shouldEqual Some(Location(Uri("https://www.example.com/hello")))
|
||||
}
|
||||
|
||||
Get("https://www.example.com/hello") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Safe and secure!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.server.directives.Credentials
|
||||
|
||||
import scala.concurrent.Future
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
class SecurityDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
"authenticateBasic-0" in {
|
||||
def myUserPassAuthenticator(credentials: Credentials): Option[String] =
|
||||
credentials match {
|
||||
case p @ Credentials.Provided(id) if p.verify("p4ssw0rd") => Some(id)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
path("secured") {
|
||||
authenticateBasic(realm = "secure site", myUserPassAuthenticator) { userName =>
|
||||
complete(s"The user is '$userName'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/secured") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
|
||||
val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/secured") ~> addCredentials(validCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "The user is 'John'"
|
||||
}
|
||||
|
||||
val invalidCredentials = BasicHttpCredentials("Peter", "pan")
|
||||
Get("/secured") ~>
|
||||
addCredentials(invalidCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The supplied authentication is invalid"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
}
|
||||
"authenticateBasicPF-0" in {
|
||||
val myUserPassAuthenticator: AuthenticatorPF[String] = {
|
||||
case p @ Credentials.Provided(id) if p.verify("p4ssw0rd") => id
|
||||
case p @ Credentials.Provided(id) if p.verify("p4ssw0rd-special") => s"$id-admin"
|
||||
}
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
path("secured") {
|
||||
authenticateBasicPF(realm = "secure site", myUserPassAuthenticator) { userName =>
|
||||
complete(s"The user is '$userName'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/secured") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
|
||||
val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/secured") ~> addCredentials(validCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "The user is 'John'"
|
||||
}
|
||||
|
||||
val validAdminCredentials = BasicHttpCredentials("John", "p4ssw0rd-special")
|
||||
Get("/secured") ~> addCredentials(validAdminCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "The user is 'John-admin'"
|
||||
}
|
||||
|
||||
val invalidCredentials = BasicHttpCredentials("Peter", "pan")
|
||||
Get("/secured") ~>
|
||||
addCredentials(invalidCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The supplied authentication is invalid"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
}
|
||||
"authenticateBasicPFAsync-0" in {
|
||||
case class User(id: String)
|
||||
def fetchUser(id: String): Future[User] = {
|
||||
// some fancy logic to obtain a User
|
||||
Future.successful(User(id))
|
||||
}
|
||||
|
||||
val myUserPassAuthenticator: AsyncAuthenticatorPF[User] = {
|
||||
case p @ Credentials.Provided(id) if p.verify("p4ssw0rd") =>
|
||||
fetchUser(id)
|
||||
}
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
path("secured") {
|
||||
authenticateBasicPFAsync(realm = "secure site", myUserPassAuthenticator) { user =>
|
||||
complete(s"The user is '${user.id}'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/secured") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
|
||||
val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/secured") ~> addCredentials(validCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "The user is 'John'"
|
||||
}
|
||||
|
||||
val invalidCredentials = BasicHttpCredentials("Peter", "pan")
|
||||
Get("/secured") ~>
|
||||
addCredentials(invalidCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The supplied authentication is invalid"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
}
|
||||
"authenticateBasicAsync-0" in {
|
||||
def myUserPassAuthenticator(credentials: Credentials): Future[Option[String]] =
|
||||
credentials match {
|
||||
case p @ Credentials.Provided(id) =>
|
||||
Future {
|
||||
// potentially
|
||||
if (p.verify("p4ssw0rd")) Some(id)
|
||||
else None
|
||||
}
|
||||
case _ => Future.successful(None)
|
||||
}
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
path("secured") {
|
||||
authenticateBasicAsync(realm = "secure site", myUserPassAuthenticator) { userName =>
|
||||
complete(s"The user is '$userName'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/secured") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
|
||||
val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/secured") ~> addCredentials(validCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "The user is 'John'"
|
||||
}
|
||||
|
||||
val invalidCredentials = BasicHttpCredentials("Peter", "pan")
|
||||
Get("/secured") ~>
|
||||
addCredentials(invalidCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The supplied authentication is invalid"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("Basic", Some("secure site"))
|
||||
}
|
||||
}
|
||||
"authenticateOrRejectWithChallenge-0" in {
|
||||
val challenge = HttpChallenge("MyAuth", Some("MyRealm"))
|
||||
|
||||
// your custom authentication logic:
|
||||
def auth(creds: HttpCredentials): Boolean = true
|
||||
|
||||
def myUserPassAuthenticator(credentials: Option[HttpCredentials]): Future[AuthenticationResult[String]] =
|
||||
Future {
|
||||
credentials match {
|
||||
case Some(creds) if auth(creds) => Right("some-user-name-from-creds")
|
||||
case _ => Left(challenge)
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
path("secured") {
|
||||
authenticateOrRejectWithChallenge(myUserPassAuthenticator _) { userName =>
|
||||
complete("Authenticated!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/secured") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`].get.challenges.head shouldEqual HttpChallenge("MyAuth", Some("MyRealm"))
|
||||
}
|
||||
|
||||
val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/secured") ~> addCredentials(validCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Authenticated!"
|
||||
}
|
||||
}
|
||||
|
||||
"0authorize-0" in {
|
||||
case class User(name: String)
|
||||
|
||||
// authenticate the user:
|
||||
def myUserPassAuthenticator(credentials: Credentials): Option[User] =
|
||||
credentials match {
|
||||
case Credentials.Provided(id) => Some(User(id))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
// check if user is authorized to perform admin actions:
|
||||
val admins = Set("Peter")
|
||||
def hasAdminPermissions(user: User): Boolean =
|
||||
admins.contains(user.name)
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
authenticateBasic(realm = "secure site", myUserPassAuthenticator) { user =>
|
||||
path("peters-lair") {
|
||||
authorize(hasAdminPermissions(user)) {
|
||||
complete(s"'${user.name}' visited Peter's lair")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
val johnsCred = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/peters-lair") ~> addCredentials(johnsCred) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.Forbidden
|
||||
responseAs[String] shouldEqual "The supplied authentication is not authorized to access this resource"
|
||||
}
|
||||
|
||||
val petersCred = BasicHttpCredentials("Peter", "pan")
|
||||
Get("/peters-lair") ~> addCredentials(petersCred) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "'Peter' visited Peter's lair"
|
||||
}
|
||||
}
|
||||
|
||||
"0authorizeAsync" in {
|
||||
case class User(name: String)
|
||||
|
||||
// authenticate the user:
|
||||
def myUserPassAuthenticator(credentials: Credentials): Option[User] =
|
||||
credentials match {
|
||||
case Credentials.Provided(id) => Some(User(id))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
// check if user is authorized to perform admin actions,
|
||||
// this could potentially be a long operation so it would return a Future
|
||||
val admins = Set("Peter")
|
||||
def hasAdminPermissions(user: User): Future[Boolean] =
|
||||
Future.successful(admins.contains(user.name))
|
||||
|
||||
val route =
|
||||
Route.seal {
|
||||
authenticateBasic(realm = "secure site", myUserPassAuthenticator) { user =>
|
||||
path("peters-lair") {
|
||||
authorizeAsync(_ => hasAdminPermissions(user)) {
|
||||
complete(s"'${user.name}' visited Peter's lair")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
val johnsCred = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/peters-lair") ~> addCredentials(johnsCred) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.Forbidden
|
||||
responseAs[String] shouldEqual "The supplied authentication is not authorized to access this resource"
|
||||
}
|
||||
|
||||
val petersCred = BasicHttpCredentials("Peter", "pan")
|
||||
Get("/peters-lair") ~> addCredentials(petersCred) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "'Peter' visited Peter's lair"
|
||||
}
|
||||
}
|
||||
|
||||
"0extractCredentials" in {
|
||||
val route =
|
||||
extractCredentials { creds =>
|
||||
complete {
|
||||
creds match {
|
||||
case Some(c) => "Credentials: " + c
|
||||
case _ => "No credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tests:
|
||||
val johnsCred = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/") ~> addCredentials(johnsCred) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
responseAs[String] shouldEqual "Credentials: Basic Sm9objpwNHNzdzByZA=="
|
||||
}
|
||||
|
||||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "No credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.{ HttpResponse, StatusCodes }
|
||||
import akka.http.scaladsl.server.Route
|
||||
import docs.CompileOnlySpec
|
||||
import akka.http.scaladsl.{ Http, TestUtils }
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.http.scaladsl.model.HttpEntity._
|
||||
import akka.http.scaladsl.model._
|
||||
import com.typesafe.config.{ Config, ConfigFactory }
|
||||
import org.scalatest.concurrent.ScalaFutures
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ Future, Promise }
|
||||
import akka.testkit.AkkaSpec
|
||||
|
||||
private[this] object TimeoutDirectivesInfiniteTimeoutTestConfig {
|
||||
val testConf: Config = ConfigFactory.parseString("""
|
||||
akka.loggers = ["akka.testkit.TestEventListener"]
|
||||
akka.loglevel = ERROR
|
||||
akka.stdout-loglevel = ERROR
|
||||
windows-connection-abort-workaround-enabled = auto
|
||||
akka.log-dead-letters = OFF
|
||||
akka.http.server.request-timeout = infinite""")
|
||||
}
|
||||
|
||||
class TimeoutDirectivesExamplesSpec extends AkkaSpec(TimeoutDirectivesInfiniteTimeoutTestConfig.testConf)
|
||||
with ScalaFutures with CompileOnlySpec {
|
||||
//#testSetup
|
||||
import system.dispatcher
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
def slowFuture(): Future[String] = Promise[String].future // TODO: move to Future.never in Scala 2.12
|
||||
|
||||
def runRoute(route: Route, routePath: String): HttpResponse = {
|
||||
val (_, hostname, port) = TestUtils.temporaryServerHostnameAndPort()
|
||||
val binding = Http().bindAndHandle(route, hostname, port)
|
||||
|
||||
val response = Http().singleRequest(HttpRequest(uri = s"http://$hostname:$port/$routePath")).futureValue
|
||||
|
||||
binding.flatMap(_.unbind()).futureValue
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
//#
|
||||
|
||||
// demonstrates that timeout is correctly set despite infinite initial value of akka.http.server.request-timeout
|
||||
"Request Timeout" should {
|
||||
"be configurable in routing layer despite infinite initial value of request-timeout" in {
|
||||
//#withRequestTimeout-plain
|
||||
val route =
|
||||
path("timeout") {
|
||||
withRequestTimeout(1.seconds) { // modifies the global akka.http.server.request-timeout for this request
|
||||
val response: Future[String] = slowFuture() // very slow
|
||||
complete(response)
|
||||
}
|
||||
}
|
||||
|
||||
// check
|
||||
runRoute(route, "timeout").status should ===(StatusCodes.ServiceUnavailable) // the timeout response
|
||||
//#
|
||||
}
|
||||
"without timeout" in compileOnlySpec {
|
||||
//#withoutRequestTimeout-1
|
||||
val route =
|
||||
path("timeout") {
|
||||
withoutRequestTimeout {
|
||||
val response: Future[String] = slowFuture() // very slow
|
||||
complete(response)
|
||||
}
|
||||
}
|
||||
|
||||
// no check as there is no time-out, the future would time out failing the test
|
||||
//#
|
||||
}
|
||||
|
||||
"allow mapping the response while setting the timeout" in {
|
||||
//#withRequestTimeout-with-handler
|
||||
val timeoutResponse = HttpResponse(
|
||||
StatusCodes.EnhanceYourCalm,
|
||||
entity = "Unable to serve response within time limit, please enhance your calm.")
|
||||
|
||||
val route =
|
||||
path("timeout") {
|
||||
// updates timeout and handler at
|
||||
withRequestTimeout(1.milli, request => timeoutResponse) {
|
||||
val response: Future[String] = slowFuture() // very slow
|
||||
complete(response)
|
||||
}
|
||||
}
|
||||
|
||||
// check
|
||||
runRoute(route, "timeout").status should ===(StatusCodes.EnhanceYourCalm) // the timeout response
|
||||
//#
|
||||
}
|
||||
|
||||
// make it compile only to avoid flaking in slow builds
|
||||
"allow mapping the response" in compileOnlySpec {
|
||||
//#withRequestTimeoutResponse
|
||||
val timeoutResponse = HttpResponse(
|
||||
StatusCodes.EnhanceYourCalm,
|
||||
entity = "Unable to serve response within time limit, please enhance your calm.")
|
||||
|
||||
val route =
|
||||
path("timeout") {
|
||||
withRequestTimeout(100.milli) { // racy! for a very short timeout like 1.milli you can still get 503
|
||||
withRequestTimeoutResponse(request => timeoutResponse) {
|
||||
val response: Future[String] = slowFuture() // very slow
|
||||
complete(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check
|
||||
runRoute(route, "timeout").status should ===(StatusCodes.EnhanceYourCalm) // the timeout response
|
||||
//#
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private[this] object TimeoutDirectivesFiniteTimeoutTestConfig {
|
||||
val testConf: Config = ConfigFactory.parseString("""
|
||||
akka.loggers = ["akka.testkit.TestEventListener"]
|
||||
akka.loglevel = ERROR
|
||||
akka.stdout-loglevel = ERROR
|
||||
windows-connection-abort-workaround-enabled = auto
|
||||
akka.log-dead-letters = OFF
|
||||
akka.http.server.request-timeout = 1000s""")
|
||||
}
|
||||
|
||||
class TimeoutDirectivesFiniteTimeoutExamplesSpec extends AkkaSpec(TimeoutDirectivesFiniteTimeoutTestConfig.testConf)
|
||||
with ScalaFutures with CompileOnlySpec {
|
||||
import system.dispatcher
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
def slowFuture(): Future[String] = Promise[String].future // TODO: move to Future.never in Scala 2.12
|
||||
|
||||
def runRoute(route: Route, routePath: String): HttpResponse = {
|
||||
val (_, hostname, port) = TestUtils.temporaryServerHostnameAndPort()
|
||||
val binding = Http().bindAndHandle(route, hostname, port)
|
||||
|
||||
val response = Http().singleRequest(HttpRequest(uri = s"http://$hostname:$port/$routePath")).futureValue
|
||||
|
||||
binding.flatMap(_.unbind()).futureValue
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
// demonstrates that timeout is correctly modified for finite initial values of akka.http.server.request-timeout
|
||||
"Request Timeout" should {
|
||||
"be configurable in routing layer for finite initial value of request-timeout" in {
|
||||
val route =
|
||||
path("timeout") {
|
||||
withRequestTimeout(1.seconds) { // modifies the global akka.http.server.request-timeout for this request
|
||||
val response: Future[String] = slowFuture() // very slow
|
||||
complete(response)
|
||||
}
|
||||
}
|
||||
runRoute(route, "timeout").status should ===(StatusCodes.ServiceUnavailable) // the timeout response
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2016 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.util.ByteString
|
||||
|
||||
import akka.stream.OverflowStrategy
|
||||
import akka.stream.scaladsl.{ Sink, Source, Flow }
|
||||
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import akka.http.scaladsl.model.ws.{ TextMessage, Message, BinaryMessage }
|
||||
import akka.http.scaladsl.testkit.WSProbe
|
||||
|
||||
class WebSocketDirectivesExamplesSpec extends RoutingSpec {
|
||||
"greeter-service" in {
|
||||
def greeter: Flow[Message, Message, Any] =
|
||||
Flow[Message].mapConcat {
|
||||
case tm: TextMessage =>
|
||||
TextMessage(Source.single("Hello ") ++ tm.textStream ++ Source.single("!")) :: Nil
|
||||
case bm: BinaryMessage =>
|
||||
// ignore binary messages but drain content to avoid the stream being clogged
|
||||
bm.dataStream.runWith(Sink.ignore)
|
||||
Nil
|
||||
}
|
||||
val websocketRoute =
|
||||
path("greeter") {
|
||||
handleWebSocketMessages(greeter)
|
||||
}
|
||||
|
||||
// tests:
|
||||
// create a testing probe representing the client-side
|
||||
val wsClient = WSProbe()
|
||||
|
||||
// WS creates a WebSocket request for testing
|
||||
WS("/greeter", wsClient.flow) ~> websocketRoute ~>
|
||||
check {
|
||||
// check response for WS Upgrade headers
|
||||
isWebSocketUpgrade shouldEqual true
|
||||
|
||||
// manually run a WS conversation
|
||||
wsClient.sendMessage("Peter")
|
||||
wsClient.expectMessage("Hello Peter!")
|
||||
|
||||
wsClient.sendMessage(BinaryMessage(ByteString("abcdef")))
|
||||
wsClient.expectNoMessage(100.millis)
|
||||
|
||||
wsClient.sendMessage("John")
|
||||
wsClient.expectMessage("Hello John!")
|
||||
|
||||
wsClient.sendCompletion()
|
||||
wsClient.expectCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
"handle-multiple-protocols" in {
|
||||
def greeterService: Flow[Message, Message, Any] =
|
||||
Flow[Message].mapConcat {
|
||||
case tm: TextMessage =>
|
||||
TextMessage(Source.single("Hello ") ++ tm.textStream ++ Source.single("!")) :: Nil
|
||||
case bm: BinaryMessage =>
|
||||
// ignore binary messages but drain content to avoid the stream being clogged
|
||||
bm.dataStream.runWith(Sink.ignore)
|
||||
Nil
|
||||
}
|
||||
|
||||
def echoService: Flow[Message, Message, Any] =
|
||||
Flow[Message]
|
||||
// needed because a noop flow hasn't any buffer that would start processing in tests
|
||||
.buffer(1, OverflowStrategy.backpressure)
|
||||
|
||||
def websocketMultipleProtocolRoute =
|
||||
path("services") {
|
||||
handleWebSocketMessagesForProtocol(greeterService, "greeter") ~
|
||||
handleWebSocketMessagesForProtocol(echoService, "echo")
|
||||
}
|
||||
|
||||
// tests:
|
||||
val wsClient = WSProbe()
|
||||
|
||||
// WS creates a WebSocket request for testing
|
||||
WS("/services", wsClient.flow, List("other", "echo")) ~>
|
||||
websocketMultipleProtocolRoute ~>
|
||||
check {
|
||||
expectWebSocketUpgradeWithProtocol { protocol =>
|
||||
protocol shouldEqual "echo"
|
||||
|
||||
wsClient.sendMessage("Peter")
|
||||
wsClient.expectMessage("Peter")
|
||||
|
||||
wsClient.sendMessage(BinaryMessage(ByteString("abcdef")))
|
||||
wsClient.expectMessage(ByteString("abcdef"))
|
||||
|
||||
wsClient.sendMessage("John")
|
||||
wsClient.expectMessage("John")
|
||||
|
||||
wsClient.sendCompletion()
|
||||
wsClient.expectCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue