+doc #19429 initial merge of docs-dev and docs
This commit is contained in:
parent
be0c8af4c0
commit
5a18d43435
501 changed files with 9876 additions and 3681 deletions
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.actor.{ ActorLogging, ActorSystem }
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class HttpClientExampleSpec extends WordSpec with Matchers {
|
||||
|
||||
"outgoing-connection-example" in {
|
||||
pending // compile-time only test
|
||||
//#outgoing-connection-example
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
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)
|
||||
//#outgoing-connection-example
|
||||
}
|
||||
|
||||
"host-level-example" in {
|
||||
pending // compile-time only test
|
||||
//#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 {
|
||||
pending // compile-time only test
|
||||
//#single-request-example
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
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 {
|
||||
pending // compile-time only test
|
||||
//#single-request-in-actor-example
|
||||
import akka.actor.Actor
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.scaladsl.ImplicitMaterializer
|
||||
|
||||
class Myself extends Actor
|
||||
with ImplicitMaterializer
|
||||
with ActorLogging {
|
||||
|
||||
import akka.pattern.pipe
|
||||
import context.dispatcher
|
||||
|
||||
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 HttpResponse(code, _, _, _) =>
|
||||
log.info("Request failed, response code: " + code)
|
||||
}
|
||||
|
||||
}
|
||||
//#single-request-in-actor-example
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,398 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.actor.{ ActorRef, ActorSystem }
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.{ Flow, Sink }
|
||||
import akka.stream.stage.{ Context, PushStage }
|
||||
import akka.testkit.TestActors
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
import scala.language.postfixOps
|
||||
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
|
||||
class HttpServerExampleSpec extends WordSpec with Matchers {
|
||||
|
||||
// never actually called
|
||||
val log: LoggingAdapter = null
|
||||
|
||||
def compileOnlySpec(body: => Unit) = ()
|
||||
|
||||
"binding-example" in compileOnlySpec {
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val ec = 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.http.scaladsl.Http
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val ec = 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: Sink[Http.IncomingConnection, Future[Http.ServerBinding]] =
|
||||
Sink.ignore.mapMaterializedValue(_ => Future.failed(new Exception("")))
|
||||
|
||||
"binding-failure-handling" in compileOnlySpec {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val ec = 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 {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val ec = 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]
|
||||
.transform { () =>
|
||||
new PushStage[IncomingConnection, IncomingConnection] {
|
||||
override def onPush(elem: IncomingConnection, ctx: Context[IncomingConnection]) =
|
||||
ctx.push(elem)
|
||||
|
||||
override def onUpstreamFailure(cause: Throwable, ctx: Context[IncomingConnection]) = {
|
||||
failureMonitor ! cause
|
||||
super.onUpstreamFailure(cause, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serverSource
|
||||
.via(reactToTopLevelFailures)
|
||||
.to(handleConnections) // Sink[Http.IncomingConnection, _]
|
||||
.run()
|
||||
}
|
||||
|
||||
"connection-stream-failure-handling" in compileOnlySpec {
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val ec = system.dispatcher
|
||||
|
||||
val (host, port) = ("localhost", 8080)
|
||||
val serverSource = Http().bind(host, port)
|
||||
|
||||
val reactToConnectionFailure = Flow[HttpRequest]
|
||||
.transform { () =>
|
||||
new PushStage[HttpRequest, HttpRequest] {
|
||||
override def onPush(elem: HttpRequest, ctx: Context[HttpRequest]) =
|
||||
ctx.push(elem)
|
||||
|
||||
override def onUpstreamFailure(cause: Throwable, ctx: Context[HttpRequest]) = {
|
||||
// handle the failure somehow
|
||||
super.onUpstreamFailure(cause, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val httpEcho = Flow[HttpRequest]
|
||||
.via(reactToConnectionFailure)
|
||||
.map { request =>
|
||||
// simple text "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.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()
|
||||
|
||||
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 _: HttpRequest =>
|
||||
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.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
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 _: HttpRequest =>
|
||||
HttpResponse(404, entity = "Unknown resource!")
|
||||
}
|
||||
|
||||
Http().bindAndHandleSync(requestHandler, "localhost", 8080)
|
||||
}
|
||||
|
||||
// format: OFF
|
||||
|
||||
"high-level-server-example" in compileOnlySpec {
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
|
||||
val route =
|
||||
get {
|
||||
pathSingleSlash {
|
||||
complete {
|
||||
<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`
|
||||
Http().bindAndHandle(route, "localhost", 8080)
|
||||
}
|
||||
|
||||
"minimal-routing-example" in compileOnlySpec {
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.stream.ActorMaterializer
|
||||
|
||||
object Main extends App {
|
||||
implicit val system = ActorSystem("my-system")
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val ec = system.dispatcher
|
||||
|
||||
val route =
|
||||
path("hello") {
|
||||
get {
|
||||
complete {
|
||||
<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...")
|
||||
Console.readLine() // for the future transformations
|
||||
bindingFuture
|
||||
.flatMap(_.unbind()) // trigger unbinding from the port
|
||||
.onComplete(_ ⇒ system.shutdown()) // and shutdown when done
|
||||
}
|
||||
}
|
||||
|
||||
"long-routing-example" in compileOnlySpec {
|
||||
//#long-routing-example
|
||||
import akka.actor.ActorRef
|
||||
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._
|
||||
// TODO: these explicit imports are only needed in complex cases, like below; Also, not needed on Scala 2.11
|
||||
import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet
|
||||
import akka.http.scaladsl.server.directives.FormFieldDirectives.FieldMagnet
|
||||
import akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller
|
||||
import akka.pattern.ask
|
||||
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" / Rest) { pathRest =>
|
||||
redirect("http://oldapi.example.com/" + pathRest, MovedPermanently)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.stream.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!
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
91
akka-docs/rst/scala/code/docs/http/scaladsl/ModelSpec.scala
Normal file
91
akka-docs/rst/scala/code/docs/http/scaladsl/ModelSpec.scala
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
//#import-model
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
//#import-model
|
||||
|
||||
import akka.stream.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 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 SprayJsonExampleSpec extends WordSpec with Matchers {
|
||||
|
||||
"spray-json example" in {
|
||||
//#example
|
||||
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
|
||||
//#
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl
|
||||
|
||||
import akka.stream.{ Materializer, ActorMaterializer }
|
||||
import akka.stream.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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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)
|
||||
}*/
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
import Directives._
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import org.scalatest._
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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!!!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import java.io.File
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.model.Multipart.FormData.BodyPart
|
||||
import akka.stream.io.{ 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.toFile(file)).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 = 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(Unit)
|
||||
}.runWith(Sink.ignore)
|
||||
|
||||
// when processing have finished create a response for the user
|
||||
onSuccess(done) {
|
||||
complete {
|
||||
"ok!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//#
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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!!!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.server.Directives
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
abstract class RoutingSpec extends WordSpec with Matchers with Directives with ScalatestRouteTest
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.model.ws.BinaryMessage
|
||||
import akka.stream.scaladsl.Sink
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class WebsocketExampleSpec extends WordSpec with Matchers {
|
||||
"core-example" in {
|
||||
pending // compile-time only test
|
||||
//#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 _: HttpRequest ⇒ 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.shutdown()) // and shutdown when done
|
||||
}
|
||||
"routing-example" in {
|
||||
pending // compile-time only test
|
||||
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
|
||||
}
|
||||
|
||||
//#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.shutdown()) // and shutdown when done
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,803 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import java.io.File
|
||||
|
||||
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.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 ec =>
|
||||
complete {
|
||||
Future(s"Run on ${ec.##}!") // uses the `ec` 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 ec =>
|
||||
complete {
|
||||
Future(s"Run on ${ec.##}!") // uses the `ec` 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).copy(fileIODispatcher = "special-io-dispatcher")
|
||||
|
||||
def sample() =
|
||||
path("sample") {
|
||||
complete {
|
||||
// internally uses the configured fileIODispatcher:
|
||||
val source = FileIO.fromFile(new File("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 { r =>
|
||||
r match {
|
||||
case Complete(response) =>
|
||||
// "Everything is OK!"
|
||||
Complete(response.copy(status = 200))
|
||||
case _ => 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 { res =>
|
||||
res match {
|
||||
case Rejected(_) => Future(Rejected(List(AuthorizationFailedRejection)))
|
||||
case _ => 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 s"RoutingSettings.renderVanityFooter = true"
|
||||
}
|
||||
//#
|
||||
}
|
||||
"mapSettings-examples" in {
|
||||
//#mapSettings-examples
|
||||
val tunedSettings = mapSettings { settings =>
|
||||
settings.copy(fileGetConditional = 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(s"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'"
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
private def compileOnlySpec(block: => Unit) = pending
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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))
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.event.Logging
|
||||
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse }
|
||||
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.toString
|
||||
DebuggingDirectives.logRequest(requestMethod _)
|
||||
|
||||
// logs just the request method at info level
|
||||
def requestMethodAsInfo(req: HttpRequest): LogEntry = LogEntry(req.method.toString, 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)
|
||||
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): Any => Option[LogEntry] = {
|
||||
case res: HttpResponse => Some(LogEntry(req.method + ":" + res.status, Logging.InfoLevel))
|
||||
case _ => None // other kind of responses
|
||||
}
|
||||
DebuggingDirectives.logRequestResult(requestMethodAndResponseStatusAsInfo _)
|
||||
|
||||
// This one doesn't use the implicit LoggingContext but uses `println` for logging
|
||||
def printRequestMethodAndResponseStatus(req: HttpRequest)(res: Any): 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: Any): String = res match {
|
||||
case x: HttpResponse => x.status.toString
|
||||
case _ => "unknown response part"
|
||||
}
|
||||
DebuggingDirectives.logResult(responseStatus _)
|
||||
|
||||
// logs just the response status at info level
|
||||
def responseStatusAsInfo(res: Any): 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: Any): Unit = println(responseStatus(res))
|
||||
val logResultPrintln = DebuggingDirectives.logResult(LoggingMagnet(_ => printResponseStatus))
|
||||
|
||||
// tests:
|
||||
Get("/") ~> logResultPrintln(complete("logged")) ~> check {
|
||||
responseAs[String] shouldEqual "logged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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(".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(".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") {
|
||||
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 =
|
||||
path("tmp") {
|
||||
getFromDirectory("/tmp")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/tmp/example") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
"getFromResourceDirectory-examples" in compileOnlySpec {
|
||||
val route =
|
||||
path("examples") {
|
||||
getFromResourceDirectory("/examples")
|
||||
}
|
||||
|
||||
// tests:
|
||||
Get("/examples/example-1") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "example file contents"
|
||||
}
|
||||
}
|
||||
|
||||
private def compileOnlySpec(block: => Unit) = pending
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Copyright (C) 2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.io.Framing
|
||||
import akka.util.ByteString
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import scala.concurrent.Future
|
||||
|
||||
class FileUploadDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
override def testConfigSource = "akka.actor.default-mailbox.mailbox-type = \"akka.dispatch.UnboundedMailbox\""
|
||||
|
||||
"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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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'"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Success, Failure }
|
||||
import akka.http.scaladsl.server.ExceptionHandler
|
||||
import akka.actor.{ Actor, Props }
|
||||
import akka.util.Timeout
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.Route
|
||||
import StatusCodes._
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
|
||||
"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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.MissingHeaderRejection
|
||||
import akka.http.scaladsl.server.Route
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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 :'(")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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.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-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""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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
|
||||
|
||||
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("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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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" =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
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.Created, "bar"))
|
||||
} ~
|
||||
(path("c") & 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.Created
|
||||
responseAs[String] shouldEqual "bar"
|
||||
}
|
||||
|
||||
Get("/c") ~> 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."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.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", "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", "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", "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", "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", "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", "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", "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", "secure site")
|
||||
}
|
||||
}
|
||||
"authenticateOrRejectWithChallenge-0" in {
|
||||
val challenge = HttpChallenge("MyAuth", "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", "MyRealm")
|
||||
}
|
||||
|
||||
val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
|
||||
Get("/secured") ~> addCredentials(validCredentials) ~> // adds Authorization header
|
||||
route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "Authenticated!"
|
||||
}
|
||||
}
|
||||
|
||||
"0authorize" 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"
|
||||
}
|
||||
}
|
||||
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.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