=ht* #17279 rename akka-http-* modules where agreed

This commit is contained in:
Mathias 2015-04-24 11:49:53 +02:00
parent 20530be054
commit 5859c39f8b
140 changed files with 330 additions and 11018 deletions

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshallers.jackson
import scala.reflect.ClassTag
import akka.http.marshalling
import akka.http.unmarshalling
import akka.http.model.MediaTypes._
import akka.http.server.japi.{ Unmarshaller, Marshaller }
import akka.http.server.japi.impl.{ UnmarshallerImpl, MarshallerImpl }
import com.fasterxml.jackson.databind.{ MapperFeature, ObjectMapper }
object Jackson {
def json[T <: AnyRef]: Marshaller[T] = _jsonMarshaller.asInstanceOf[Marshaller[T]]
def jsonAs[T](clazz: Class[T]): Unmarshaller[T] =
UnmarshallerImpl[T] { (_ec, _flowMaterializer)
implicit val ec = _ec
implicit val mat = _flowMaterializer
unmarshalling.Unmarshaller.messageUnmarshallerFromEntityUnmarshaller { // isn't implicitly inferred for unknown reasons
unmarshalling.Unmarshaller.stringUnmarshaller
.forContentTypes(`application/json`)
.map { jsonString
val reader = new ObjectMapper().reader(clazz)
clazz.cast(reader.readValue(jsonString))
}
}
}(ClassTag(clazz))
private val _jsonMarshaller: Marshaller[AnyRef] =
MarshallerImpl[AnyRef] { implicit ec
marshalling.Marshaller.StringMarshaller.wrap(`application/json`) { (value: AnyRef)
val writer = new ObjectMapper().enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY).writer()
writer.writeValueAsString(value)
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshallers.sprayjson
import scala.language.implicitConversions
import scala.concurrent.ExecutionContext
import akka.stream.FlowMaterializer
import akka.http.marshalling.{ ToEntityMarshaller, Marshaller }
import akka.http.unmarshalling.{ FromEntityUnmarshaller, Unmarshaller }
import akka.http.model.{ ContentTypes, HttpCharsets }
import akka.http.model.MediaTypes.`application/json`
import spray.json._
/**
* A trait providing automatic to and from JSON marshalling/unmarshalling using an in-scope *spray-json* protocol.
*/
trait SprayJsonSupport {
implicit def sprayJsonUnmarshallerConverter[T](reader: RootJsonReader[T])(implicit ec: ExecutionContext, mat: FlowMaterializer): FromEntityUnmarshaller[T] =
sprayJsonUnmarshaller(reader, ec, mat)
implicit def sprayJsonUnmarshaller[T](implicit reader: RootJsonReader[T], ec: ExecutionContext, mat: FlowMaterializer): FromEntityUnmarshaller[T] =
sprayJsValueUnmarshaller.map(jsonReader[T].read)
implicit def sprayJsValueUnmarshaller(implicit ec: ExecutionContext, mat: FlowMaterializer): FromEntityUnmarshaller[JsValue] =
Unmarshaller.byteStringUnmarshaller.forContentTypes(`application/json`).mapWithCharset { (data, charset)
val input =
if (charset == HttpCharsets.`UTF-8`) ParserInput(data.toArray)
else ParserInput(data.decodeString(charset.nioCharset.name)) // FIXME: identify charset by instance, not by name!
JsonParser(input)
}
implicit def sprayJsonMarshallerConverter[T](writer: RootJsonWriter[T])(implicit printer: JsonPrinter = PrettyPrinter, ec: ExecutionContext): ToEntityMarshaller[T] =
sprayJsonMarshaller[T](writer, printer, ec)
implicit def sprayJsonMarshaller[T](implicit writer: RootJsonWriter[T], printer: JsonPrinter = PrettyPrinter, ec: ExecutionContext): ToEntityMarshaller[T] =
sprayJsValueMarshaller[T].compose(writer.write)
implicit def sprayJsValueMarshaller[T](implicit writer: RootJsonWriter[T], printer: JsonPrinter = PrettyPrinter, ec: ExecutionContext): ToEntityMarshaller[JsValue] =
Marshaller.StringMarshaller.wrap(ContentTypes.`application/json`)(printer.apply)
}
object SprayJsonSupport extends SprayJsonSupport

View file

@ -0,0 +1,43 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshallers.xml
import java.io.{ ByteArrayInputStream, InputStreamReader }
import scala.collection.immutable
import scala.concurrent.ExecutionContext
import scala.xml.{ XML, NodeSeq }
import akka.stream.FlowMaterializer
import akka.http.unmarshalling._
import akka.http.marshalling._
import akka.http.model._
import MediaTypes._
trait ScalaXmlSupport {
implicit def defaultNodeSeqMarshaller(implicit ec: ExecutionContext): ToEntityMarshaller[NodeSeq] =
Marshaller.oneOf(ScalaXmlSupport.nodeSeqContentTypes.map(nodeSeqMarshaller): _*)
def nodeSeqMarshaller(contentType: ContentType)(implicit ec: ExecutionContext): ToEntityMarshaller[NodeSeq] =
Marshaller.StringMarshaller.wrap(contentType)(_.toString())
implicit def defaultNodeSeqUnmarshaller(implicit fm: FlowMaterializer,
ec: ExecutionContext): FromEntityUnmarshaller[NodeSeq] =
nodeSeqUnmarshaller(ScalaXmlSupport.nodeSeqContentTypeRanges: _*)
def nodeSeqUnmarshaller(ranges: ContentTypeRange*)(implicit fm: FlowMaterializer,
ec: ExecutionContext): FromEntityUnmarshaller[NodeSeq] =
Unmarshaller.byteArrayUnmarshaller.forContentTypes(ranges: _*).mapWithCharset { (bytes, charset)
if (bytes.length > 0) {
val parser = XML.parser
try parser.setProperty("http://apache.org/xml/properties/locale", java.util.Locale.ROOT)
catch { case e: org.xml.sax.SAXNotRecognizedException /* property is not needed */ }
val reader = new InputStreamReader(new ByteArrayInputStream(bytes), charset.nioCharset)
XML.withSAXParser(parser).load(reader): NodeSeq // blocking call! Ideally we'd have a `loadToFuture`
} else NodeSeq.Empty
}
}
object ScalaXmlSupport extends ScalaXmlSupport {
val nodeSeqContentTypes: immutable.Seq[ContentType] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)
val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqContentTypes.map(ContentTypeRange(_))
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Await }
import akka.http.unmarshalling.{ Unmarshal, FromEntityUnmarshaller }
import akka.http.marshalling._
import akka.http.model.HttpEntity
import akka.stream.FlowMaterializer
import scala.util.Try
trait MarshallingTestUtils {
def marshal[T: ToEntityMarshaller](value: T)(implicit ec: ExecutionContext, mat: FlowMaterializer): HttpEntity.Strict =
Await.result(Marshal(value).to[HttpEntity].flatMap(_.toStrict(1.second)), 1.second)
def unmarshalValue[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: FlowMaterializer): T =
unmarshal(entity).get
def unmarshal[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: FlowMaterializer): Try[T] = {
val fut = Unmarshal(entity).to[T]
Await.ready(fut, 1.second)
fut.value.get
}
}

View file

@ -1,140 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import com.typesafe.config.{ ConfigFactory, Config }
import scala.collection.immutable
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration._
import scala.util.DynamicVariable
import scala.reflect.ClassTag
import akka.actor.ActorSystem
import akka.stream.ActorFlowMaterializer
import akka.http.client.RequestBuilding
import akka.http.util.FastFuture
import akka.http.server._
import akka.http.unmarshalling._
import akka.http.model._
import headers.Host
import FastFuture._
trait RouteTest extends RequestBuilding with RouteTestResultComponent with MarshallingTestUtils {
this: TestFrameworkInterface
/** Override to supply a custom ActorSystem */
protected def createActorSystem(): ActorSystem =
ActorSystem(actorSystemNameFrom(getClass), testConfig)
def actorSystemNameFrom(clazz: Class[_]) =
clazz.getName
.replace('.', '-')
.replace('_', '-')
.filter(_ != '$')
def testConfigSource: String = ""
def testConfig: Config = {
val source = testConfigSource
val config = if (source.isEmpty) ConfigFactory.empty() else ConfigFactory.parseString(source)
config.withFallback(ConfigFactory.load())
}
implicit val system = createActorSystem()
implicit def executor = system.dispatcher
implicit val materializer = ActorFlowMaterializer()
def cleanUp(): Unit = system.shutdown()
private val dynRR = new DynamicVariable[RouteTestResult](null)
private def result =
if (dynRR.value ne null) dynRR.value
else sys.error("This value is only available inside of a `check` construct!")
def check[T](body: T): RouteTestResult T = result dynRR.withValue(result.awaitResult)(body)
def handled: Boolean = result.handled
def response: HttpResponse = result.response
def responseEntity: HttpEntity = result.entity
def chunks: immutable.Seq[HttpEntity.ChunkStreamPart] = result.chunks
def entityAs[T: FromEntityUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = {
def msg(e: Throwable) = s"Could not unmarshal entity to type '${implicitly[ClassTag[T]]}' for `entityAs` assertion: $e\n\nResponse was: $response"
Await.result(Unmarshal(responseEntity).to[T].fast.recover[T] { case error failTest(msg(error)) }, timeout)
}
def responseAs[T: FromResponseUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = {
def msg(e: Throwable) = s"Could not unmarshal response to type '${implicitly[ClassTag[T]]}' for `responseAs` assertion: $e\n\nResponse was: $response"
Await.result(Unmarshal(response).to[T].fast.recover[T] { case error failTest(msg(error)) }, timeout)
}
def contentType: ContentType = responseEntity.contentType
def mediaType: MediaType = contentType.mediaType
def charset: HttpCharset = contentType.charset
def definedCharset: Option[HttpCharset] = contentType.definedCharset
def headers: immutable.Seq[HttpHeader] = response.headers
def header[T <: HttpHeader: ClassTag]: Option[T] = response.header[T]
def header(name: String): Option[HttpHeader] = response.headers.find(_.is(name.toLowerCase))
def status: StatusCode = response.status
def closingExtension: String = chunks.lastOption match {
case Some(HttpEntity.LastChunk(extension, _)) extension
case _ ""
}
def trailer: immutable.Seq[HttpHeader] = chunks.lastOption match {
case Some(HttpEntity.LastChunk(_, trailer)) trailer
case _ Nil
}
def rejections: immutable.Seq[Rejection] = result.rejections
def rejection: Rejection = {
val r = rejections
if (r.size == 1) r.head else failTest("Expected a single rejection but got %s (%s)".format(r.size, r))
}
/**
* A dummy that can be used as `~> runRoute` to run the route but without blocking for the result.
* The result of the pipeline is the result that can later be checked with `check`. See the
* "separate running route from checking" example from ScalatestRouteTestSpec.scala.
*/
def runRoute: RouteTestResult RouteTestResult = akka.http.util.identityFunc
// there is already an implicit class WithTransformation in scope (inherited from akka.http.testkit.TransformerPipelineSupport)
// however, this one takes precedence
implicit class WithTransformation2(request: HttpRequest) {
def ~>[A, B](f: A B)(implicit ta: TildeArrow[A, B]): ta.Out = ta(request, f)
}
abstract class TildeArrow[A, B] {
type Out
def apply(request: HttpRequest, f: A B): Out
}
case class DefaultHostInfo(host: Host, securedConnection: Boolean)
object DefaultHostInfo {
implicit def defaultHost: DefaultHostInfo = DefaultHostInfo(Host("example.com"), securedConnection = false)
}
object TildeArrow {
implicit object InjectIntoRequestTransformer extends TildeArrow[HttpRequest, HttpRequest] {
type Out = HttpRequest
def apply(request: HttpRequest, f: HttpRequest HttpRequest) = f(request)
}
implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, setup: RoutingSetup,
defaultHostInfo: DefaultHostInfo) =
new TildeArrow[RequestContext, Future[RouteResult]] {
type Out = RouteTestResult
def apply(request: HttpRequest, route: Route): Out = {
val routeTestResult = new RouteTestResult(timeout.duration)
val effectiveRequest =
request.withEffectiveUri(
securedConnection = defaultHostInfo.securedConnection,
defaultHostHeader = defaultHostInfo.host)
val ctx = new RequestContextImpl(effectiveRequest, setup.routingLog.requestLog(effectiveRequest), setup.settings)
val sealedExceptionHandler = setup.exceptionHandler.seal(setup.settings)(setup.executionContext)
val semiSealedRoute = // sealed for exceptions but not for rejections
Directives.handleExceptions(sealedExceptionHandler) { route }
val deferrableRouteResult = semiSealedRoute(ctx)
deferrableRouteResult.fast.foreach(routeTestResult.handleResult)(setup.executor)
routeTestResult
}
}
}
}
//FIXME: trait Specs2RouteTest extends RouteTest with Specs2Interface

View file

@ -1,101 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import java.util.concurrent.CountDownLatch
import scala.collection.immutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
import akka.http.util._
import akka.stream.FlowMaterializer
import akka.stream.scaladsl._
import akka.http.model.HttpEntity.ChunkStreamPart
import akka.http.server._
import akka.http.model._
trait RouteTestResultComponent {
def failTest(msg: String): Nothing
/**
* A receptacle for the response or rejections created by a route.
*/
class RouteTestResult(timeout: FiniteDuration)(implicit fm: FlowMaterializer) {
private[this] var result: Option[Either[immutable.Seq[Rejection], HttpResponse]] = None
private[this] val latch = new CountDownLatch(1)
def handled: Boolean = synchronized { result.isDefined && result.get.isRight }
def rejections: immutable.Seq[Rejection] = synchronized {
result match {
case Some(Left(rejections)) rejections
case Some(Right(response)) failTest("Request was not rejected, response was " + response)
case None failNeitherCompletedNorRejected()
}
}
def response: HttpResponse = rawResponse.copy(entity = entity)
/** Returns a "fresh" entity with a "fresh" unconsumed byte- or chunk stream (if not strict) */
def entity: ResponseEntity = entityRecreator()
def chunks: immutable.Seq[ChunkStreamPart] =
entity match {
case HttpEntity.Chunked(_, chunks) awaitAllElements[ChunkStreamPart](chunks)
case _ Nil
}
def ~>[T](f: RouteTestResult T): T = f(this)
private def rawResponse: HttpResponse = synchronized {
result match {
case Some(Right(response)) response
case Some(Left(Nil)) failTest("Request was rejected")
case Some(Left(rejection :: Nil)) failTest("Request was rejected with rejection " + rejection)
case Some(Left(rejections)) failTest("Request was rejected with rejections " + rejections)
case None failNeitherCompletedNorRejected()
}
}
private[testkit] def handleResult(rr: RouteResult)(implicit ec: ExecutionContext): Unit =
synchronized {
if (result.isEmpty) {
result = rr match {
case RouteResult.Complete(response) Some(Right(response))
case RouteResult.Rejected(rejections) Some(Left(RejectionHandler.applyTransformations(rejections)))
}
latch.countDown()
} else failTest("Route completed/rejected more than once")
}
private[testkit] def awaitResult: this.type = {
latch.await(timeout.toMillis, MILLISECONDS)
this
}
private[this] lazy val entityRecreator: () ResponseEntity =
rawResponse.entity match {
case s: HttpEntity.Strict () s
case HttpEntity.Default(contentType, contentLength, data)
val dataChunks = awaitAllElements(data);
{ () HttpEntity.Default(contentType, contentLength, Source(dataChunks)) }
case HttpEntity.CloseDelimited(contentType, data)
val dataChunks = awaitAllElements(data);
{ () HttpEntity.CloseDelimited(contentType, Source(dataChunks)) }
case HttpEntity.Chunked(contentType, chunks)
val dataChunks = awaitAllElements(chunks);
{ () HttpEntity.Chunked(contentType, Source(dataChunks)) }
}
private def failNeitherCompletedNorRejected(): Nothing =
failTest("Request was neither completed nor rejected within " + timeout)
private def awaitAllElements[T](data: Source[T, _]): immutable.Seq[T] =
data.grouped(100000).runWith(Sink.head).awaitResult(timeout)
}
}

View file

@ -1,15 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import scala.concurrent.duration._
import akka.actor.ActorSystem
import akka.testkit._
case class RouteTestTimeout(duration: FiniteDuration)
object RouteTestTimeout {
implicit def default(implicit system: ActorSystem) = RouteTestTimeout(1.second dilated)
}

View file

@ -1,31 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import scala.util.Try
import scala.concurrent.{ ExecutionContext, Future, Await }
import scala.concurrent.duration._
import org.scalatest.Suite
import org.scalatest.matchers.Matcher
import akka.http.model.HttpEntity
import akka.http.unmarshalling.FromEntityUnmarshaller
import akka.stream.FlowMaterializer
trait ScalatestUtils extends MarshallingTestUtils {
import org.scalatest.Matchers._
def evaluateTo[T](value: T): Matcher[Future[T]] =
equal(value).matcher[T] compose (x Await.result(x, 1.second))
def haveFailedWith(t: Throwable): Matcher[Future[_]] =
equal(t).matcher[Throwable] compose (x Await.result(x.failed, 1.second))
def unmarshalToValue[T: FromEntityUnmarshaller](value: T)(implicit ec: ExecutionContext, mat: FlowMaterializer): Matcher[HttpEntity] =
equal(value).matcher[T] compose (unmarshalValue(_))
def unmarshalTo[T: FromEntityUnmarshaller](value: Try[T])(implicit ec: ExecutionContext, mat: FlowMaterializer): Matcher[HttpEntity] =
equal(value).matcher[Try[T]] compose (unmarshal(_))
}
trait ScalatestRouteTest extends RouteTest with TestFrameworkInterface.Scalatest with ScalatestUtils { this: Suite }

View file

@ -1,30 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import org.scalatest.exceptions.TestFailedException
import org.scalatest.{ BeforeAndAfterAll, Suite }
trait TestFrameworkInterface {
def cleanUp()
def failTest(msg: String): Nothing
}
object TestFrameworkInterface {
trait Scalatest extends TestFrameworkInterface with BeforeAndAfterAll {
this: Suite
def failTest(msg: String) = throw new TestFailedException(msg, 11)
abstract override protected def afterAll(): Unit = {
cleanUp()
super.afterAll()
}
}
}

View file

@ -1,81 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.testkit
import scala.concurrent.duration._
import org.scalatest.FreeSpec
import org.scalatest.Matchers
import akka.testkit.TestProbe
import akka.util.Timeout
import akka.pattern.ask
import akka.http.model.headers.RawHeader
import akka.http.server._
import akka.http.model._
import StatusCodes._
import HttpMethods._
import Directives._
class ScalatestRouteTestSpec extends FreeSpec with Matchers with ScalatestRouteTest {
"The ScalatestRouteTest should support" - {
"the most simple and direct route test" in {
Get() ~> complete(HttpResponse()) ~> { rr rr.awaitResult; rr.response } shouldEqual HttpResponse()
}
"a test using a directive and some checks" in {
val pinkHeader = RawHeader("Fancy", "pink")
Get() ~> addHeader(pinkHeader) ~> {
respondWithHeader(pinkHeader) {
complete("abc")
}
} ~> check {
status shouldEqual OK
responseEntity shouldEqual HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")
header("Fancy") shouldEqual Some(pinkHeader)
}
}
"proper rejection collection" in {
Post("/abc", "content") ~> {
(get | put) {
complete("naah")
}
} ~> check {
rejections shouldEqual List(MethodRejection(GET), MethodRejection(PUT))
}
}
"separation of route execution from checking" in {
val pinkHeader = RawHeader("Fancy", "pink")
case object Command
val service = TestProbe()
val handler = TestProbe()
implicit def serviceRef = service.ref
implicit val askTimeout: Timeout = 1.second
val result =
Get() ~> pinkHeader ~> {
respondWithHeader(pinkHeader) {
complete(handler.ref.ask(Command).mapTo[String])
}
} ~> runRoute
handler.expectMsg(Command)
handler.reply("abc")
check {
status shouldEqual OK
responseEntity shouldEqual HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")
header("Fancy") shouldEqual Some(pinkHeader)
}(result)
}
}
// TODO: remove once RespondWithDirectives have been ported
def respondWithHeader(responseHeader: HttpHeader): Directive0 =
mapResponseHeaders(responseHeader +: _)
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.japi.examples.simple;
import akka.actor.ActorSystem;
import akka.http.server.japi.*;
import java.io.IOException;
public class SimpleServerApp8 extends HttpApp {
static Parameter<Integer> x = Parameters.integer("x");
static Parameter<Integer> y = Parameters.integer("y");
static PathMatcher<Integer> xSegment = PathMatchers.integerNumber();
static PathMatcher<Integer> ySegment = PathMatchers.integerNumber();
public static RouteResult multiply(RequestContext ctx, int x, int y) {
int result = x * y;
return ctx.complete(String.format("%d * %d = %d", x, y, result));
}
static class Test {
int constant;
Test(int constant) {
this.constant = constant;
}
RouteResult constantPlusMultiply(RequestContext ctx, int x, int y) {
int result = x * y + constant;
return ctx.complete(String.format("%d * %d + %d = %d", x, y, constant, result));
}
}
public void test() {
handleWith(xSegment, ySegment, SimpleServerApp8::multiply);
}
@Override
public Route createRoute() {
Handler addHandler = new Handler() {
@Override
public RouteResult handle(RequestContext ctx) {
int xVal = x.get(ctx);
int yVal = y.get(ctx);
int result = xVal + yVal;
return ctx.complete(String.format("%d + %d = %d", xVal, yVal, result));
}
};
Handler2<Integer, Integer> subtractHandler = new Handler2<Integer, Integer>() {
public RouteResult handle(RequestContext ctx, Integer xVal, Integer yVal) {
int result = xVal - yVal;
return ctx.complete(String.format("%d - %d = %d", xVal, yVal, result));
}
};
return
route(
// matches the empty path
pathSingleSlash().route(
getFromResource("web/calculator.html")
),
// matches paths like this: /add?x=42&y=23
path("add").route(
handleWith(addHandler, x, y)
),
path("subtract").route(
handleWith(x, y, subtractHandler)
),
path("divide").route(
handleWith(x, y,
(ctx, x, y) ->
ctx.complete(String.format("%d / %d = %d", x, y, x / y))
)
),
// matches paths like this: /multiply/{x}/{y}
path("multiply", xSegment, ySegment).route(
// bind handler by reflection
handleWith(xSegment, ySegment, SimpleServerApp8::multiply)
),
path("multiply-methodref", xSegment, ySegment).route(
// bind handler by reflection
handleWith(xSegment, ySegment, new Test(123)::constantPlusMultiply)
)
);
}
public static void main(String[] args) throws IOException {
ActorSystem system = ActorSystem.create();
new SimpleServerApp8().bindRoute("localhost", 8080, system);
System.out.println("Type RETURN to exit");
System.in.read();
system.shutdown();
}
}

View file

@ -0,0 +1,23 @@
<html>
<body>
<h1>Calculator</h1>
<h2>Add</h2>
<form action="add">
<label for="x">x:</label><input id="x" name="x" type="text" value="42"/>
<label for="y">y:</label><input id="y" name="y" type="text" value="23"/>
<input type="submit" />
</form>
<h2>Subtract</h2>
<form action="subtract">
<label for="x">x:</label><input id="x" name="x" type="text" value="42"/>
<label for="y">y:</label><input id="y" name="y" type="text" value="23"/>
<input type="submit" />
</form>
<h3>Multiply</h3>
<a href="/multiply/42/23">/multiply/42/23</a>
</body>
</html>

View file

@ -0,0 +1,12 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
import akka.http.server.japi.HandlerBindingTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses(HandlerBindingTest.class)
public class AllJavaTests {
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.japi;
import akka.http.model.HttpRequest;
import org.junit.Test;
import static akka.http.server.japi.Directives.*;
public class HandlerBindingTest extends JUnitRouteTest {
@Test
public void testHandlerWithoutExtractions() {
Route route = handleWith(ctx -> ctx.complete("Ok"));
TestResponse response = runRoute(route, HttpRequest.GET("/"));
response.assertEntity("Ok");
}
@Test
public void testHandler1() {
Route route = handleWith(Parameters.integer("a"), (ctx, a) -> ctx.complete("Ok " + a));
TestResponse response = runRoute(route, HttpRequest.GET("?a=23"));
response.assertStatusCode(200);
response.assertEntity("Ok 23");
}
@Test
public void testHandler2() {
Route route =
handleWith(
Parameters.integer("a"),
Parameters.integer("b"),
(ctx, a, b) -> ctx.complete("Sum: " + (a + b)));
TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42"));
response.assertStatusCode(200);
response.assertEntity("Sum: 65");
}
@Test
public void testHandler3() {
Route route =
handleWith(
Parameters.integer("a"),
Parameters.integer("b"),
Parameters.integer("c"),
(ctx, a, b, c) -> ctx.complete("Sum: " + (a + b + c)));
TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30"));
response.assertStatusCode(200);
response.assertEntity("Sum: 95");
}
@Test
public void testHandler4() {
Route route =
handleWith(
Parameters.integer("a"),
Parameters.integer("b"),
Parameters.integer("c"),
Parameters.integer("d"),
(ctx, a, b, c, d) -> ctx.complete("Sum: " + (a + b + c + d)));
TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30&d=45"));
response.assertStatusCode(200);
response.assertEntity("Sum: 140");
}
public RouteResult sum(RequestContext ctx, int a, int b, int c, int d) {
return ctx.complete("Sum: "+(a + b + c + d));
}
@Test
public void testHandler4MethodRef() {
Route route =
handleWith(
Parameters.integer("a"),
Parameters.integer("b"),
Parameters.integer("c"),
Parameters.integer("d"),
this::sum);
TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30&d=45"));
response.assertStatusCode(200);
response.assertEntity("Sum: 140");
}
}

View file

@ -1 +0,0 @@
<p>Lorem ipsum!</p>

View file

@ -1 +0,0 @@
XyZ

View file

@ -1,42 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http
import scala.concurrent.duration._
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec }
import org.scalatest.concurrent.ScalaFutures
import akka.actor.ActorSystem
import akka.stream.ActorFlowMaterializer
import akka.http.unmarshalling.Unmarshal
import akka.http.marshalling.Marshal
import akka.http.model._
class FormDataSpec extends WordSpec with Matchers with ScalaFutures with BeforeAndAfterAll {
implicit val system = ActorSystem(getClass.getSimpleName)
implicit val materializer = ActorFlowMaterializer()
import system.dispatcher
val formData = FormData(Map("surname" -> "Smith", "age" -> "42"))
"The FormData infrastructure" should {
"properly round-trip the fields of www-urlencoded forms" in {
Marshal(formData).to[HttpEntity]
.flatMap(Unmarshal(_).to[FormData]).futureValue shouldEqual formData
}
"properly marshal www-urlencoded forms containing special chars" in {
Marshal(FormData(Map("name" -> "Smith&Wesson"))).to[HttpEntity]
.flatMap(Unmarshal(_).to[String]).futureValue shouldEqual "name=Smith%26Wesson"
Marshal(FormData(Map("name" -> "Smith+Wesson; hopefully!"))).to[HttpEntity]
.flatMap(Unmarshal(_).to[String]).futureValue shouldEqual "name=Smith%2BWesson%3B+hopefully%21"
}
}
override def afterAll() = {
system.shutdown()
system.awaitTermination(10.seconds)
}
}

View file

@ -1,79 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import org.scalatest.{ Suite, BeforeAndAfterAll, Matchers }
import scala.concurrent.duration._
import akka.actor.ActorSystem
import akka.stream.ActorFlowMaterializer
import akka.util.ByteString
trait CodecSpecSupport extends Matchers with BeforeAndAfterAll { self: Suite
def readAs(string: String, charset: String = "UTF8") = equal(string).matcher[String] compose { (_: ByteString).decodeString(charset) }
def hexDump(bytes: ByteString) = bytes.map("%02x".format(_)).mkString
def fromHexDump(dump: String) = dump.grouped(2).toArray.map(chars Integer.parseInt(new String(chars), 16).toByte)
def printBytes(i: Int, id: String) = {
def byte(i: Int) = (i & 0xFF).toHexString
println(id + ": " + byte(i) + ":" + byte(i >> 8) + ":" + byte(i >> 16) + ":" + byte(i >> 24))
i
}
lazy val smallTextBytes = ByteString(smallText, "UTF8")
lazy val largeTextBytes = ByteString(largeText, "UTF8")
val smallText = "Yeah!"
val largeText =
"""Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing
elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos
et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor
sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat
nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis
dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh
euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo
consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu
feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit
augue duis dolore te feugait nulla facilisi.
Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim
assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet
dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit
lobortis nisl ut aliquip ex ea commodo consequat.
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat
nulla facilisis.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt
ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et
et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua.
est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
invidunt ut labore et dolore magna aliquyam erat.
Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus
est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy e""".replace("\r\n", "\n")
implicit val system = ActorSystem(getClass.getSimpleName)
implicit val materializer = ActorFlowMaterializer()
override def afterAll() = {
system.shutdown()
system.awaitTermination(10.seconds)
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,43 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.stream.stage.{ SyncDirective, Context, PushStage, Stage }
import akka.util.ByteString
import org.scalatest.WordSpec
import akka.http.model._
import headers._
import HttpMethods.POST
import akka.http.util._
import scala.concurrent.duration._
class DecoderSpec extends WordSpec with CodecSpecSupport {
"A Decoder" should {
"not transform the message if it doesn't contain a Content-Encoding header" in {
val request = HttpRequest(POST, entity = HttpEntity(smallText))
DummyDecoder.decode(request) shouldEqual request
}
"correctly transform the message if it contains a Content-Encoding header" in {
val request = HttpRequest(POST, entity = HttpEntity(smallText), headers = List(`Content-Encoding`(DummyDecoder.encoding)))
val decoded = DummyDecoder.decode(request)
decoded.headers shouldEqual Nil
decoded.entity.toStrict(1.second).awaitResult(1.second) shouldEqual HttpEntity(dummyDecompress(smallText))
}
}
def dummyDecompress(s: String): String = dummyDecompress(ByteString(s, "UTF8")).decodeString("UTF8")
def dummyDecompress(bytes: ByteString): ByteString = DummyDecoder.decode(bytes).awaitResult(1.second)
case object DummyDecoder extends StreamDecoder {
val encoding = HttpEncodings.compress
def newDecompressorStage(maxBytesPerChunk: Int): () Stage[ByteString, ByteString] =
() new PushStage[ByteString, ByteString] {
def onPush(elem: ByteString, ctx: Context[ByteString]): SyncDirective =
ctx.push(elem ++ ByteString("compressed"))
}
}
}

View file

@ -1,28 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.util.ByteString
import java.io.{ InputStream, OutputStream }
import java.util.zip._
class DeflateSpec extends CoderSpec {
protected def Coder: Coder with StreamDecoder = Deflate
protected def newDecodedInputStream(underlying: InputStream): InputStream =
new InflaterInputStream(underlying)
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream =
new DeflaterOutputStream(underlying)
override def extraTests(): Unit = {
"throw early if header is corrupt" in {
(the[RuntimeException] thrownBy {
ourDecode(ByteString(0, 1, 2, 3, 4))
}).getCause should be(a[DataFormatException])
}
}
}

View file

@ -1,47 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.util.ByteString
import org.scalatest.WordSpec
import akka.http.model._
import headers._
import HttpMethods.POST
import scala.concurrent.duration._
import akka.http.util._
class EncoderSpec extends WordSpec with CodecSpecSupport {
"An Encoder" should {
"not transform the message if messageFilter returns false" in {
val request = HttpRequest(POST, entity = HttpEntity(smallText.getBytes("UTF8")))
DummyEncoder.encode(request) shouldEqual request
}
"correctly transform the HttpMessage if messageFilter returns true" in {
val request = HttpRequest(POST, entity = HttpEntity(smallText))
val encoded = DummyEncoder.encode(request)
encoded.headers shouldEqual List(`Content-Encoding`(DummyEncoder.encoding))
encoded.entity.toStrict(1.second).awaitResult(1.second) shouldEqual HttpEntity(dummyCompress(smallText))
}
}
def dummyCompress(s: String): String = dummyCompress(ByteString(s, "UTF8")).utf8String
def dummyCompress(bytes: ByteString): ByteString = DummyCompressor.compressAndFinish(bytes)
case object DummyEncoder extends Encoder {
val messageFilter = Encoder.DefaultFilter
val encoding = HttpEncodings.compress
def newCompressor = DummyCompressor
}
case object DummyCompressor extends Compressor {
def compress(input: ByteString) = input ++ ByteString("compressed")
def flush() = ByteString.empty
def finish() = ByteString.empty
def compressAndFlush(input: ByteString): ByteString = compress(input)
def compressAndFinish(input: ByteString): ByteString = compress(input)
}
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.http.util._
import java.io.{ InputStream, OutputStream }
import java.util.zip.{ ZipException, GZIPInputStream, GZIPOutputStream }
import akka.util.ByteString
class GzipSpec extends CoderSpec {
protected def Coder: Coder with StreamDecoder = Gzip
protected def newDecodedInputStream(underlying: InputStream): InputStream =
new GZIPInputStream(underlying)
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream =
new GZIPOutputStream(underlying)
override def extraTests(): Unit = {
"decode concatenated compressions" in {
pending // FIXME: unbreak
ourDecode(Seq(encode("Hello, "), encode("dear "), encode("User!")).join) should readAs("Hello, dear User!")
}
"provide a better compression ratio than the standard Gzip/Gunzip streams" in {
ourEncode(largeTextBytes).length should be < streamEncode(largeTextBytes).length
}
"throw an error on truncated input" in {
pending // FIXME: unbreak
val ex = the[ZipException] thrownBy ourDecode(streamEncode(smallTextBytes).dropRight(5))
ex.getMessage should equal("Truncated GZIP stream")
}
"throw early if header is corrupt" in {
val cause = (the[RuntimeException] thrownBy ourDecode(ByteString(0, 1, 2, 3, 4))).getCause
cause should (be(a[ZipException]) and have message "Not in GZIP format")
}
}
}

View file

@ -1,16 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import java.io.{ OutputStream, InputStream }
class NoCodingSpec extends CoderSpec {
protected def Coder: Coder with StreamDecoder = NoCoding
override protected def corruptInputCheck = false
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream = underlying
protected def newDecodedInputStream(underlying: InputStream): InputStream = underlying
}

View file

@ -1,70 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshallers
import akka.http.marshalling.ToEntityMarshaller
import akka.http.model.{ HttpCharsets, HttpEntity, MediaTypes }
import akka.http.testkit.ScalatestRouteTest
import akka.http.unmarshalling.FromEntityUnmarshaller
import akka.http.util._
import org.scalatest.{ Matchers, WordSpec }
case class Employee(fname: String, name: String, age: Int, id: Long, boardMember: Boolean) {
require(!boardMember || age > 40, "Board members must be older than 40")
}
object Employee {
val simple = Employee("Frank", "Smith", 42, 12345, false)
val json = """{"fname":"Frank","name":"Smith","age":42,"id":12345,"boardMember":false}"""
val utf8 = Employee("Fränk", "Smi√", 42, 12345, false)
val utf8json =
"""{
| "fname": "Fränk",
| "name": "Smi",
| "age": 42,
| "id": 12345,
| "boardMember": false
|}""".stripMargin.getBytes(HttpCharsets.`UTF-8`.nioCharset)
val illegalEmployeeJson = """{"fname":"Little Boy","name":"Smith","age":7,"id":12345,"boardMember":true}"""
}
/** Common infrastructure needed for several json support subprojects */
abstract class JsonSupportSpec extends WordSpec with Matchers with ScalatestRouteTest {
require(getClass.getSimpleName.endsWith("Spec"))
// assuming that the classname ends with "Spec"
def name: String = getClass.getSimpleName.dropRight(4)
implicit def marshaller: ToEntityMarshaller[Employee]
implicit def unmarshaller: FromEntityUnmarshaller[Employee]
"The " + name should {
"provide unmarshalling support for a case class" in {
HttpEntity(MediaTypes.`application/json`, Employee.json) should unmarshalToValue(Employee.simple)
}
"provide marshalling support for a case class" in {
val marshalled = marshal(Employee.simple)
marshalled.data.utf8String shouldEqual
"""{
| "age": 42,
| "boardMember": false,
| "fname": "Frank",
| "id": 12345,
| "name": "Smith"
|}""".stripMarginWithNewline("\n")
}
"use UTF-8 as the default charset for JSON source decoding" in {
HttpEntity(MediaTypes.`application/json`, Employee.utf8json) should unmarshalToValue(Employee.utf8)
}
"provide proper error messages for requirement errors" in {
val result = unmarshal(HttpEntity(MediaTypes.`application/json`, Employee.illegalEmployeeJson))
result.isFailure shouldEqual true
val ex = result.failed.get
ex.getMessage shouldEqual "requirement failed: Board members must be older than 40"
}
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshallers.sprayjson
import java.lang.StringBuilder
import akka.http.marshallers.{ JsonSupportSpec, Employee }
import akka.http.marshalling.ToEntityMarshaller
import akka.http.unmarshalling.FromEntityUnmarshaller
import spray.json.{ JsValue, PrettyPrinter, JsonPrinter, DefaultJsonProtocol }
import scala.collection.immutable.ListMap
class SprayJsonSupportSpec extends JsonSupportSpec {
object EmployeeJsonProtocol extends DefaultJsonProtocol {
implicit val employeeFormat = jsonFormat5(Employee.apply)
}
import EmployeeJsonProtocol._
implicit val orderedFieldPrint: JsonPrinter = new PrettyPrinter {
override protected def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int): Unit =
super.printObject(ListMap(members.toSeq.sortBy(_._1): _*), sb, indent)
}
implicit def marshaller: ToEntityMarshaller[Employee] = SprayJsonSupport.sprayJsonMarshaller[Employee]
implicit def unmarshaller: FromEntityUnmarshaller[Employee] = SprayJsonSupport.sprayJsonUnmarshaller[Employee]
}

View file

@ -1,31 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshallers.xml
import scala.xml.NodeSeq
import org.scalatest.{ Matchers, WordSpec }
import akka.http.testkit.ScalatestRouteTest
import akka.http.unmarshalling.{ Unmarshaller, Unmarshal }
import akka.http.model._
import HttpCharsets._
import MediaTypes._
class ScalaXmlSupportSpec extends WordSpec with Matchers with ScalatestRouteTest {
import ScalaXmlSupport._
"ScalaXmlSupport" should {
"NodeSeqMarshaller should marshal xml snippets to `text/xml` content in UTF-8" in {
marshal(<employee><nr>Hallo</nr></employee>) shouldEqual
HttpEntity(ContentType(`text/xml`, `UTF-8`), "<employee><nr>Ha“llo</nr></employee>")
}
"nodeSeqUnmarshaller should unmarshal `text/xml` content in UTF-8 to NodeSeqs" in {
Unmarshal(HttpEntity(`text/xml`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should evaluateTo("Hällö")
}
"nodeSeqUnmarshaller should reject `application/octet-stream`" in {
Unmarshal(HttpEntity(`application/octet-stream`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should
haveFailedWith(Unmarshaller.UnsupportedContentTypeException(nodeSeqContentTypeRanges: _*))
}
}
}

View file

@ -1,138 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.concurrent.Await
import scala.concurrent.duration._
import org.scalatest.{ Matchers, FreeSpec }
import akka.http.util.FastFuture._
import akka.http.model._
import MediaTypes._
import HttpCharsets._
class ContentNegotiationSpec extends FreeSpec with Matchers {
"Content Negotiation should work properly for requests with header(s)" - {
"(without headers)" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept: */*" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept: */*;q=.8" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept: text/*" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
accept(`audio/ogg`) should reject
}
"Accept: text/*;q=.8" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
accept(`audio/ogg`) should reject
}
"Accept: text/*;q=0" in test { accept
accept(`text/plain`) should reject
accept(`text/xml` withCharset `UTF-16`) should reject
accept(`audio/ogg`) should reject
}
"Accept-Charset: UTF-16" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
accept(`text/plain` withCharset `UTF-8`) should reject
}
"Accept-Charset: UTF-16, UTF-8" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept-Charset: UTF-8;q=.2, UTF-16" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
}
"Accept-Charset: UTF-8;q=.2" in test { accept
accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`)
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
}
"Accept-Charset: latin1;q=.1, UTF-8;q=.2" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
}
"Accept-Charset: *" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept-Charset: *;q=0" in test { accept
accept(`text/plain`) should reject
accept(`text/plain` withCharset `UTF-16`) should reject
}
"Accept-Charset: us;q=0.1,*;q=0" in test { accept
accept(`text/plain`) should select(`text/plain`, `US-ASCII`)
accept(`text/plain` withCharset `UTF-8`) should reject
}
"Accept: text/xml, text/html;q=.5" in test { accept
accept(`text/plain`) should reject
accept(`text/xml`) should select(`text/xml`, `UTF-8`)
accept(`text/html`) should select(`text/html`, `UTF-8`)
accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`)
accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`)
accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`)
accept(`text/plain`, `text/html`) should select(`text/html`, `UTF-8`)
}
"""Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2
Accept-Charset: UTF-16""" in test { accept
accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `UTF-16`)
accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain`, `UTF-16`)
accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript`, `UTF-16`)
accept(`image/gif`, `application/javascript`) should select(`application/javascript`, `UTF-16`)
accept(`image/gif`, `audio/ogg`) should select(`image/gif`, `UTF-16`)
}
}
def test[U](body: ((ContentType*) Option[ContentType]) U): String U = { example
val headers =
if (example != "(without headers)") {
example.split('\n').toList map { rawHeader
val Array(name, value) = rawHeader.split(':')
HttpHeader.parse(name.trim, value) match {
case HttpHeader.ParsingResult.Ok(header, Nil) header
case result fail(result.errors.head.formatPretty)
}
}
} else Nil
val request = HttpRequest(headers = headers)
body { contentTypes
import scala.concurrent.ExecutionContext.Implicits.global
implicit val marshallers = contentTypes map {
case ct @ ContentType(mt, Some(cs)) Marshaller.withFixedCharset(mt, cs)((s: String) HttpEntity(ct, s))
case ContentType(mt, None) Marshaller.withOpenCharset(mt)((s: String, cs) HttpEntity(ContentType(mt, cs), s))
}
Await.result(Marshal("foo").toResponseFor(request)
.fast.map(response Some(response.entity.contentType))
.fast.recover { case _: Marshal.UnacceptableResponseContentTypeException None }, 1.second)
}
}
def reject = equal(None)
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset)))
}

View file

@ -1,149 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import akka.http.testkit.MarshallingTestUtils
import akka.http.marshallers.xml.ScalaXmlSupport._
import scala.collection.immutable.ListMap
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
import akka.actor.ActorSystem
import akka.stream.ActorFlowMaterializer
import akka.stream.scaladsl.Source
import akka.http.util._
import akka.http.model._
import headers._
import HttpCharsets._
import MediaTypes._
class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with MultipartMarshallers with MarshallingTestUtils {
implicit val system = ActorSystem(getClass.getSimpleName)
implicit val materializer = ActorFlowMaterializer()
import system.dispatcher
"The PredefinedToEntityMarshallers." - {
"StringMarshaller should marshal strings to `text/plain` content in UTF-8" in {
marshal("Ha“llo") shouldEqual HttpEntity("Ha“llo")
}
"CharArrayMarshaller should marshal char arrays to `text/plain` content in UTF-8" in {
marshal("Ha“llo".toCharArray) shouldEqual HttpEntity("Ha“llo")
}
"FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in {
marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual
HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), "name=Bob&pass=h%C3%A4llo&admin=")
}
}
"The GenericMarshallers." - {
"optionMarshaller should enable marshalling of Option[T]" in {
marshal(Some("Ha“llo")) shouldEqual HttpEntity("Ha“llo")
marshal(None: Option[String]) shouldEqual HttpEntity.Empty
}
"eitherMarshaller should enable marshalling of Either[A, B]" in {
marshal[Either[Array[Char], String]](Right("right")) shouldEqual HttpEntity("right")
marshal[Either[Array[Char], String]](Left("left".toCharArray)) shouldEqual HttpEntity("left")
}
}
"The MultipartMarshallers." - {
"multipartMarshaller should correctly marshal multipart content with" - {
"one empty part" in {
marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity(
contentType = ContentType(`multipart/mixed` withBoundary randomBoundary),
string = s"""--$randomBoundary
|Content-Type: text/plain; charset=UTF-8
|
|
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
}
"one non-empty part" in {
marshal(Multipart.General(`multipart/alternative`, Multipart.General.BodyPart.Strict(
entity = HttpEntity(ContentType(`text/plain`, `UTF-8`), "test@there.com"),
headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")) :: Nil))) shouldEqual
HttpEntity(
contentType = ContentType(`multipart/alternative` withBoundary randomBoundary),
string = s"""--$randomBoundary
|Content-Type: text/plain; charset=UTF-8
|Content-Disposition: form-data; name=email
|
|test@there.com
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
}
"two different parts" in {
marshal(Multipart.General(`multipart/related`,
Multipart.General.BodyPart.Strict(HttpEntity(ContentType(`text/plain`, Some(`US-ASCII`)), "first part, with a trailing linebreak\r\n")),
Multipart.General.BodyPart.Strict(
HttpEntity(ContentType(`application/octet-stream`), "filecontent"),
RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual
HttpEntity(
contentType = ContentType(`multipart/related` withBoundary randomBoundary),
string = s"""--$randomBoundary
|Content-Type: text/plain; charset=US-ASCII
|
|first part, with a trailing linebreak
|
|--$randomBoundary
|Content-Type: application/octet-stream
|Content-Transfer-Encoding: binary
|
|filecontent
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
}
}
"multipartFormDataMarshaller should correctly marshal 'multipart/form-data' content with" - {
"two fields" in {
marshal(Multipart.FormData(ListMap(
"surname" -> HttpEntity("Mike"),
"age" -> marshal(<int>42</int>)))) shouldEqual
HttpEntity(
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
string = s"""--$randomBoundary
|Content-Type: text/plain; charset=UTF-8
|Content-Disposition: form-data; name=surname
|
|Mike
|--$randomBoundary
|Content-Type: text/xml; charset=UTF-8
|Content-Disposition: form-data; name=age
|
|<int>42</int>
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
}
"two fields having a custom `Content-Disposition`" in {
marshal(Multipart.FormData(Source(List(
Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv`, "name,age\r\n\"John Doe\",20\r\n"),
Map("filename" -> "attachment.csv")),
Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes),
Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual
HttpEntity(
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
string = s"""--$randomBoundary
|Content-Type: text/csv
|Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]"
|
|name,age
|"John Doe",20
|
|--$randomBoundary
|Content-Type: application/octet-stream
|Content-Disposition: form-data; filename=attachment2.csv; name="attachment[1]"
|Content-Transfer-Encoding: binary
|
|naice!
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
}
}
}
override def afterAll() = system.shutdown()
protected class FixedRandom extends java.util.Random {
override def nextBytes(array: Array[Byte]): Unit = "my-stable-boundary".getBytes("UTF-8").copyToArray(array)
}
override protected val multipartBoundaryRandom = new FixedRandom // fix for stable value
}

View file

@ -1,181 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import akka.http.model
import model.HttpMethods._
import model.StatusCodes
class BasicRouteSpecs extends RoutingSpec {
"routes created by the concatenation operator '~'" should {
"yield the first sub route if it succeeded" in {
Get() ~> {
get { complete("first") } ~ get { complete("second") }
} ~> check { responseAs[String] shouldEqual "first" }
}
"yield the second sub route if the first did not succeed" in {
Get() ~> {
post { complete("first") } ~ get { complete("second") }
} ~> check { responseAs[String] shouldEqual "second" }
}
"collect rejections from both sub routes" in {
Delete() ~> {
get { completeOk } ~ put { completeOk }
} ~> check { rejections shouldEqual Seq(MethodRejection(GET), MethodRejection(PUT)) }
}
"clear rejections that have already been 'overcome' by previous directives" in {
pending
/*Put() ~> {
put { parameter('yeah) { echoComplete } } ~
get { completeOk }
} ~> check { rejection shouldEqual MissingQueryParamRejection("yeah") }*/
}
}
"Route conjunction" should {
val stringDirective = provide("The cat")
val intDirective = provide(42)
val doubleDirective = provide(23.0)
val dirStringInt = stringDirective & intDirective
val dirStringIntDouble = dirStringInt & doubleDirective
val dirDoubleStringInt = doubleDirective & dirStringInt
val dirStringIntStringInt = dirStringInt & dirStringInt
"work for two elements" in {
Get("/abc") ~> {
dirStringInt { (str, i)
complete(s"$str ${i + 1}")
}
} ~> check { responseAs[String] shouldEqual "The cat 43" }
}
"work for 2 + 1" in {
Get("/abc") ~> {
dirStringIntDouble { (str, i, d)
complete(s"$str ${i + 1} ${d + 0.1}")
}
} ~> check { responseAs[String] shouldEqual "The cat 43 23.1" }
}
"work for 1 + 2" in {
Get("/abc") ~> {
dirDoubleStringInt { (d, str, i)
complete(s"$str ${i + 1} ${d + 0.1}")
}
} ~> check { responseAs[String] shouldEqual "The cat 43 23.1" }
}
"work for 2 + 2" in {
Get("/abc") ~> {
dirStringIntStringInt { (str, i, str2, i2)
complete(s"$str ${i + i2} $str2")
}
} ~> check { responseAs[String] shouldEqual "The cat 84 The cat" }
}
}
"Route disjunction" should {
"work in the happy case" in {
val route = Route.seal((path("abc") | path("def")) {
completeOk
})
Get("/abc") ~> route ~> check {
status shouldEqual StatusCodes.OK
}
Get("/def") ~> route ~> check {
status shouldEqual StatusCodes.OK
}
Get("/ghi") ~> route ~> check {
status shouldEqual StatusCodes.NotFound
}
}
"don't apply alternative if inner route rejects" in {
object MyRejection extends Rejection
val route = (path("abc") | post) {
reject(MyRejection)
}
Get("/abc") ~> route ~> check {
rejection shouldEqual MyRejection
}
}
}
"Case class extraction with Directive.as" should {
"extract one argument" in {
case class MyNumber(i: Int)
val abcPath = path("abc" / IntNumber).as(MyNumber)(echoComplete)
Get("/abc/5") ~> abcPath ~> check {
responseAs[String] shouldEqual "MyNumber(5)"
}
}
"extract two arguments" in {
case class Person(name: String, age: Int)
val personPath = path("person" / Segment / IntNumber).as(Person)(echoComplete)
Get("/person/john/38") ~> personPath ~> check {
responseAs[String] shouldEqual "Person(john,38)"
}
}
}
"Dynamic execution of inner routes of Directive0" should {
"re-execute inner routes every time" in {
var a = ""
val dynamicRoute = get { a += "x"; complete(a) }
def expect(route: Route, s: String) = Get() ~> route ~> check { responseAs[String] shouldEqual s }
expect(dynamicRoute, "x")
expect(dynamicRoute, "xx")
expect(dynamicRoute, "xxx")
expect(dynamicRoute, "xxxx")
}
}
case object MyException extends RuntimeException
"Route sealing" should {
"catch route execution exceptions" in {
Get("/abc") ~> Route.seal {
get { ctx
throw MyException
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
}
}
"catch route building exceptions" in {
Get("/abc") ~> Route.seal {
get {
throw MyException
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
}
}
"convert all rejections to responses" in {
object MyRejection extends Rejection
Get("/abc") ~> Route.seal {
get {
reject(MyRejection)
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
}
}
"always prioritize MethodRejections over AuthorizationFailedRejections" in {
Get("/abc") ~> Route.seal {
post { completeOk } ~
authorize(false) { completeOk }
} ~> check {
status shouldEqual StatusCodes.MethodNotAllowed
responseAs[String] shouldEqual "HTTP method not allowed, supported methods: POST"
}
Get("/abc") ~> Route.seal {
authorize(false) { completeOk } ~
post { completeOk }
} ~> check { status shouldEqual StatusCodes.MethodNotAllowed }
}
}
}

View file

@ -1,19 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import org.scalatest.{ WordSpec, Suite, Matchers }
import akka.http.model.HttpResponse
import akka.http.testkit.ScalatestRouteTest
trait GenericRoutingSpec extends Matchers with Directives with ScalatestRouteTest { this: Suite
val Ok = HttpResponse()
val completeOk = complete(Ok)
def echoComplete[T]: T Route = { x complete(x.toString) }
def echoComplete2[T, U]: (T, U) Route = { (x, y) complete(s"$x $y") }
}
abstract class RoutingSpec extends WordSpec with GenericRoutingSpec

View file

@ -1,67 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import akka.http.marshallers.xml.ScalaXmlSupport
import akka.http.server.directives.AuthenticationDirectives._
import com.typesafe.config.{ ConfigFactory, Config }
import akka.actor.ActorSystem
import akka.stream.ActorFlowMaterializer
import akka.http.Http
object TestServer extends App {
val testConf: Config = ConfigFactory.parseString("""
akka.loglevel = INFO
akka.log-dead-letters = off""")
implicit val system = ActorSystem("ServerTest", testConf)
import system.dispatcher
implicit val materializer = ActorFlowMaterializer()
import ScalaXmlSupport._
import Directives._
def auth =
HttpBasicAuthenticator.provideUserName {
case p @ UserCredentials.Provided(name) p.verifySecret(name + "-password")
case _ false
}
val bindingFuture = Http().bindAndHandle({
get {
path("") {
complete(index)
} ~
path("secure") {
HttpBasicAuthentication("My very secure site")(auth) { user
complete(<html><body>Hello <b>{ user }</b>. Access has been granted!</body></html>)
}
} ~
path("ping") {
complete("PONG!")
} ~
path("crash") {
complete(sys.error("BOOM!"))
}
}
}, interface = "localhost", port = 8080)
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
Console.readLine()
bindingFuture.flatMap(_.unbind()).onComplete(_ system.shutdown())
lazy val index =
<html>
<body>
<h1>Say hello to <i>akka-http-core</i>!</h1>
<p>Defined resources:</p>
<ul>
<li><a href="/ping">/ping</a></li>
<li><a href="/secure">/secure</a> Use any username and '&lt;username&gt;-password' as credentials</li>
<li><a href="/crash">/crash</a></li>
</ul>
</body>
</html>
}

View file

@ -1,79 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model._
import akka.http.model.headers._
import akka.http.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
import akka.http.server.directives.AuthenticationDirectives._
import scala.concurrent.Future
class AuthenticationDirectivesSpec extends RoutingSpec {
val dontAuth = HttpBasicAuthentication("MyRealm")(HttpBasicAuthenticator[String](_ Future.successful(None)))
val doAuth = HttpBasicAuthentication("MyRealm")(HttpBasicAuthenticator.provideUserName(_ true))
val authWithAnonymous = doAuth.withAnonymousUser("We are Legion")
val challenge = HttpChallenge("Basic", "MyRealm")
"the 'HttpBasicAuthentication' directive" should {
"reject requests without Authorization header with an AuthenticationFailedRejection" in {
Get() ~> {
dontAuth { echoComplete }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) }
}
"reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in {
Get() ~> Authorization(BasicHttpCredentials("Bob", "")) ~> {
dontAuth { echoComplete }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) }
}
"reject requests with illegal Authorization header with 401" in {
Get() ~> RawHeader("Authorization", "bob alice") ~> Route.seal {
dontAuth { echoComplete }
} ~> check {
status shouldEqual StatusCodes.Unauthorized
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge))
}
}
"extract the object representing the user identity created by successful authentication" in {
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
doAuth { echoComplete }
} ~> check { responseAs[String] shouldEqual "Alice" }
}
"extract the object representing the user identity created for the anonymous user" in {
Get() ~> {
authWithAnonymous { echoComplete }
} ~> check { responseAs[String] shouldEqual "We are Legion" }
}
"properly handle exceptions thrown in its inner route" in {
object TestException extends RuntimeException
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
Route.seal {
doAuth { _ throw TestException }
}
} ~> check { status shouldEqual StatusCodes.InternalServerError }
}
}
"AuthenticationDirectives facilities" should {
"properly stack several authentication directives" in {
val otherChallenge = HttpChallenge("MyAuth", "MyRealm2")
val otherAuth: Directive1[String] = AuthenticationDirectives.authenticateOrRejectWithChallenge { (cred: Option[HttpCredentials])
Future.successful(Left(otherChallenge))
}
val bothAuth = dontAuth | otherAuth
Get() ~> Route.seal {
bothAuth { echoComplete }
} ~> check {
status shouldEqual StatusCodes.Unauthorized
headers.collect {
case `WWW-Authenticate`(challenge +: Nil) challenge
} shouldEqual Seq(challenge, otherChallenge)
}
}
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
class BasicDirectivesSpec extends RoutingSpec {
"The `mapUnmatchedPath` directive" should {
"map the unmatched path" in {
Get("/abc") ~> {
mapUnmatchedPath(_ / "def") {
path("abc" / "def") { completeOk }
}
} ~> check { response shouldEqual Ok }
}
}
"The `extract` directive" should {
"extract from the RequestContext" in {
Get("/abc") ~> {
extract(_.request.method.value) {
echoComplete
}
} ~> check { responseAs[String] shouldEqual "GET" }
}
}
}

View file

@ -1,186 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model._
import StatusCodes._
import akka.http.server
import headers._
import akka.http.util.DateTime
class CacheConditionDirectivesSpec extends RoutingSpec {
"the `conditional` directive" should {
val timestamp = DateTime.now - 2000
val ifUnmodifiedSince = `If-Unmodified-Since`(timestamp)
val ifModifiedSince = `If-Modified-Since`(timestamp)
val tag = EntityTag("fresh")
val responseHeaders = List(ETag(tag), `Last-Modified`(timestamp))
def taggedAndTimestamped = conditional(tag, timestamp) { completeOk }
def weak = conditional(tag.copy(weak = true), timestamp) { completeOk }
"return OK for new resources" in {
Get() ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
}
"return OK for non-matching resources" in {
Get() ~> `If-None-Match`(EntityTag("old")) ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-None-Match`(EntityTag("old")) ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
}
"ignore If-Modified-Since if If-None-Match is defined" in {
Get() ~> `If-None-Match`(tag) ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
}
Get() ~> `If-None-Match`(EntityTag("old")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual OK
}
}
"return PreconditionFailed for matched but unsafe resources" in {
Put() ~> `If-None-Match`(tag) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual PreconditionFailed
headers shouldEqual Nil
}
}
"return NotModified for matching resources" in {
Get() ~> `If-None-Match`.`*` ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-None-Match`(tag) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-None-Match`(tag) ~> `If-Modified-Since`(timestamp + 1000) ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-None-Match`(tag.copy(weak = true)) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-None-Match`(tag, EntityTag("some"), EntityTag("other")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
}
"return NotModified when only one matching header is set" in {
Get() ~> `If-None-Match`.`*` ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> `If-None-Match`(tag) ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
Get() ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (responseHeaders)
}
}
"return NotModified for matching weak resources" in {
val weakTag = tag.copy(weak = true)
Get() ~> `If-None-Match`(tag) ~> weak ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (List(ETag(weakTag), `Last-Modified`(timestamp)))
}
Get() ~> `If-None-Match`(weakTag) ~> weak ~> check {
status shouldEqual NotModified
headers should contain theSameElementsAs (List(ETag(weakTag), `Last-Modified`(timestamp)))
}
}
"return normally for matching If-Match/If-Unmodified" in {
Put() ~> `If-Match`.`*` ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
Put() ~> `If-Match`(tag) ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
Put() ~> ifUnmodifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual OK
headers should contain theSameElementsAs (responseHeaders)
}
}
"return PreconditionFailed for non-matching If-Match/If-Unmodified" in {
Put() ~> `If-Match`(EntityTag("old")) ~> taggedAndTimestamped ~> check {
status shouldEqual PreconditionFailed
headers shouldEqual Nil
}
Put() ~> `If-Unmodified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
status shouldEqual PreconditionFailed
headers shouldEqual Nil
}
}
"ignore If-Unmodified-Since if If-Match is defined" in {
Put() ~> `If-Match`(tag) ~> `If-Unmodified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
status shouldEqual OK
}
Put() ~> `If-Match`(EntityTag("old")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
status shouldEqual PreconditionFailed
}
}
"not filter out a `Range` header if `If-Range` does match the timestamp" in {
Get() ~> `If-Range`(timestamp) ~> Range(ByteRange(0, 10)) ~> {
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
} ~> check {
status shouldEqual OK
responseAs[String] should startWith("Some")
}
}
"filter out a `Range` header if `If-Range` doesn't match the timestamp" in {
Get() ~> `If-Range`(timestamp - 1000) ~> Range(ByteRange(0, 10)) ~> {
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
} ~> check {
status shouldEqual OK
responseAs[String] shouldEqual "None"
}
}
"not filter out a `Range` header if `If-Range` does match the ETag" in {
Get() ~> `If-Range`(tag) ~> Range(ByteRange(0, 10)) ~> {
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
} ~> check {
status shouldEqual OK
responseAs[String] should startWith("Some")
}
}
"filter out a `Range` header if `If-Range` doesn't match the ETag" in {
Get() ~> `If-Range`(EntityTag("other")) ~> Range(ByteRange(0, 10)) ~> {
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
} ~> check {
status shouldEqual OK
responseAs[String] shouldEqual "None"
}
}
}
}

View file

@ -1,413 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import org.scalatest.matchers.Matcher
import akka.util.ByteString
import akka.stream.scaladsl.Source
import akka.http.util._
import akka.http.model._
import akka.http.coding._
import headers._
import HttpEntity.{ ChunkStreamPart, Chunk }
import HttpCharsets._
import HttpEncodings._
import MediaTypes._
import StatusCodes._
import scala.concurrent.duration._
class CodingDirectivesSpec extends RoutingSpec {
val echoRequestContent: Route = { ctx ctx.complete(ctx.request.entity.dataBytes.utf8String) }
val yeah = complete("Yeah!")
lazy val yeahGzipped = compress("Yeah!", Gzip)
lazy val yeahDeflated = compress("Yeah!", Deflate)
lazy val helloGzipped = compress("Hello", Gzip)
lazy val helloDeflated = compress("Hello", Deflate)
"the NoEncoding decoder" should {
"decode the request content if it has encoding 'identity'" in {
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
decodeRequestWith(NoCoding) { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "yes" }
}
"reject requests with content encoded with 'deflate'" in {
Post("/", "yes") ~> `Content-Encoding`(deflate) ~> {
decodeRequestWith(NoCoding) { echoRequestContent }
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(identity) }
}
"decode the request content if no Content-Encoding header is present" in {
Post("/", "yes") ~> decodeRequestWith(NoCoding) { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
}
"leave request without content unchanged" in {
Post() ~> decodeRequestWith(Gzip) { completeOk } ~> check { response shouldEqual Ok }
}
}
"the Gzip decoder" should {
"decode the request content if it has encoding 'gzip'" in {
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decodeRequestWith(Gzip) { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "Hello" }
}
"reject the request content if it has encoding 'gzip' but is corrupt" in {
Post("/", fromHexDump("000102")) ~> `Content-Encoding`(gzip) ~> {
decodeRequestWith(Gzip) { echoRequestContent }
} ~> check {
status shouldEqual BadRequest
responseAs[String] shouldEqual "The request's encoding is corrupt"
}
}
"reject truncated gzip request content" in {
Post("/", helloGzipped.dropRight(2)) ~> `Content-Encoding`(gzip) ~> {
decodeRequestWith(Gzip) { echoRequestContent }
} ~> check {
status shouldEqual BadRequest
responseAs[String] shouldEqual "The request's encoding is corrupt"
}
}
"reject requests with content encoded with 'deflate'" in {
Post("/", "Hello") ~> `Content-Encoding`(deflate) ~> {
decodeRequestWith(Gzip) { completeOk }
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(gzip) }
}
"reject requests without Content-Encoding header" in {
Post("/", "Hello") ~> {
decodeRequestWith(Gzip) { completeOk }
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(gzip) }
}
"leave request without content unchanged" in {
Post() ~> {
decodeRequestWith(Gzip) { completeOk }
} ~> check { response shouldEqual Ok }
}
}
"a (decodeRequestWith(Gzip) | decodeRequestWith(NoEncoding)) compound directive" should {
lazy val decodeWithGzipOrNoEncoding = decodeRequestWith(Gzip) | decodeRequestWith(NoCoding)
"decode the request content if it has encoding 'gzip'" in {
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decodeWithGzipOrNoEncoding { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "Hello" }
}
"decode the request content if it has encoding 'identity'" in {
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
decodeWithGzipOrNoEncoding { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "yes" }
}
"decode the request content if no Content-Encoding header is present" in {
Post("/", "yes") ~> decodeWithGzipOrNoEncoding { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
}
"reject requests with content encoded with 'deflate'" in {
Post("/", "yes") ~> `Content-Encoding`(deflate) ~> {
decodeWithGzipOrNoEncoding { echoRequestContent }
} ~> check {
rejections shouldEqual Seq(
UnsupportedRequestEncodingRejection(gzip),
UnsupportedRequestEncodingRejection(identity))
}
}
}
"the Gzip encoder" should {
"encode the response content with GZIP if the client accepts it with a dedicated Accept-Encoding header" in {
Post() ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Gzip) { yeah }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"encode the response content with GZIP if the request has no Accept-Encoding header" in {
Post() ~> {
encodeResponseWith(Gzip) { yeah }
} ~> check { strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped) }
}
"reject the request if the client does not accept GZIP encoding" in {
Post() ~> `Accept-Encoding`(identity) ~> {
encodeResponseWith(Gzip) { completeOk }
} ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(gzip) }
}
"leave responses without content unchanged" in {
Post() ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Gzip) { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"leave responses with an already set Content-Encoding header unchanged" in {
Post() ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Gzip) {
RespondWithDirectives.respondWithHeader(`Content-Encoding`(identity)) { completeOk }
}
} ~> check { response shouldEqual Ok.withHeaders(`Content-Encoding`(identity)) }
}
"correctly encode the chunk stream produced by a chunked response" in {
val text = "This is a somewhat lengthy text that is being chunked by the autochunk directive!"
val textChunks =
() text.grouped(8).map { chars
Chunk(chars.mkString): ChunkStreamPart
}
val chunkedTextEntity = HttpEntity.Chunked(MediaTypes.`text/plain`, Source(textChunks))
Post() ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Gzip) {
complete(chunkedTextEntity)
}
} ~> check {
response should haveContentEncoding(gzip)
chunks.size shouldEqual (11 + 1) // 11 regular + the last one
val bytes = chunks.foldLeft(ByteString.empty)(_ ++ _.data)
Gzip.decode(bytes).awaitResult(1.second) should readAs(text)
}
}
}
"the encodeResponseWith(NoEncoding) directive" should {
"produce a response if no Accept-Encoding is present in the request" in {
Post() ~> encodeResponseWith(NoCoding) { completeOk } ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"produce a not encoded response if the client only accepts non matching encodings" in {
Post() ~> `Accept-Encoding`(gzip, identity) ~> {
encodeResponseWith(NoCoding) { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
Post() ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Deflate, NoCoding) { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"reject the request if the request has an 'Accept-Encoding: identity; q=0' header" in {
Post() ~> `Accept-Encoding`(identity.withQValue(0f)) ~> {
encodeResponseWith(NoCoding) { completeOk }
} ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(identity) }
}
}
"a (encodeResponse(Gzip) | encodeResponse(NoEncoding)) compound directive" should {
lazy val encodeGzipOrIdentity = encodeResponseWith(Gzip) | encodeResponseWith(NoCoding)
"produce a not encoded response if the request has no Accept-Encoding header" in {
Post() ~> {
encodeGzipOrIdentity { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"produce a GZIP encoded response if the request has an `Accept-Encoding: deflate;q=0.5, gzip` header" in {
Post() ~> `Accept-Encoding`(deflate.withQValue(.5f), gzip) ~> {
encodeGzipOrIdentity { yeah }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a non-encoded response if the request has an `Accept-Encoding: identity` header" in {
Post() ~> `Accept-Encoding`(identity) ~> {
encodeGzipOrIdentity { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"produce a non-encoded response if the request has an `Accept-Encoding: deflate` header" in {
Post() ~> `Accept-Encoding`(deflate) ~> {
encodeGzipOrIdentity { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
}
"the encodeResponse directive" should {
"produce a non-encoded response if the request has no Accept-Encoding header" in {
Get("/") ~> {
encodeResponse { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"produce a GZIP encoded response if the request has an `Accept-Encoding: gzip, deflate` header" in {
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> {
encodeResponse { yeah }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
encodeResponse { yeah }
} ~> check {
response should haveContentEncoding(deflate)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
}
}
}
"the encodeResponseWith directive" should {
"produce a response encoded with the specified Encoder if the request has a matching Accept-Encoding header" in {
Get("/") ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Gzip) { yeah }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a response encoded with one of the specified Encoders if the request has a matching Accept-Encoding header" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
encodeResponseWith(Gzip, Deflate) { yeah }
} ~> check {
response should haveContentEncoding(deflate)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
}
}
"produce a response encoded with the first of the specified Encoders if the request has no Accept-Encoding header" in {
Get("/") ~> {
encodeResponseWith(Gzip, Deflate) { yeah }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a response with no encoding if the request has an empty Accept-Encoding header" in {
Get("/") ~> `Accept-Encoding`() ~> {
encodeResponseWith(Gzip, Deflate, NoCoding) { completeOk }
} ~> check {
response shouldEqual Ok
response should haveNoContentEncoding
}
}
"negotiate the correct content encoding" in {
Get("/") ~> `Accept-Encoding`(identity.withQValue(.5f), deflate.withQValue(0f), gzip) ~> {
encodeResponseWith(NoCoding, Deflate, Gzip) { yeah }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
encodeResponseWith(Gzip) { yeah }
} ~> check {
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
}
}
"reject the request if it has an Accept-Encoding header with an encoding that matches but is blacklisted" in {
Get("/") ~> `Accept-Encoding`(gzip.withQValue(0f)) ~> {
encodeResponseWith(Gzip) { yeah }
} ~> check {
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
}
}
}
"the decodeRequest directive" should {
"decode the request content if it has a `Content-Encoding: gzip` header and the content is gzip encoded" in {
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decodeRequest { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "Hello" }
}
"decode the request content if it has a `Content-Encoding: deflate` header and the content is deflate encoded" in {
Post("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> {
decodeRequest { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "Hello" }
}
"decode the request content if it has a `Content-Encoding: identity` header and the content is not encoded" in {
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
decodeRequest { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "yes" }
}
"decode the request content using NoEncoding if no Content-Encoding header is present" in {
Post("/", "yes") ~> decodeRequest { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
}
"reject the request if it has a `Content-Encoding: deflate` header but the request is encoded with Gzip" in {
Post("/", helloGzipped) ~> `Content-Encoding`(deflate) ~>
decodeRequest { echoRequestContent } ~> check {
status shouldEqual BadRequest
responseAs[String] shouldEqual "The request's encoding is corrupt"
}
}
}
"the decodeRequestWith directive" should {
"decode the request content if its `Content-Encoding` header matches the specified encoder" in {
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decodeRequestWith(Gzip) { echoRequestContent }
} ~> check { responseAs[String] shouldEqual "Hello" }
}
"reject the request if its `Content-Encoding` header doesn't match the specified encoder" in {
Post("/", helloGzipped) ~> `Content-Encoding`(deflate) ~> {
decodeRequestWith(Gzip) { echoRequestContent }
} ~> check {
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
}
}
"reject the request when decodeing with GZIP and no Content-Encoding header is present" in {
Post("/", "yes") ~> decodeRequestWith(Gzip) { echoRequestContent } ~> check {
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
}
}
}
"the (decodeRequest & encodeResponse) compound directive" should {
lazy val decodeEncode = decodeRequest & encodeResponse
"decode a GZIP encoded request and produce a none encoded response if the request has no Accept-Encoding header" in {
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decodeEncode { echoRequestContent }
} ~> check {
response should haveNoContentEncoding
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hello")
}
}
"decode a GZIP encoded request and produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> `Accept-Encoding`(deflate) ~> {
decodeEncode { echoRequestContent }
} ~> check {
response should haveContentEncoding(deflate)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), helloDeflated)
}
}
"decode an unencoded request and produce a GZIP encoded response if the request has an `Accept-Encoding: gzip` header" in {
Post("/", "Hello") ~> `Accept-Encoding`(gzip) ~> {
decodeEncode { echoRequestContent }
} ~> check {
response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), helloGzipped)
}
}
}
def compress(input: String, encoder: Encoder): ByteString = {
val compressor = encoder.newCompressor
compressor.compressAndFlush(ByteString(input)) ++ compressor.finish()
}
def hexDump(bytes: Array[Byte]) = bytes.map("%02x" format _).mkString
def fromHexDump(dump: String) = dump.grouped(2).toArray.map(chars Integer.parseInt(new String(chars), 16).toByte)
def haveNoContentEncoding: Matcher[HttpResponse] = be(None) compose { (_: HttpResponse).header[`Content-Encoding`] }
def haveContentEncoding(encoding: HttpEncoding): Matcher[HttpResponse] =
be(Some(`Content-Encoding`(encoding))) compose { (_: HttpResponse).header[`Content-Encoding`] }
def readAs(string: String, charset: String = "UTF8") = be(string) compose { (_: ByteString).decodeString(charset) }
def strictify(entity: HttpEntity) = entity.toStrict(1.second).awaitResult(1.second)
}

View file

@ -1,96 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model._
import StatusCodes.OK
import headers._
import akka.http.util.DateTime
class CookieDirectivesSpec extends RoutingSpec {
val deletedTimeStamp = DateTime.fromIsoDateTimeString("1800-01-01T00:00:00")
"The 'cookie' directive" should {
"extract the respectively named cookie" in {
Get() ~> addHeader(Cookie(HttpCookie("fancy", "pants"))) ~> {
cookie("fancy") { echoComplete }
} ~> check { responseAs[String] shouldEqual "fancy=pants" }
}
"reject the request if the cookie is not present" in {
Get() ~> {
cookie("fancy") { echoComplete }
} ~> check { rejection shouldEqual MissingCookieRejection("fancy") }
}
"properly pass through inner rejections" in {
Get() ~> addHeader(Cookie(HttpCookie("fancy", "pants"))) ~> {
cookie("fancy") { c reject(ValidationRejection("Dont like " + c.content)) }
} ~> check { rejection shouldEqual ValidationRejection("Dont like pants") }
}
}
"The 'deleteCookie' directive" should {
"add a respective Set-Cookie headers to successful responses" in {
Get() ~> {
deleteCookie("myCookie", "test.com") { completeOk }
} ~> check {
status shouldEqual OK
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("myCookie", "deleted", expires = deletedTimeStamp,
domain = Some("test.com"))))
}
}
"support deleting multiple cookies at a time" in {
Get() ~> {
deleteCookie(HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com")) { completeOk }
} ~> check {
status shouldEqual OK
headers.collect { case `Set-Cookie`(x) x } shouldEqual List(
HttpCookie("myCookie", "deleted", expires = deletedTimeStamp),
HttpCookie("myCookie2", "deleted", expires = deletedTimeStamp))
}
}
}
"The 'optionalCookie' directive" should {
"produce a `Some(cookie)` extraction if the cookie is present" in {
Get() ~> Cookie(HttpCookie("abc", "123")) ~> {
optionalCookie("abc") { echoComplete }
} ~> check { responseAs[String] shouldEqual "Some(abc=123)" }
}
"produce a `None` extraction if the cookie is not present" in {
Get() ~> optionalCookie("abc") { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
}
"let rejections from its inner route pass through" in {
Get() ~> {
optionalCookie("test-cookie") { _
validate(false, "ouch") { completeOk }
}
} ~> check { rejection shouldEqual ValidationRejection("ouch") }
}
}
"The 'setCookie' directive" should {
"add a respective Set-Cookie headers to successful responses" in {
Get() ~> {
setCookie(HttpCookie("myCookie", "test.com")) { completeOk }
} ~> check {
status shouldEqual OK
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("myCookie", "test.com")))
}
}
"support setting multiple cookies at a time" in {
Get() ~> {
setCookie(HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com")) { completeOk }
} ~> check {
status shouldEqual OK
headers.collect { case `Set-Cookie`(x) x } shouldEqual List(
HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com"))
}
}
}
}

View file

@ -1,77 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.event.LoggingAdapter
import akka.http.util._
class DebuggingDirectivesSpec extends RoutingSpec {
var debugMsg = ""
def resetDebugMsg(): Unit = { debugMsg = "" }
val log = new LoggingAdapter {
def isErrorEnabled = true
def isWarningEnabled = true
def isInfoEnabled = true
def isDebugEnabled = true
def notifyError(message: String): Unit = {}
def notifyError(cause: Throwable, message: String): Unit = {}
def notifyWarning(message: String): Unit = {}
def notifyInfo(message: String): Unit = {}
def notifyDebug(message: String): Unit = { debugMsg += message + '\n' }
}
"The 'logRequest' directive" should {
"produce a proper log message for incoming requests" in {
val route =
withLog(log)(
logRequest("1")(
completeOk))
resetDebugMsg()
Get("/hello") ~> route ~> check {
response shouldEqual Ok
debugMsg shouldEqual "1: HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))\n"
}
}
}
"The 'logResponse' directive" should {
"produce a proper log message for outgoing responses" in {
val route =
withLog(log)(
logResult("2")(
completeOk))
resetDebugMsg()
Get("/hello") ~> route ~> check {
response shouldEqual Ok
debugMsg shouldEqual "2: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))\n"
}
}
}
"The 'logRequestResponse' directive" should {
"produce proper log messages for outgoing responses, thereby showing the corresponding request" in {
val route =
withLog(log)(
logRequestResult("3")(
completeOk))
resetDebugMsg()
Get("/hello") ~> route ~> check {
response shouldEqual Ok
debugMsg shouldEqual """|3: Response for
| Request : HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))
| Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))
|""".stripMarginWithNewline("\n")
}
}
}
}

View file

@ -1,104 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model.{ MediaTypes, MediaRanges, StatusCodes }
import akka.http.model.headers._
import scala.concurrent.Future
class ExecutionDirectivesSpec extends RoutingSpec {
object MyException extends RuntimeException
val handler =
ExceptionHandler {
case MyException complete(500, "Pling! Plong! Something went wrong!!!")
}
"The `handleExceptions` directive" should {
"handle an exception strictly thrown in the inner route with the supplied exception handler" in {
exceptionShouldBeHandled {
handleExceptions(handler) { ctx
throw MyException
}
}
}
"handle an Future.failed RouteResult with the supplied exception handler" in {
exceptionShouldBeHandled {
handleExceptions(handler) { ctx
Future.failed(MyException)
}
}
}
"handle an eventually failed Future[RouteResult] with the supplied exception handler" in {
exceptionShouldBeHandled {
handleExceptions(handler) { ctx
Future {
Thread.sleep(100)
throw MyException
}
}
}
}
"handle an exception happening during route building" in {
exceptionShouldBeHandled {
get {
handleExceptions(handler) {
throw MyException
}
}
}
}
"not interfere with alternative routes" in {
Get("/abc") ~>
get {
handleExceptions(handler)(reject) ~ { ctx
throw MyException
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "There was an internal server error."
}
}
"not handle other exceptions" in {
Get("/abc") ~>
get {
handleExceptions(handler) {
throw new RuntimeException
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "There was an internal server error."
}
}
"always fall back to a default content type" in {
Get("/abc") ~> Accept(MediaTypes.`application/json`) ~>
get {
handleExceptions(handler) {
throw new RuntimeException
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "There was an internal server error."
}
Get("/abc") ~> Accept(MediaTypes.`text/xml`, MediaRanges.`*/*`.withQValue(0f)) ~>
get {
handleExceptions(handler) {
throw new RuntimeException
}
} ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "There was an internal server error."
}
}
}
def exceptionShouldBeHandled(route: Route) =
Get("/abc") ~> route ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "Pling! Plong! Something went wrong!!!"
}
}

View file

@ -1,327 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import java.io.{ File, FileOutputStream }
import akka.http.model.MediaTypes._
import akka.http.model._
import akka.http.model.headers._
import akka.http.util._
import org.scalatest.matchers.Matcher
import org.scalatest.{ Inside, Inspectors }
import scala.concurrent.duration._
import scala.concurrent.{ Await, ExecutionContext, Future }
import scala.util.Properties
class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Inside {
override def testConfigSource =
"""akka.http.routing {
| file-chunking-threshold-size = 16
| file-chunking-chunk-size = 8
| range-coalescing-threshold = 1
|}""".stripMargin
"getFromFile" should {
"reject non-GET requests" in {
Put() ~> getFromFile("some") ~> check { handled shouldEqual (false) }
}
"reject requests to non-existing files" in {
Get() ~> getFromFile("nonExistentFile") ~> check { handled shouldEqual (false) }
}
"reject requests to directories" in {
Get() ~> getFromFile(Properties.javaHome) ~> check { handled shouldEqual (false) }
}
"return the file content with the MediaType matching the file extension" in {
val file = File.createTempFile("akkaHttpTest", ".PDF")
try {
writeAllText("This is PDF", file)
Get() ~> getFromFile(file.getPath) ~> check {
mediaType shouldEqual `application/pdf`
definedCharset shouldEqual None
responseAs[String] shouldEqual "This is PDF"
headers should contain(`Last-Modified`(DateTime(file.lastModified)))
}
} finally file.delete
}
"return the file content with MediaType 'application/octet-stream' on unknown file extensions" in {
val file = File.createTempFile("akkaHttpTest", null)
try {
writeAllText("Some content", file)
Get() ~> getFromFile(file) ~> check {
mediaType shouldEqual `application/octet-stream`
responseAs[String] shouldEqual "Some content"
}
} finally file.delete
}
"return a single range from a file" in {
val file = File.createTempFile("partialTest", null)
try {
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
Get() ~> addHeader(Range(ByteRange(0, 10))) ~> getFromFile(file) ~> check {
status shouldEqual StatusCodes.PartialContent
headers should contain(`Content-Range`(ContentRange(0, 10, 26)))
responseAs[String] shouldEqual "ABCDEFGHIJK"
}
} finally file.delete
}
"return multiple ranges from a file at once" in {
val file = File.createTempFile("partialTest", null)
try {
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
val rangeHeader = Range(ByteRange(1, 10), ByteRange.suffix(10))
Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain`) ~> check {
status shouldEqual StatusCodes.PartialContent
header[`Content-Range`] shouldEqual None
mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges`
val parts = responseAs[Multipart.ByteRanges].toStrict(1.second).awaitResult(3.seconds).strictParts
parts.size shouldEqual 2
parts(0).entity.data.utf8String shouldEqual "BCDEFGHIJK"
parts(1).entity.data.utf8String shouldEqual "QRSTUVWXYZ"
}
} finally file.delete
}
}
"getFromResource" should {
"reject non-GET requests" in {
Put() ~> getFromResource("some") ~> check { handled shouldEqual (false) }
}
"reject requests to non-existing resources" in {
Get() ~> getFromResource("nonExistingResource") ~> check { handled shouldEqual (false) }
}
"return the resource content with the MediaType matching the file extension" in {
val route = getFromResource("sample.html")
def runCheck() =
Get() ~> route ~> check {
mediaType shouldEqual `text/html`
forAtLeast(1, headers) { h
inside(h) {
case `Last-Modified`(dt)
DateTime(2011, 7, 1) should be < dt
dt.clicks should be < System.currentTimeMillis()
}
}
responseAs[String] shouldEqual "<p>Lorem ipsum!</p>"
}
runCheck()
runCheck() // additional test to check that no internal state is kept
}
"return the file content with MediaType 'application/octet-stream' on unknown file extensions" in {
Get() ~> getFromResource("sample.xyz") ~> check {
mediaType shouldEqual `application/octet-stream`
responseAs[String] shouldEqual "XyZ"
}
}
}
"getFromResourceDirectory" should {
"reject requests to non-existing resources" in {
Get("not/found") ~> getFromResourceDirectory("subDirectory") ~> check { handled shouldEqual (false) }
}
val verify = check {
mediaType shouldEqual `application/pdf`
responseAs[String] shouldEqual "123"
}
"return the resource content with the MediaType matching the file extension - example 1" in { Get("empty.pdf") ~> getFromResourceDirectory("subDirectory") ~> verify }
"return the resource content with the MediaType matching the file extension - example 2" in { Get("empty.pdf") ~> getFromResourceDirectory("subDirectory/") ~> verify }
"return the resource content with the MediaType matching the file extension - example 3" in { Get("subDirectory/empty.pdf") ~> getFromResourceDirectory("") ~> verify }
"reject requests to directory resources" in {
Get() ~> getFromResourceDirectory("subDirectory") ~> check { handled shouldEqual (false) }
}
}
"listDirectoryContents" should {
val base = new File(getClass.getClassLoader.getResource("").toURI).getPath
new File(base, "subDirectory/emptySub").mkdir()
def eraseDateTime(s: String) = s.replaceAll("""\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d""", "xxxx-xx-xx xx:xx:xx")
implicit val settings = RoutingSettings.default.copy(renderVanityFooter = false)
"properly render a simple directory" in {
Get() ~> listDirectoryContents(base + "/someDir") ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /</title></head>
|<body>
|<h1>Index of /</h1>
|<hr>
|<pre>
|<a href="/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|<a href="/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|<a href="/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"properly render a sub directory" in {
Get("/sub/") ~> listDirectoryContents(base + "/someDir") ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /sub/</title></head>
|<body>
|<h1>Index of /sub/</h1>
|<hr>
|<pre>
|<a href="/">../</a>
|<a href="/sub/file.html">file.html</a> xxxx-xx-xx xx:xx:xx 0 B
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"properly render the union of several directories" in {
Get() ~> listDirectoryContents(base + "/someDir", base + "/subDirectory") ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /</title></head>
|<body>
|<h1>Index of /</h1>
|<hr>
|<pre>
|<a href="/emptySub/">emptySub/</a> xxxx-xx-xx xx:xx:xx
|<a href="/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|<a href="/empty.pdf">empty.pdf</a> xxxx-xx-xx xx:xx:xx 3 B
|<a href="/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|<a href="/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"properly render an empty sub directory with vanity footer" in {
val settings = 0 // shadow implicit
Get("/emptySub/") ~> listDirectoryContents(base + "/subDirectory") ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /emptySub/</title></head>
|<body>
|<h1>Index of /emptySub/</h1>
|<hr>
|<pre>
|<a href="/">../</a>
|</pre>
|<hr>
|<div style="width:100%;text-align:right;color:gray">
|<small>rendered by <a href="http://akka.io">Akka Http</a> on xxxx-xx-xx xx:xx:xx</small>
|</div>
|</body>
|</html>
|"""
}
}
}
"properly render an empty top-level directory" in {
Get() ~> listDirectoryContents(base + "/subDirectory/emptySub") ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /</title></head>
|<body>
|<h1>Index of /</h1>
|<hr>
|<pre>
|(no files)
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"properly render a simple directory with a path prefix" in {
Get("/files/") ~> pathPrefix("files")(listDirectoryContents(base + "/someDir")) ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /files/</title></head>
|<body>
|<h1>Index of /files/</h1>
|<hr>
|<pre>
|<a href="/files/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|<a href="/files/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|<a href="/files/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"properly render a sub directory with a path prefix" in {
Get("/files/sub/") ~> pathPrefix("files")(listDirectoryContents(base + "/someDir")) ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /files/sub/</title></head>
|<body>
|<h1>Index of /files/sub/</h1>
|<hr>
|<pre>
|<a href="/files/">../</a>
|<a href="/files/sub/file.html">file.html</a> xxxx-xx-xx xx:xx:xx 0 B
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"properly render an empty top-level directory with a path prefix" in {
Get("/files/") ~> pathPrefix("files")(listDirectoryContents(base + "/subDirectory/emptySub")) ~> check {
eraseDateTime(responseAs[String]) shouldEqual prep {
"""<html>
|<head><title>Index of /files/</title></head>
|<body>
|<h1>Index of /files/</h1>
|<hr>
|<pre>
|(no files)
|</pre>
|<hr>
|</body>
|</html>
|"""
}
}
}
"reject requests to file resources" in {
Get() ~> listDirectoryContents(base + "subDirectory/empty.pdf") ~> check { handled shouldEqual (false) }
}
}
def prep(s: String) = s.stripMarginWithNewline("\n")
def writeAllText(text: String, file: File): Unit = {
val fos = new FileOutputStream(file)
try {
fos.write(text.getBytes("UTF-8"))
} finally fos.close()
}
def evaluateTo[T](t: T, atMost: Duration = 100.millis)(implicit ec: ExecutionContext): Matcher[Future[T]] =
be(t).compose[Future[T]] { fut
import scala.concurrent.Await
fut.awaitResult(atMost)
}
}

View file

@ -1,140 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.common.StrictForm
import akka.http.marshallers.xml.ScalaXmlSupport
import akka.http.unmarshalling.Unmarshaller.HexInt
import akka.http.model._
import MediaTypes._
class FormFieldDirectivesSpec extends RoutingSpec {
implicit val nodeSeqUnmarshaller =
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`, `text/html`, `text/plain`)
val nodeSeq: xml.NodeSeq = <b>yes</b>
val urlEncodedForm = FormData(Map("firstName" -> "Mike", "age" -> "42"))
val urlEncodedFormWithVip = FormData(Map("firstName" -> "Mike", "age" -> "42", "VIP" -> "true", "super" -> "<b>no</b>"))
val multipartForm = Multipart.FormData {
Map(
"firstName" -> HttpEntity("Mike"),
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
"VIPBoolean" -> HttpEntity("true"))
}
val multipartFormWithTextHtml = Multipart.FormData {
Map(
"firstName" -> HttpEntity("Mike"),
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
"VIP" -> HttpEntity(`text/html`, "<b>yes</b>"),
"super" -> HttpEntity("no"))
}
val multipartFormWithFile = Multipart.FormData(
Multipart.FormData.BodyPart.Strict("file", HttpEntity(MediaTypes.`text/xml`, "<int>42</int>"),
Map("filename" -> "age.xml")))
"The 'formFields' extraction directive" should {
"properly extract the value of www-urlencoded form fields" in {
Post("/", urlEncodedForm) ~> {
formFields('firstName, "age".as[Int], 'sex?, "VIP" ? false) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check { responseAs[String] shouldEqual "Mike42Nonefalse" }
}
"properly extract the value of www-urlencoded form fields when an explicit unmarshaller is given" in {
Post("/", urlEncodedForm) ~> {
formFields('firstName, "age".as(HexInt), 'sex?, "VIP" ? false) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check { responseAs[String] shouldEqual "Mike66Nonefalse" }
}
"properly extract the value of multipart form fields" in {
Post("/", multipartForm) ~> {
formFields('firstName, "age", 'sex?, "VIP" ? nodeSeq) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check { responseAs[String] shouldEqual "Mike<int>42</int>None<b>yes</b>" }
}
"extract StrictForm.FileData from a multipart part" in {
Post("/", multipartFormWithFile) ~> {
formFields('file.as[StrictForm.FileData]) {
case StrictForm.FileData(name, HttpEntity.Strict(ct, data))
complete(s"type ${ct.mediaType} length ${data.length} filename ${name.get}")
}
} ~> check { responseAs[String] shouldEqual "type text/xml length 13 filename age.xml" }
}
"reject the request with a MissingFormFieldRejection if a required form field is missing" in {
Post("/", urlEncodedForm) ~> {
formFields('firstName, "age", 'sex, "VIP" ? false) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check { rejection shouldEqual MissingFormFieldRejection("sex") }
}
"properly extract the value if only a urlencoded deserializer is available for a multipart field that comes without a" +
"Content-Type (or text/plain)" in {
Post("/", multipartForm) ~> {
formFields('firstName, "age", 'sex?, "VIPBoolean" ? false) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check {
responseAs[String] shouldEqual "Mike<int>42</int>Nonetrue"
}
}
"work even if only a FromStringUnmarshaller is available for a multipart field with custom Content-Type" in {
Post("/", multipartFormWithTextHtml) ~> {
formFields(('firstName, "age", 'super ? false)) { (firstName, age, vip)
complete(firstName + age + vip)
}
} ~> check {
responseAs[String] shouldEqual "Mike<int>42</int>false"
}
}
"work even if only a FromEntityUnmarshaller is available for a www-urlencoded field" in {
Post("/", urlEncodedFormWithVip) ~> {
formFields('firstName, "age", 'sex?, "super" ? nodeSeq) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check {
responseAs[String] shouldEqual "Mike42None<b>no</b>"
}
}
}
"The 'formField' requirement directive" should {
"block requests that do not contain the required formField" in {
Post("/", urlEncodedForm) ~> {
formFields('name ! "Mr. Mike") { completeOk }
} ~> check { handled shouldEqual false }
}
"block requests that contain the required parameter but with an unmatching value" in {
Post("/", urlEncodedForm) ~> {
formFields('firstName ! "Pete") { completeOk }
} ~> check { handled shouldEqual false }
}
"let requests pass that contain the required parameter with its required value" in {
Post("/", urlEncodedForm) ~> {
formFields('firstName ! "Mike") { completeOk }
} ~> check { response shouldEqual Ok }
}
}
"The 'formField' requirement with explicit unmarshaller directive" should {
"block requests that do not contain the required formField" in {
Post("/", urlEncodedForm) ~> {
formFields('oldAge.as(HexInt) ! 78) { completeOk }
} ~> check { handled shouldEqual false }
}
"block requests that contain the required parameter but with an unmatching value" in {
Post("/", urlEncodedForm) ~> {
formFields('age.as(HexInt) ! 78) { completeOk }
} ~> check { handled shouldEqual false }
}
"let requests pass that contain the required parameter with its required value" in {
Post("/", urlEncodedForm) ~> {
formFields('age.as(HexInt) ! 66 /* hex! */ ) { completeOk }
} ~> check { response shouldEqual Ok }
}
}
}

View file

@ -1,106 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model.StatusCodes
import scala.concurrent.Future
class FutureDirectivesSpec extends RoutingSpec {
class TestException(msg: String) extends Exception(msg)
object TestException extends Exception("XXX")
def throwTestException[T](msgPrefix: String): T Nothing = t throw new TestException(msgPrefix + t)
implicit val exceptionHandler = ExceptionHandler {
case e: TestException complete(StatusCodes.InternalServerError, "Oops. " + e)
}
"The `onComplete` directive" should {
"unwrap a Future in the success case" in {
var i = 0
def nextNumber() = { i += 1; i }
val route = onComplete(Future.successful(nextNumber())) { echoComplete }
Get() ~> route ~> check {
responseAs[String] shouldEqual "Success(1)"
}
Get() ~> route ~> check {
responseAs[String] shouldEqual "Success(2)"
}
}
"unwrap a Future in the failure case" in {
Get() ~> onComplete(Future.failed[String](new RuntimeException("no"))) { echoComplete } ~> check {
responseAs[String] shouldEqual "Failure(java.lang.RuntimeException: no)"
}
}
"catch an exception in the success case" in {
Get() ~> onComplete(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "Oops. akka.http.server.directives.FutureDirectivesSpec$TestException: EX when Success(ok)"
}
}
"catch an exception in the failure case" in {
Get() ~> onComplete(Future.failed[String](new RuntimeException("no"))) { throwTestException("EX when ") } ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "Oops. akka.http.server.directives.FutureDirectivesSpec$TestException: EX when Failure(java.lang.RuntimeException: no)"
}
}
}
"The `onSuccess` directive" should {
"unwrap a Future in the success case" in {
Get() ~> onSuccess(Future.successful("yes")) { echoComplete } ~> check {
responseAs[String] shouldEqual "yes"
}
}
"propagate the exception in the failure case" in {
Get() ~> onSuccess(Future.failed(TestException)) { echoComplete } ~> check {
status shouldEqual StatusCodes.InternalServerError
}
}
"catch an exception in the success case" in {
Get() ~> onSuccess(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "Oops. akka.http.server.directives.FutureDirectivesSpec$TestException: EX when ok"
}
}
"catch an exception in the failure case" in {
Get() ~> onSuccess(Future.failed(TestException)) { throwTestException("EX when ") } ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "There was an internal server error."
}
}
}
"The `completeOrRecoverWith` directive" should {
"complete the request with the Future's value if the future succeeds" in {
Get() ~> completeOrRecoverWith(Future.successful("yes")) { echoComplete } ~> check {
responseAs[String] shouldEqual "yes"
}
}
"don't call the inner route if the Future succeeds" in {
Get() ~> completeOrRecoverWith(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual "ok"
}
}
"recover using the inner route if the Future fails" in {
val route = completeOrRecoverWith(Future.failed[String](TestException)) {
case e complete(s"Exception occurred: ${e.getMessage}")
}
Get() ~> route ~> check {
responseAs[String] shouldEqual "Exception occurred: XXX"
}
}
"catch an exception during recovery" in {
Get() ~> completeOrRecoverWith(Future.failed[String](TestException)) { throwTestException("EX when ") } ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "Oops. akka.http.server.directives.FutureDirectivesSpec$TestException: EX when akka.http.server.directives.FutureDirectivesSpec$TestException$: XXX"
}
}
}
}

View file

@ -1,100 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.directives
import akka.http.model._
import headers._
import akka.http.server._
import org.scalatest.Inside
class HeaderDirectivesSpec extends RoutingSpec with Inside {
"The headerValuePF directive" should {
lazy val myHeaderValue = headerValuePF { case Connection(tokens) tokens.head }
"extract the respective header value if a matching request header is present" in {
Get("/abc") ~> addHeader(Connection("close")) ~> myHeaderValue { echoComplete } ~> check {
responseAs[String] shouldEqual "close"
}
}
"reject with an empty rejection set if no matching request header is present" in {
Get("/abc") ~> myHeaderValue { echoComplete } ~> check { rejections shouldEqual Nil }
}
"reject with a MalformedHeaderRejection if the extract function throws an exception" in {
Get("/abc") ~> addHeader(Connection("close")) ~> {
(headerValuePF { case _ sys.error("Naah!") }) { echoComplete }
} ~> check {
inside(rejection) { case MalformedHeaderRejection("Connection", "Naah!", _) }
}
}
}
"The headerValueByType directive" should {
lazy val route =
headerValueByType[Origin]() { origin
complete(s"The first origin was ${origin.origins.head}")
}
"extract a header if the type is matching" in {
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
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" in {
Get("abc") ~> route ~> check {
inside(rejection) {
case MissingHeaderRejection("Origin")
}
}
}
}
"The optionalHeaderValue directive" should {
lazy val myHeaderValue = optionalHeaderValue {
case Connection(tokens) Some(tokens.head)
case _ None
}
"extract the respective header value if a matching request header is present" in {
Get("/abc") ~> addHeader(Connection("close")) ~> myHeaderValue { echoComplete } ~> check {
responseAs[String] shouldEqual "Some(close)"
}
}
"extract None if no matching request header is present" in {
Get("/abc") ~> myHeaderValue { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
}
"reject with a MalformedHeaderRejection if the extract function throws an exception" in {
Get("/abc") ~> addHeader(Connection("close")) ~> {
val myHeaderValue = optionalHeaderValue { case _ sys.error("Naaah!") }
myHeaderValue { echoComplete }
} ~> check {
inside(rejection) { case MalformedHeaderRejection("Connection", "Naaah!", _) }
}
}
}
"The optionalHeaderValueByType directive" should {
val route =
optionalHeaderValueByType[Origin]() {
case Some(origin) complete(s"The first origin was ${origin.origins.head}")
case None complete("No Origin header found.")
}
"extract Some(header) if the type is matching" in {
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
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" in {
Get("abc") ~> route ~> check {
responseAs[String] shouldEqual "No Origin header found."
}
}
}
}

View file

@ -1,55 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model.headers.Host
import org.scalatest.FreeSpec
class HostDirectivesSpec extends FreeSpec with GenericRoutingSpec {
"The 'host' directive" - {
"in its simple String form should" - {
"block requests to unmatched hosts" in {
Get() ~> Host("spray.io") ~> {
host("spray.com") { completeOk }
} ~> check { handled shouldEqual false }
}
"let requests to matching hosts pass" in {
Get() ~> Host("spray.io") ~> {
host("spray.com", "spray.io") { completeOk }
} ~> check { response shouldEqual Ok }
}
}
"in its simple RegEx form" - {
"block requests to unmatched hosts" in {
Get() ~> Host("spray.io") ~> {
host("hairspray.*".r) { echoComplete }
} ~> check { handled shouldEqual false }
}
"let requests to matching hosts pass and extract the full host" in {
Get() ~> Host("spray.io") ~> {
host("spra.*".r) { echoComplete }
} ~> check { responseAs[String] shouldEqual "spray.io" }
}
}
"in its group RegEx form" - {
"block requests to unmatched hosts" in {
Get() ~> Host("spray.io") ~> {
host("hairspray(.*)".r) { echoComplete }
} ~> check { handled shouldEqual false }
}
"let requests to matching hosts pass and extract the full host" in {
Get() ~> Host("spray.io") ~> {
host("spra(.*)".r) { echoComplete }
} ~> check { responseAs[String] shouldEqual "y.io" }
}
}
}
}

View file

@ -1,175 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import scala.xml.NodeSeq
import org.scalatest.Inside
import akka.http.marshallers.xml.ScalaXmlSupport
import akka.http.unmarshalling._
import akka.http.marshalling._
import akka.http.model._
import akka.http.marshallers.sprayjson.SprayJsonSupport._
import MediaTypes._
import HttpCharsets._
import headers._
import spray.json.DefaultJsonProtocol._
class MarshallingDirectivesSpec extends RoutingSpec with Inside {
import ScalaXmlSupport._
private val iso88592 = HttpCharsets.getForKey("iso-8859-2").get
implicit val IntUnmarshaller: FromEntityUnmarshaller[Int] =
nodeSeqUnmarshaller(ContentTypeRange(`text/xml`, iso88592), `text/html`) map {
case NodeSeq.Empty throw Unmarshaller.NoContentException
case x { val i = x.text.toInt; require(i >= 0); i }
}
implicit val IntMarshaller: ToEntityMarshaller[Int] =
Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType
nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) <int>{ i }</int> }
}
"The 'entityAs' directive" should {
"extract an object from the requests entity using the in-scope Unmarshaller" in {
Put("/", <p>cool</p>) ~> {
entity(as[NodeSeq]) { echoComplete }
} ~> check { responseAs[String] shouldEqual "<p>cool</p>" }
}
"return a RequestEntityExpectedRejection rejection if the request has no entity" in {
Put() ~> {
entity(as[Int]) { echoComplete }
} ~> check { rejection shouldEqual RequestEntityExpectedRejection }
}
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in {
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
entity(as[NodeSeq]) { echoComplete }
} ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
}
Put("/", HttpEntity(ContentType(`text/xml`, `UTF-16`), "<int>26</int>")) ~> {
entity(as[Int]) { echoComplete }
} ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
}
}
"cancel UnsupportedRequestContentTypeRejections if a subsequent `entity` directive succeeds" in {
Put("/", HttpEntity(`text/plain`, "yeah")) ~> {
entity(as[NodeSeq]) { _ completeOk } ~
entity(as[String]) { _ validate(false, "Problem") { completeOk } }
} ~> check { rejection shouldEqual ValidationRejection("Problem") }
}
"return a ValidationRejection if the request entity is semantically invalid (IllegalArgumentException)" in {
Put("/", HttpEntity(ContentType(`text/xml`, iso88592), "<int>-3</int>")) ~> {
entity(as[Int]) { _ completeOk }
} ~> check {
inside(rejection) {
case ValidationRejection("requirement failed", Some(_: IllegalArgumentException))
}
}
}
"return a MalformedRequestContentRejection if unmarshalling failed due to a not further classified error" in {
Put("/", HttpEntity(`text/xml`, "<foo attr='illegal xml'")) ~> {
entity(as[NodeSeq]) { _ completeOk }
} ~> check {
rejection shouldEqual MalformedRequestContentRejection(
"XML document structures must start and end within the same entity.", None)
}
}
"extract an Option[T] from the requests entity using the in-scope Unmarshaller" in {
Put("/", <p>cool</p>) ~> {
entity(as[Option[NodeSeq]]) { echoComplete }
} ~> check { responseAs[String] shouldEqual "Some(<p>cool</p>)" }
}
"extract an Option[T] as None if the request has no entity" in {
Put() ~> {
entity(as[Option[Int]]) { echoComplete }
} ~> check { responseAs[String] shouldEqual "None" }
}
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in {
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
entity(as[Option[NodeSeq]]) { echoComplete }
} ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
}
}
"properly extract with a super-unmarshaller" in {
case class Person(name: String)
val jsonUnmarshaller: FromEntityUnmarshaller[Person] = jsonFormat1(Person)
val xmlUnmarshaller: FromEntityUnmarshaller[Person] =
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`).map(seq Person(seq.text))
implicit val unmarshaller = Unmarshaller.firstOf(jsonUnmarshaller, xmlUnmarshaller)
val route = entity(as[Person]) { echoComplete }
Put("/", HttpEntity(`text/xml`, "<name>Peter Xml</name>")) ~> route ~> check {
responseAs[String] shouldEqual "Person(Peter Xml)"
}
Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check {
responseAs[String] shouldEqual "Person(Paul Json)"
}
Put("/", HttpEntity(`text/plain`, """name = Sir Text }""")) ~> route ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`application/json`, `text/xml`))
}
}
}
"The 'completeWith' directive" should {
"provide a completion function converting custom objects to an HttpEntity using the in-scope marshaller" in {
Get() ~> completeWith(instanceOf[Int]) { prod prod(42) } ~> check {
responseEntity shouldEqual HttpEntity(ContentType(`application/xhtml+xml`, `UTF-8`), "<int>42</int>")
}
}
"return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in {
Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod prod(42) } ~> check {
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
}
}
"convert the response content to an accepted charset" in {
Get() ~> `Accept-Charset`(`UTF-8`) ~> completeWith(instanceOf[String]) { prod prod("Hällö") } ~> check {
responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hällö")
}
}
}
"The 'handleWith' directive" should {
def times2(x: Int) = x * 2
"support proper round-trip content unmarshalling/marshalling to and from a function" in (
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "<int>84</int>") })
"result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in (
Put("/", HttpEntity(`text/xml`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
})
"result in an UnacceptedResponseContentTypeRejection rejection if there is no marshaller supporting the requests Accept-Charset header" in (
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> addHeaders(Accept(`text/xml`), `Accept-Charset`(`UTF-16`)) ~>
handleWith(times2) ~> check {
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
})
}
"The marshalling infrastructure for JSON" should {
import spray.json._
case class Foo(name: String)
implicit val fooFormat = jsonFormat1(Foo)
val foo = Foo("Hällö")
"render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in {
Get() ~> complete(foo) ~> check {
responseEntity shouldEqual HttpEntity(ContentType(`application/json`, `UTF-8`), foo.toJson.prettyPrint)
}
}
"reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in {
Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check {
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`)))
}
}
}
}

View file

@ -1,89 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.directives
import akka.http.model.{ StatusCodes, HttpMethods }
import akka.http.server._
class MethodDirectivesSpec extends RoutingSpec {
"get | put" should {
lazy val getOrPut = (get | put) { completeOk }
"block POST requests" in {
Post() ~> getOrPut ~> check { handled shouldEqual false }
}
"let GET requests pass" in {
Get() ~> getOrPut ~> check { response shouldEqual Ok }
}
"let PUT requests pass" in {
Put() ~> getOrPut ~> check { response shouldEqual Ok }
}
}
"two failed `get` directives" should {
"only result in a single Rejection" in {
Put() ~> {
get { completeOk } ~
get { completeOk }
} ~> check {
rejections shouldEqual List(MethodRejection(HttpMethods.GET))
}
}
}
"overrideMethodWithParameter" should {
"change the request method" in {
Get("/?_method=put") ~> overrideMethodWithParameter("_method") {
get { complete("GET") } ~
put { complete("PUT") }
} ~> check { responseAs[String] shouldEqual "PUT" }
}
"not affect the request when not specified" in {
Get() ~> overrideMethodWithParameter("_method") {
get { complete("GET") } ~
put { complete("PUT") }
} ~> check { responseAs[String] shouldEqual "GET" }
}
"complete with 501 Not Implemented when not a valid method" in {
Get("/?_method=hallo") ~> overrideMethodWithParameter("_method") {
get { complete("GET") } ~
put { complete("PUT") }
} ~> check { status shouldEqual StatusCodes.NotImplemented }
}
}
"MethodRejections under a successful match" should {
"be cancelled if the match happens after the rejection" in {
Put() ~> {
get { completeOk } ~
put { reject(RequestEntityExpectedRejection) }
} ~> check {
rejections shouldEqual List(RequestEntityExpectedRejection)
}
}
"be cancelled if the match happens after the rejection (example 2)" in {
Put() ~> {
(get & complete(Ok)) ~ (put & reject(RequestEntityExpectedRejection))
} ~> check {
rejections shouldEqual List(RequestEntityExpectedRejection)
}
}
"be cancelled if the match happens before the rejection" in {
Put() ~> {
put { reject(RequestEntityExpectedRejection) } ~ get { completeOk }
} ~> check {
rejections shouldEqual List(RequestEntityExpectedRejection)
}
}
"be cancelled if the match happens before the rejection (example 2)" in {
Put() ~> {
(put & reject(RequestEntityExpectedRejection)) ~ (get & complete(Ok))
} ~> check {
rejections shouldEqual List(RequestEntityExpectedRejection)
}
}
}
}

View file

@ -1,33 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model._
import headers._
import HttpMethods._
import MediaTypes._
import Uri._
class MiscDirectivesSpec extends RoutingSpec {
"the extractClientIP directive" should {
"extract from a X-Forwarded-For header" in {
Get() ~> addHeaders(`X-Forwarded-For`("2.3.4.5"), RawHeader("x-real-ip", "1.2.3.4")) ~> {
extractClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "2.3.4.5" }
}
"extract from a Remote-Address header" in {
Get() ~> addHeaders(RawHeader("x-real-ip", "1.2.3.4"), `Remote-Address`(RemoteAddress("5.6.7.8"))) ~> {
extractClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "5.6.7.8" }
}
"extract from a X-Real-IP header" in {
Get() ~> addHeader(RawHeader("x-real-ip", "1.2.3.4")) ~> {
extractClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "1.2.3.4" }
}
}
}

View file

@ -1,185 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import org.scalatest.{ FreeSpec, Inside }
import akka.http.unmarshalling.Unmarshaller.HexInt
class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Inside {
"when used with 'as[Int]' the parameter directive should" - {
"extract a parameter value as Int" in {
Get("/?amount=123") ~> {
parameter('amount.as[Int]) { echoComplete }
} ~> check { responseAs[String] shouldEqual "123" }
}
"cause a MalformedQueryParamRejection on illegal Int values" in {
Get("/?amount=1x3") ~> {
parameter('amount.as[Int]) { echoComplete }
} ~> check {
inside(rejection) {
case MalformedQueryParamRejection("amount", "'1x3' is not a valid 32-bit signed integer value", Some(_))
}
}
}
"supply typed default values" in {
Get() ~> {
parameter('amount ? 45) { echoComplete }
} ~> check { responseAs[String] shouldEqual "45" }
}
"create typed optional parameters that" - {
"extract Some(value) when present" in {
Get("/?amount=12") ~> {
parameter("amount".as[Int]?) { echoComplete }
} ~> check { responseAs[String] shouldEqual "Some(12)" }
}
"extract None when not present" in {
Get() ~> {
parameter("amount".as[Int]?) { echoComplete }
} ~> check { responseAs[String] shouldEqual "None" }
}
"cause a MalformedQueryParamRejection on illegal Int values" in {
Get("/?amount=x") ~> {
parameter("amount".as[Int]?) { echoComplete }
} ~> check {
inside(rejection) {
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit signed integer value", Some(_))
}
}
}
}
}
"when used with 'as(HexInt)' the parameter directive should" - {
"extract parameter values as Int" in {
Get("/?amount=1f") ~> {
parameter('amount.as(HexInt)) { echoComplete }
} ~> check { responseAs[String] shouldEqual "31" }
}
"cause a MalformedQueryParamRejection on illegal Int values" in {
Get("/?amount=1x3") ~> {
parameter('amount.as(HexInt)) { echoComplete }
} ~> check {
inside(rejection) {
case MalformedQueryParamRejection("amount", "'1x3' is not a valid 32-bit hexadecimal integer value", Some(_))
}
}
}
"supply typed default values" in {
Get() ~> {
parameter('amount.as(HexInt) ? 45) { echoComplete }
} ~> check { responseAs[String] shouldEqual "45" }
}
"create typed optional parameters that" - {
"extract Some(value) when present" in {
Get("/?amount=A") ~> {
parameter("amount".as(HexInt)?) { echoComplete }
} ~> check { responseAs[String] shouldEqual "Some(10)" }
}
"extract None when not present" in {
Get() ~> {
parameter("amount".as(HexInt)?) { echoComplete }
} ~> check { responseAs[String] shouldEqual "None" }
}
"cause a MalformedQueryParamRejection on illegal Int values" in {
Get("/?amount=x") ~> {
parameter("amount".as(HexInt)?) { echoComplete }
} ~> check {
inside(rejection) {
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit hexadecimal integer value", Some(_))
}
}
}
}
}
"when used with 'as[Boolean]' the parameter directive should" - {
"extract parameter values as Boolean" in {
Get("/?really=true") ~> {
parameter('really.as[Boolean]) { echoComplete }
} ~> check { responseAs[String] shouldEqual "true" }
Get("/?really=no") ~> {
parameter('really.as[Boolean]) { echoComplete }
} ~> check { responseAs[String] shouldEqual "false" }
}
"extract optional parameter values as Boolean" in {
Get() ~> {
parameter('really.as[Boolean] ? false) { echoComplete }
} ~> check { responseAs[String] shouldEqual "false" }
}
"cause a MalformedQueryParamRejection on illegal Boolean values" in {
Get("/?really=absolutely") ~> {
parameter('really.as[Boolean]) { echoComplete }
} ~> check {
inside(rejection) {
case MalformedQueryParamRejection("really", "'absolutely' is not a valid Boolean value", None)
}
}
}
}
"The 'parameters' extraction directive should" - {
"extract the value of given parameters" in {
Get("/?name=Parsons&FirstName=Ellen") ~> {
parameters("name", 'FirstName) { (name, firstName)
complete(firstName + name)
}
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
}
"correctly extract an optional parameter" in {
Get("/?foo=bar") ~> parameters('foo ?) { echoComplete } ~> check { responseAs[String] shouldEqual "Some(bar)" }
Get("/?foo=bar") ~> parameters('baz ?) { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
}
"ignore additional parameters" in {
Get("/?name=Parsons&FirstName=Ellen&age=29") ~> {
parameters("name", 'FirstName) { (name, firstName)
complete(firstName + name)
}
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
}
"reject the request with a MissingQueryParamRejection if a required parameter is missing" in {
Get("/?name=Parsons&sex=female") ~> {
parameters('name, 'FirstName, 'age) { (name, firstName, age)
completeOk
}
} ~> check { rejection shouldEqual MissingQueryParamRejection("FirstName") }
}
"supply the default value if an optional parameter is missing" in {
Get("/?name=Parsons&FirstName=Ellen") ~> {
parameters("name"?, 'FirstName, 'age ? "29", 'eyes?) { (name, firstName, age, eyes)
complete(firstName + name + age + eyes)
}
} ~> check { responseAs[String] shouldEqual "EllenSome(Parsons)29None" }
}
}
"The 'parameter' requirement directive should" - {
"block requests that do not contain the required parameter" in {
Get("/person?age=19") ~> {
parameter('nose ! "large") { completeOk }
} ~> check { handled shouldEqual false }
}
"block requests that contain the required parameter but with an unmatching value" in {
Get("/person?age=19&nose=small") ~> {
parameter('nose ! "large") { completeOk }
} ~> check { handled shouldEqual false }
}
"let requests pass that contain the required parameter with its required value" in {
Get("/person?nose=large&eyes=blue") ~> {
parameter('nose ! "large") { completeOk }
} ~> check { response shouldEqual Ok }
}
"be useable for method tunneling" in {
val route = {
(post | parameter('method ! "post")) { complete("POST") } ~
get { complete("GET") }
}
Get("/?method=post") ~> route ~> check { responseAs[String] shouldEqual "POST" }
Post() ~> route ~> check { responseAs[String] shouldEqual "POST" }
Get() ~> route ~> check { responseAs[String] shouldEqual "GET" }
}
}
}

View file

@ -1,339 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.directives
import akka.http.server._
import org.scalatest.Inside
class PathDirectivesSpec extends RoutingSpec with Inside {
val echoUnmatchedPath = extractUnmatchedPath { echoComplete }
def echoCaptureAndUnmatchedPath[T]: T Route =
capture ctx ctx.complete(capture.toString + ":" + ctx.unmatchedPath)
"""path("foo")""" should {
val test = testFor(path("foo") { echoUnmatchedPath })
"reject [/bar]" in test()
"reject [/foobar]" in test()
"reject [/foo/bar]" in test()
"accept [/foo] and clear the unmatchedPath" in test("")
"reject [/foo/]" in test()
}
"""path("foo" /)""" should {
val test = testFor(path("foo" /) { echoUnmatchedPath })
"reject [/foo]" in test()
"accept [/foo/] and clear the unmatchedPath" in test("")
}
"""path("")""" should {
val test = testFor(path("") { echoUnmatchedPath })
"reject [/foo]" in test()
"accept [/] and clear the unmatchedPath" in test("")
}
"""pathPrefix("foo")""" should {
val test = testFor(pathPrefix("foo") { echoUnmatchedPath })
"reject [/bar]" in test()
"accept [/foobar]" in test("bar")
"accept [/foo/bar]" in test("/bar")
"accept [/foo] and clear the unmatchedPath" in test("")
"accept [/foo/] and clear the unmatchedPath" in test("/")
}
"""pathPrefix("foo" / "bar")""" should {
val test = testFor(pathPrefix("foo" / "bar") { echoUnmatchedPath })
"reject [/bar]" in test()
"accept [/foo/bar]" in test("")
"accept [/foo/bar/baz]" in test("/baz")
}
"""pathPrefix("ab[cd]+".r)""" should {
val test = testFor(pathPrefix("ab[cd]+".r) { echoCaptureAndUnmatchedPath })
"reject [/bar]" in test()
"reject [/ab/cd]" in test()
"reject [/abcdef]" in test("abcd:ef")
"reject [/abcdd/ef]" in test("abcdd:/ef")
}
"""pathPrefix("ab(cd)".r)""" should {
val test = testFor(pathPrefix("ab(cd)+".r) { echoCaptureAndUnmatchedPath })
"reject [/bar]" in test()
"reject [/ab/cd]" in test()
"reject [/abcdef]" in test("cd:ef")
"reject [/abcde/fg]" in test("cd:e/fg")
}
"pathPrefix(regex)" should {
"fail when the regex contains more than one group" in {
an[IllegalArgumentException] must be thrownBy path("a(b+)(c+)".r) { echoCaptureAndUnmatchedPath }
}
}
"pathPrefix(IntNumber)" should {
val test = testFor(pathPrefix(IntNumber) { echoCaptureAndUnmatchedPath })
"accept [/23]" in test("23:")
"accept [/12345yes]" in test("12345:yes")
"reject [/]" in test()
"reject [/abc]" in test()
"reject [/2147483648]" in test() // > Int.MaxValue
}
"pathPrefix(CustomShortNumber)" should {
object CustomShortNumber extends NumberMatcher[Short](Short.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
val test = testFor(pathPrefix(CustomShortNumber) { echoCaptureAndUnmatchedPath })
"accept [/23]" in test("23:")
"accept [/12345yes]" in test("12345:yes")
"reject [/]" in test()
"reject [/abc]" in test()
"reject [/33000]" in test() // > Short.MaxValue
}
"pathPrefix(JavaUUID)" should {
val test = testFor(pathPrefix(JavaUUID) { echoCaptureAndUnmatchedPath })
"accept [/bdea8652-f26c-40ca-8157-0b96a2a8389d]" in test("bdea8652-f26c-40ca-8157-0b96a2a8389d:")
"accept [/bdea8652-f26c-40ca-8157-0b96a2a8389dyes]" in test("bdea8652-f26c-40ca-8157-0b96a2a8389d:yes")
"reject [/]" in test()
"reject [/abc]" in test()
}
"pathPrefix(Map(\"red\" -> 1, \"green\" -> 2, \"blue\" -> 3))" should {
val test = testFor(pathPrefix(Map("red" -> 1, "green" -> 2, "blue" -> 3)) { echoCaptureAndUnmatchedPath })
"accept [/green]" in test("2:")
"accept [/redsea]" in test("1:sea")
"reject [/black]" in test()
}
"pathPrefix(Map.empty)" should {
val test = testFor(pathPrefix(Map[String, Int]()) { echoCaptureAndUnmatchedPath })
"reject [/black]" in test()
}
"pathPrefix(Segment)" should {
val test = testFor(pathPrefix(Segment) { echoCaptureAndUnmatchedPath })
"accept [/abc]" in test("abc:")
"accept [/abc/]" in test("abc:/")
"accept [/abc/def]" in test("abc:/def")
"reject [/]" in test()
}
"pathPrefix(Segments)" should {
val test = testFor(pathPrefix(Segments) { echoCaptureAndUnmatchedPath })
"accept [/]" in test("List():")
"accept [/a/b/c]" in test("List(a, b, c):")
"accept [/a/b/c/]" in test("List(a, b, c):/")
}
"""pathPrefix(separateOnSlashes("a/b"))""" should {
val test = testFor(pathPrefix(separateOnSlashes("a/b")) { echoUnmatchedPath })
"accept [/a/b]" in test("")
"accept [/a/b/]" in test("/")
"accept [/a/c]" in test()
}
"""pathPrefix(separateOnSlashes("abc"))""" should {
val test = testFor(pathPrefix(separateOnSlashes("abc")) { echoUnmatchedPath })
"accept [/abc]" in test("")
"accept [/abcdef]" in test("def")
"accept [/ab]" in test()
}
"""pathPrefixTest("a" / Segment ~ Slash)""" should {
val test = testFor(pathPrefixTest("a" / Segment ~ Slash) { echoCaptureAndUnmatchedPath })
"accept [/a/bc/]" in test("bc:/a/bc/")
"accept [/a/bc]" in test()
"accept [/a/]" in test()
}
"""pathSuffix("edit" / Segment)""" should {
val test = testFor(pathSuffix("edit" / Segment) { echoCaptureAndUnmatchedPath })
"accept [/orders/123/edit]" in test("123:/orders/")
"accept [/orders/123/ed]" in test()
"accept [/edit]" in test()
}
"""pathSuffix("foo" / "bar" ~ "baz")""" should {
val test = testFor(pathSuffix("foo" / "bar" ~ "baz") { echoUnmatchedPath })
"accept [/orders/barbaz/foo]" in test("/orders/")
"accept [/orders/bazbar/foo]" in test()
}
"pathSuffixTest(Slash)" should {
val test = testFor(pathSuffixTest(Slash) { echoUnmatchedPath })
"accept [/]" in test("/")
"accept [/foo/]" in test("/foo/")
"accept [/foo]" in test()
}
"""pathPrefix("foo" | "bar")""" should {
val test = testFor(pathPrefix("foo" | "bar") { echoUnmatchedPath })
"accept [/foo]" in test("")
"accept [/foops]" in test("ps")
"accept [/bar]" in test("")
"reject [/baz]" in test()
}
"""pathSuffix(!"foo")""" should {
val test = testFor(pathSuffix(!"foo") { echoUnmatchedPath })
"accept [/bar]" in test("/bar")
"reject [/foo]" in test()
}
"pathPrefix(IntNumber?)" should {
val test = testFor(pathPrefix(IntNumber?) { echoCaptureAndUnmatchedPath })
"accept [/12]" in test("Some(12):")
"accept [/12a]" in test("Some(12):a")
"accept [/foo]" in test("None:foo")
}
"""pathPrefix("foo"?)""" should {
val test = testFor(pathPrefix("foo"?) { echoUnmatchedPath })
"accept [/foo]" in test("")
"accept [/fool]" in test("l")
"accept [/bar]" in test("bar")
}
"""pathPrefix("foo") & pathEnd""" should {
val test = testFor((pathPrefix("foo") & pathEnd) { echoUnmatchedPath })
"reject [/foobar]" in test()
"reject [/foo/bar]" in test()
"accept [/foo] and clear the unmatchedPath" in test("")
"reject [/foo/]" in test()
}
"""pathPrefix("foo") & pathEndOrSingleSlash""" should {
val test = testFor((pathPrefix("foo") & pathEndOrSingleSlash) { echoUnmatchedPath })
"reject [/foobar]" in test()
"reject [/foo/bar]" in test()
"accept [/foo] and clear the unmatchedPath" in test("")
"accept [/foo/] and clear the unmatchedPath" in test("")
}
"""pathPrefix(IntNumber.repeat(separator = "."))""" should {
{
val test = testFor(pathPrefix(IntNumber.repeat(min = 2, max = 5, separator = ".")) { echoCaptureAndUnmatchedPath })
"reject [/foo]" in test()
"reject [/1foo]" in test()
"reject [/1.foo]" in test()
"accept [/1.2foo]" in test("List(1, 2):foo")
"accept [/1.2.foo]" in test("List(1, 2):.foo")
"accept [/1.2.3foo]" in test("List(1, 2, 3):foo")
"accept [/1.2.3.foo]" in test("List(1, 2, 3):.foo")
"accept [/1.2.3.4foo]" in test("List(1, 2, 3, 4):foo")
"accept [/1.2.3.4.foo]" in test("List(1, 2, 3, 4):.foo")
"accept [/1.2.3.4.5foo]" in test("List(1, 2, 3, 4, 5):foo")
"accept [/1.2.3.4.5.foo]" in test("List(1, 2, 3, 4, 5):.foo")
"accept [/1.2.3.4.5.6foo]" in test("List(1, 2, 3, 4, 5):.6foo")
"accept [/1.2.3.]" in test("List(1, 2, 3):.")
"accept [/1.2.3/]" in test("List(1, 2, 3):/")
"accept [/1.2.3./]" in test("List(1, 2, 3):./")
}
{
val test = testFor(pathPrefix(IntNumber.repeat(2, ".")) { echoCaptureAndUnmatchedPath })
"reject [/bar]" in test()
"reject [/1bar]" in test()
"reject [/1.bar]" in test()
"accept [/1.2bar]" in test("List(1, 2):bar")
"accept [/1.2.bar]" in test("List(1, 2):.bar")
"accept [/1.2.3bar]" in test("List(1, 2):.3bar")
}
}
"PathMatchers" should {
{
val test = testFor(path(Rest.tmap { case Tuple1(s) Tuple1(s.split('-').toList) }) { echoComplete })
"support the hmap modifier in accept [/yes-no]" in test("List(yes, no)")
}
{
val test = testFor(path(Rest.map(_.split('-').toList)) { echoComplete })
"support the map modifier in accept [/yes-no]" in test("List(yes, no)")
}
{
val test = testFor(path(Rest.tflatMap { case Tuple1(s) Some(s).filter("yes" ==).map(x Tuple1(x)) }) { echoComplete })
"support the hflatMap modifier in accept [/yes]" in test("yes")
"support the hflatMap modifier in reject [/blub]" in test()
}
{
val test = testFor(path(Rest.flatMap(s Some(s).filter("yes" ==))) { echoComplete })
"support the flatMap modifier in accept [/yes]" in test("yes")
"support the flatMap modifier reject [/blub]" in test()
}
}
implicit class WithIn(str: String) {
def in(f: String Unit) = convertToWordSpecStringWrapper(str) in f(str)
def in(body: Unit) = convertToWordSpecStringWrapper(str) in body
}
case class testFor(route: Route) {
def apply(expectedResponse: String = null): String Unit = exampleString
"\\[([^\\]]+)\\]".r.findFirstMatchIn(exampleString) match {
case Some(uri) Get(uri.group(1)) ~> route ~> check {
if (expectedResponse eq null) handled shouldEqual false
else responseAs[String] shouldEqual expectedResponse
}
case None failTest("Example '" + exampleString + "' doesn't contain a test uri")
}
}
import akka.http.model.StatusCodes._
"the `redirectToTrailingSlashIfMissing` directive" should {
val route = redirectToTrailingSlashIfMissing(Found) { completeOk }
"pass if the request path already has a trailing slash" in {
Get("/foo/bar/") ~> route ~> check { response shouldEqual Ok }
}
"redirect if the request path doesn't have a trailing slash" in {
Get("/foo/bar") ~> route ~> checkRedirectTo("/foo/bar/")
}
"preserves the query and the frag when redirect" in {
Get("/foo/bar?query#frag") ~> route ~> checkRedirectTo("/foo/bar/?query#frag")
}
"redirect with the given redirection status code" in {
Get("/foo/bar") ~>
redirectToTrailingSlashIfMissing(MovedPermanently) { completeOk } ~>
check { status shouldEqual MovedPermanently }
}
}
"the `redirectToNoTrailingSlashIfPresent` directive" should {
val route = redirectToNoTrailingSlashIfPresent(Found) { completeOk }
"pass if the request path already doesn't have a trailing slash" in {
Get("/foo/bar") ~> route ~> check { response shouldEqual Ok }
}
"redirect if the request path has a trailing slash" in {
Get("/foo/bar/") ~> route ~> checkRedirectTo("/foo/bar")
}
"preserves the query and the frag when redirect" in {
Get("/foo/bar/?query#frag") ~> route ~> checkRedirectTo("/foo/bar?query#frag")
}
"redirect with the given redirection status code" in {
Get("/foo/bar/") ~>
redirectToNoTrailingSlashIfPresent(MovedPermanently) { completeOk } ~>
check { status shouldEqual MovedPermanently }
}
}
import akka.http.model.headers.Location
import akka.http.model.Uri
private def checkRedirectTo(expectedUri: Uri) =
check {
status shouldBe a[Redirection]
inside(header[Location]) {
case Some(Location(uri))
(if (expectedUri.isAbsolute) uri else uri.toRelative) shouldEqual expectedUri
}
}
}

View file

@ -1,139 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import scala.concurrent.Await
import scala.concurrent.duration._
import akka.http.model.StatusCodes._
import akka.http.model._
import akka.http.model.headers._
import akka.http.util._
import akka.stream.scaladsl.{ Sink, Source }
import akka.util.ByteString
import org.scalatest.{ Inside, Inspectors }
class RangeDirectivesSpec extends RoutingSpec with Inspectors with Inside {
lazy val wrs =
mapSettings(_.copy(rangeCountLimit = 10, rangeCoalescingThreshold = 1L)) &
withRangeSupport
def bytes(length: Byte) = Array.tabulate[Byte](length)(_.toByte)
"The `withRangeSupport` directive" should {
def completeWithRangedBytes(length: Byte) = wrs(complete(bytes(length)))
"return an Accept-Ranges(bytes) header for GET requests" in {
Get() ~> { wrs { complete("any") } } ~> check {
headers should contain(`Accept-Ranges`(RangeUnits.Bytes))
}
}
"not return an Accept-Ranges(bytes) header for non-GET requests" in {
Put() ~> { wrs { complete("any") } } ~> check {
headers should not contain `Accept-Ranges`(RangeUnits.Bytes)
}
}
"return a Content-Range header for a ranged request with a single range" in {
Get() ~> addHeader(Range(ByteRange(0, 1))) ~> completeWithRangedBytes(10) ~> check {
headers should contain(`Content-Range`(ContentRange(0, 1, 10)))
status shouldEqual PartialContent
responseAs[Array[Byte]] shouldEqual bytes(2)
}
}
"return a partial response for a ranged request with a single range with undefined lastBytePosition" in {
Get() ~> addHeader(Range(ByteRange.fromOffset(5))) ~> completeWithRangedBytes(10) ~> check {
responseAs[Array[Byte]] shouldEqual Array[Byte](5, 6, 7, 8, 9)
}
}
"return a partial response for a ranged request with a single suffix range" in {
Get() ~> addHeader(Range(ByteRange.suffix(1))) ~> completeWithRangedBytes(10) ~> check {
responseAs[Array[Byte]] shouldEqual Array[Byte](9)
}
}
"return a partial response for a ranged request with a overlapping suffix range" in {
Get() ~> addHeader(Range(ByteRange.suffix(100))) ~> completeWithRangedBytes(10) ~> check {
responseAs[Array[Byte]] shouldEqual bytes(10)
}
}
"be transparent to non-GET requests" in {
Post() ~> addHeader(Range(ByteRange(1, 2))) ~> completeWithRangedBytes(5) ~> check {
responseAs[Array[Byte]] shouldEqual bytes(5)
}
}
"be transparent to non-200 responses" in {
Get() ~> addHeader(Range(ByteRange(1, 2))) ~> Route.seal(wrs(reject())) ~> check {
status == NotFound
headers.exists { case `Content-Range`(_, _) true; case _ false } shouldEqual false
}
}
"reject an unsatisfiable single range" in {
Get() ~> addHeader(Range(ByteRange(100, 200))) ~> completeWithRangedBytes(10) ~> check {
rejection shouldEqual UnsatisfiableRangeRejection(Seq(ByteRange(100, 200)), 10)
}
}
"reject an unsatisfiable single suffix range with length 0" in {
Get() ~> addHeader(Range(ByteRange.suffix(0))) ~> completeWithRangedBytes(42) ~> check {
rejection shouldEqual UnsatisfiableRangeRejection(Seq(ByteRange.suffix(0)), 42)
}
}
"return a mediaType of 'multipart/byteranges' for a ranged request with multiple ranges" in {
Get() ~> addHeader(Range(ByteRange(0, 10), ByteRange(0, 10))) ~> completeWithRangedBytes(10) ~> check {
mediaType.withParams(Map.empty) shouldEqual MediaTypes.`multipart/byteranges`
}
}
"return a 'multipart/byteranges' for a ranged request with multiple coalesced ranges and expect ranges in ascending order" in {
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
wrs { complete("Some random and not super short entity.") }
} ~> check {
header[`Content-Range`] should be(None)
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
parts.size shouldEqual 2
inside(parts(0)) {
case Multipart.ByteRanges.BodyPart(range, entity, unit, headers)
range shouldEqual ContentRange.Default(0, 2, Some(39))
unit shouldEqual RangeUnits.Bytes
Await.result(entity.dataBytes.utf8String, 100.millis) shouldEqual "Som"
}
inside(parts(1)) {
case Multipart.ByteRanges.BodyPart(range, entity, unit, headers)
range shouldEqual ContentRange.Default(5, 10, Some(39))
unit shouldEqual RangeUnits.Bytes
Await.result(entity.dataBytes.utf8String, 100.millis) shouldEqual "random"
}
}
}
"return a 'multipart/byteranges' for a ranged request with multiple ranges if entity data source isn't reusable" in {
val content = "Some random and not super short entity."
def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content)))
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
wrs { complete(HttpEntity.Default(MediaTypes.`text/plain`, content.length, entityData())) }
} ~> check {
header[`Content-Range`] should be(None)
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
parts.size shouldEqual 2
}
}
"reject a request with too many requested ranges" in {
val ranges = (1 to 20).map(a ByteRange.fromOffset(a))
Get() ~> addHeader(Range(ranges)) ~> completeWithRangedBytes(100) ~> check {
rejection shouldEqual TooManyRangesRejection(10)
}
}
}
}

View file

@ -1,78 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.directives
import akka.http.model._
import MediaTypes._
import headers._
import StatusCodes._
import akka.http.server._
class RespondWithDirectivesSpec extends RoutingSpec {
"overrideStatusCode" should {
"set the given status on successful responses" in {
Get() ~> {
overrideStatusCode(Created) { completeOk }
} ~> check { response shouldEqual HttpResponse(Created) }
}
"leave rejections unaffected" in {
Get() ~> {
overrideStatusCode(Created) { reject }
} ~> check { rejections shouldEqual Nil }
}
}
val customHeader = RawHeader("custom", "custom")
val customHeader2 = RawHeader("custom2", "custom2")
val existingHeader = RawHeader("custom", "existing")
"respondWithHeader" should {
val customHeader = RawHeader("custom", "custom")
"add the given header to successful responses" in {
Get() ~> {
respondWithHeader(customHeader) { completeOk }
} ~> check { response shouldEqual HttpResponse(headers = customHeader :: Nil) }
}
}
"respondWithHeaders" should {
"add the given headers to successful responses" in {
Get() ~> {
respondWithHeaders(customHeader, customHeader2) { completeOk }
} ~> check { response shouldEqual HttpResponse(headers = customHeader :: customHeader2 :: Nil) }
}
}
"respondWithDefaultHeader" should {
def route(extraHeaders: HttpHeader*) = respondWithDefaultHeader(customHeader) {
respondWithHeaders(extraHeaders: _*) {
completeOk
}
}
"add the given header to a response if the header was missing before" in {
Get() ~> route() ~> check { response shouldEqual HttpResponse(headers = customHeader :: Nil) }
}
"not change a response if the header already existed" in {
Get() ~> route(existingHeader) ~> check { response shouldEqual HttpResponse(headers = existingHeader :: Nil) }
}
}
"respondWithDefaultHeaders" should {
def route(extraHeaders: HttpHeader*) = respondWithDefaultHeaders(customHeader, customHeader2) {
respondWithHeaders(extraHeaders: _*) {
completeOk
}
}
"add the given headers to a response if the header was missing before" in {
Get() ~> route() ~> check { response shouldEqual HttpResponse(headers = customHeader :: customHeader2 :: Nil) }
}
"not update an existing header" in {
Get() ~> route(existingHeader) ~> check {
response shouldEqual HttpResponse(headers = List(customHeader2, existingHeader))
}
}
}
}

View file

@ -1,148 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.directives
import org.scalatest.FreeSpec
import scala.concurrent.Promise
import akka.http.marshallers.xml.ScalaXmlSupport._
import akka.http.marshalling._
import akka.http.server._
import akka.http.model._
import akka.http.util._
import headers._
import StatusCodes._
import MediaTypes._
class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
"The `complete` directive should" - {
"by chainable with the `&` operator" in {
Get() ~> (get & complete("yeah")) ~> check { responseAs[String] shouldEqual "yeah" }
}
"be lazy in its argument evaluation, independently of application style" in {
var i = 0
Put() ~> {
get { complete { i += 1; "get" } } ~
put { complete { i += 1; "put" } } ~
(post & complete { i += 1; "post" })
} ~> check {
responseAs[String] shouldEqual "put"
i shouldEqual 1
}
}
"support completion from response futures" - {
"simple case without marshaller" in {
Get() ~> {
get & complete(Promise.successful(HttpResponse(entity = "yup")).future)
} ~> check { responseAs[String] shouldEqual "yup" }
}
"for successful futures and marshalling" in {
Get() ~> complete(Promise.successful("yes").future) ~> check { responseAs[String] shouldEqual "yes" }
}
"for failed futures and marshalling" in {
object TestException extends RuntimeException
Get() ~> complete(Promise.failed[String](TestException).future) ~>
check {
status shouldEqual StatusCodes.InternalServerError
responseAs[String] shouldEqual "There was an internal server error."
}
}
"for futures failed with a RejectionError" in {
Get() ~> complete(Promise.failed[String](RejectionError(AuthorizationFailedRejection)).future) ~>
check {
rejection shouldEqual AuthorizationFailedRejection
}
}
}
"allow easy handling of futured ToResponseMarshallers" in pending /*{
trait RegistrationStatus
case class Registered(name: String) extends RegistrationStatus
case object AlreadyRegistered extends RegistrationStatus
val route =
get {
path("register" / Segment) { name
def registerUser(name: String): Future[RegistrationStatus] = Future.successful {
name match {
case "otto" AlreadyRegistered
case _ Registered(name)
}
}
complete {
registerUser(name).map[ToResponseMarshallable] {
case Registered(_) HttpEntity.Empty
case AlreadyRegistered
import spray.json.DefaultJsonProtocol._
import spray.httpx.SprayJsonSupport._
(StatusCodes.BadRequest, Map("error" -> "User already Registered"))
}
}
}
}
Get("/register/otto") ~> route ~> check {
status shouldEqual StatusCodes.BadRequest
}
Get("/register/karl") ~> route ~> check {
status shouldEqual StatusCodes.OK
entity shouldEqual HttpEntity.Empty
}
}*/
"do Content-Type negotiation for multi-marshallers" in pendingUntilFixed {
val route = get & complete(Data("Ida", 83))
import akka.http.model.headers.Accept
Get().withHeaders(Accept(MediaTypes.`application/json`)) ~> route ~> check {
responseAs[String] shouldEqual
"""{
| "name": "Ida",
| "age": 83
|}""".stripMarginWithNewline("\n")
}
Get().withHeaders(Accept(MediaTypes.`text/xml`)) ~> route ~> check {
responseAs[xml.NodeSeq] shouldEqual <data><name>Ida</name><age>83</age></data>
}
pending
/*Get().withHeaders(Accept(MediaTypes.`text/plain`)) ~> HttpService.sealRoute(route) ~> check {
status shouldEqual StatusCodes.NotAcceptable
}*/
}
}
"the redirect directive should" - {
"produce proper 'Found' redirections" in {
Get() ~> {
redirect("/foo", Found)
} ~> check {
response shouldEqual HttpResponse(
status = 302,
entity = HttpEntity(`text/html`, "The requested resource temporarily resides under <a href=\"/foo\">this URI</a>."),
headers = Location("/foo") :: Nil)
}
}
"produce proper 'NotModified' redirections" in {
Get() ~> {
redirect("/foo", NotModified)
} ~> check { response shouldEqual HttpResponse(304, headers = Location("/foo") :: Nil) }
}
}
case class Data(name: String, age: Int)
object Data {
//import spray.json.DefaultJsonProtocol._
//import spray.httpx.SprayJsonSupport._
val jsonMarshaller: ToEntityMarshaller[Data] = FIXME // jsonFormat2(Data.apply)
val xmlMarshaller: ToEntityMarshaller[Data] = FIXME
/*Marshaller.delegate[Data, xml.NodeSeq](MediaTypes.`text/xml`) { (data: Data) ⇒
<data><name>{ data.name }</name><age>{ data.age }</age></data>
}*/
implicit val dataMarshaller: ToResponseMarshaller[Data] = FIXME
//ToResponseMarshaller.oneOf(MediaTypes.`application/json`, MediaTypes.`text/xml`)(jsonMarshaller, xmlMarshaller)
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
package directives
import akka.http.model.StatusCodes._
class SchemeDirectivesSpec extends RoutingSpec {
"the extractScheme directive" should {
"extract the Uri scheme" in {
Put("http://localhost/", "Hello") ~> extractScheme { echoComplete } ~> check { responseAs[String] shouldEqual "http" }
}
}
"""the scheme("http") directive""" should {
"let requests with an http Uri scheme pass" in {
Put("http://localhost/", "Hello") ~> scheme("http") { completeOk } ~> check { response shouldEqual Ok }
}
"reject requests with an https Uri scheme" in {
Get("https://localhost/") ~> scheme("http") { completeOk } ~> check { rejections shouldEqual List(SchemeRejection("http")) }
}
"cancel SchemeRejection if other scheme passed" in {
val route =
scheme("https") { completeOk } ~
scheme("http") { reject }
Put("http://localhost/", "Hello") ~> route ~> check {
rejections should be(Nil)
}
}
}
"""the scheme("https") directive""" should {
"let requests with an https Uri scheme pass" in {
Put("https://localhost/", "Hello") ~> scheme("https") { completeOk } ~> check { response shouldEqual Ok }
}
"reject requests with an http Uri scheme" in {
Get("http://localhost/") ~> scheme("https") { completeOk } ~> check { rejections shouldEqual List(SchemeRejection("https")) }
}
}
}

View file

@ -1,49 +0,0 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.directives
import akka.http.engine.ws.InternalCustomHeader
import akka.http.model
import akka.http.model.headers.{ Connection, UpgradeProtocol, Upgrade }
import akka.http.model.{ HttpRequest, StatusCodes, HttpResponse }
import akka.http.model.ws.{ Message, UpgradeToWebsocket }
import akka.http.server.{ Route, RoutingSpec }
import akka.http.util.Rendering
import akka.stream.FlowMaterializer
import akka.stream.scaladsl.Flow
import scala.collection.immutable.Seq
class WebsocketDirectivesSpec extends RoutingSpec {
"the handleWebsocketMessages directive" should {
"handle websocket requests" in {
Get("http://localhost/") ~> Upgrade(List(UpgradeProtocol("websocket"))) ~>
emulateHttpCore ~> Route.seal(handleWebsocketMessages(Flow[Message])) ~>
check {
status shouldEqual StatusCodes.SwitchingProtocols
}
}
"reject non-websocket requests" in {
Get("http://localhost/") ~> emulateHttpCore ~> Route.seal(handleWebsocketMessages(Flow[Message])) ~> check {
status shouldEqual StatusCodes.BadRequest
responseAs[String] shouldEqual "Expected Websocket Upgrade request"
}
}
}
/** Only checks for upgrade header and then adds UpgradeToWebsocket mock header */
def emulateHttpCore(req: HttpRequest): HttpRequest =
req.header[Upgrade] match {
case Some(upgrade) if upgrade.hasWebsocket req.copy(headers = req.headers :+ upgradeToWebsocketHeaderMock)
case _ req
}
def upgradeToWebsocketHeaderMock: UpgradeToWebsocket =
new InternalCustomHeader("UpgradeToWebsocketMock") with UpgradeToWebsocket {
def requestedProtocols: Seq[String] = Nil
def handleMessages(handlerFlow: Flow[Message, Message, Any], subprotocol: Option[String])(implicit mat: FlowMaterializer): HttpResponse =
HttpResponse(StatusCodes.SwitchingProtocols)
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.util
import org.scalatest.{ Matchers, WordSpec }
class TupleOpsSpec extends WordSpec with Matchers {
import TupleOps._
"The TupleOps" should {
"support folding over tuples using a binary poly-function" in {
object Funky extends BinaryPolyFunc {
implicit def step1 = at[Double, Int](_ + _)
implicit def step2 = at[Double, Symbol]((d, s) (d + s.name.tail.toInt).toByte)
implicit def step3 = at[Byte, String]((byte, s) byte + s.toLong)
}
(1, 'X2, "3").foldLeft(0.0)(Funky) shouldEqual 6L
}
"support joining tuples" in {
(1, 'X2, "3") join () shouldEqual (1, 'X2, "3")
() join (1, 'X2, "3") shouldEqual (1, 'X2, "3")
(1, 'X2, "3") join (4.0, 5L) shouldEqual (1, 'X2, "3", 4.0, 5L)
}
}
}

View file

@ -1,290 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.unmarshalling
import akka.http.testkit.ScalatestUtils
import akka.util.ByteString
import scala.concurrent.duration._
import scala.concurrent.{ Future, Await }
import org.scalatest.matchers.Matcher
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
import akka.actor.ActorSystem
import akka.stream.ActorFlowMaterializer
import akka.stream.scaladsl._
import akka.http.model._
import akka.http.util._
import headers._
import MediaTypes._
import FastFuture._
class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils {
implicit val system = ActorSystem(getClass.getSimpleName)
implicit val materializer = ActorFlowMaterializer()
import system.dispatcher
"The PredefinedFromEntityUnmarshallers." - {
"stringUnmarshaller should unmarshal `text/plain` content in UTF-8 to Strings" in {
Unmarshal(HttpEntity("Hällö")).to[String] should evaluateTo("Hällö")
}
"charArrayUnmarshaller should unmarshal `text/plain` content in UTF-8 to char arrays" in {
Unmarshal(HttpEntity("árvíztűrő ütvefúrógép")).to[Array[Char]] should evaluateTo("árvíztűrő ütvefúrógép".toCharArray)
}
}
"The MultipartUnmarshallers." - {
"multipartGeneralUnmarshaller should correctly unmarshal 'multipart/*' content with" - {
"an empty part" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
"""--XYZABC
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
}
"two empty parts" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
"""--XYZABC
|--XYZABC
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)),
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
}
"a part without entity and missing header separation CRLF" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
"""--XYZABC
|Content-type: text/xml
|Age: 12
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12))))
}
"an implicitly typed part (without headers)" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
"""--XYZABC
|
|Perfectly fine part content.
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content.")))
}
"one non-empty form-data part" in {
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
"""---
|Content-type: text/plain; charset=UTF8
|content-disposition: form-data; name="email"
|
|test@there.com
|-----""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))
}
"two different parts" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
"""--12345
|
|first part, with a trailing newline
|
|--12345
|Content-Type: application/octet-stream
|Content-Transfer-Encoding: binary
|
|filecontent
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")),
Multipart.General.BodyPart.Strict(
HttpEntity(`application/octet-stream`, "filecontent"),
List(RawHeader("Content-Transfer-Encoding", "binary"))))
}
"illegal headers" in (
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
"""--XYZABC
|Date: unknown
|content-disposition: form-data; name=email
|
|test@there.com
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")),
RawHeader("date", "unknown")))))
"a full example (Strict)" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
"""preamble and
|more preamble
|--12345
|
|first part, implicitly typed
|--12345
|Content-Type: application/octet-stream
|
|second part, explicitly typed
|--12345--
|epilogue and
|more epilogue""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
}
"a full example (Default)" in {
val content = """preamble and
|more preamble
|--12345
|
|first part, implicitly typed
|--12345
|Content-Type: application/octet-stream
|
|second part, explicitly typed
|--12345--
|epilogue and
|more epilogue""".stripMarginWithNewline("\r\n")
val byteStrings = content.map(c ByteString(c.toString)) // one-char ByteStrings
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345", content.length, Source(byteStrings)))
.to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
}
}
"multipartGeneralUnmarshaller should reject illegal multipart content with" - {
"an empty entity" in {
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", ByteString.empty))
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
}
"an entity without initial boundary" in {
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
"""this is
|just preamble text""".stripMarginWithNewline("\r\n")))
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
}
"a stray boundary" in {
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC",
"""--ABC
|Content-type: text/plain; charset=UTF8
|--ABCContent-type: application/json
|content-disposition: form-data; name="email"
|-----""".stripMarginWithNewline("\r\n")))
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal multipart boundary in message content"
}
"duplicate Content-Type header" in {
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
"""---
|Content-type: text/plain; charset=UTF8
|Content-type: application/json
|content-disposition: form-data; name="email"
|
|test@there.com
|-----""".stripMarginWithNewline("\r\n")))
.to[Multipart.General].failed, 1.second).getMessage shouldEqual
"multipart part must not contain more than one Content-Type header"
}
"a missing header-separating CRLF (in Strict entity)" in {
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
"""---
|not good here
|-----""".stripMarginWithNewline("\r\n")))
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
}
"a missing header-separating CRLF (in Default entity)" in {
val content = """---
|
|ok
|---
|not ok
|-----""".stripMarginWithNewline("\r\n")
val byteStrings = content.map(c ByteString(c.toString)) // one-char ByteStrings
val contentType = `multipart/form-data` withBoundary "-"
Await.result(Unmarshal(HttpEntity.Default(contentType, content.length, Source(byteStrings)))
.to[Multipart.General]
.flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
}
}
"multipartByteRangesUnmarshaller should correctly unmarshal multipart/byteranges content with two different parts" in {
Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345",
"""--12345
|Content-Range: bytes 0-2/26
|Content-Type: text/plain
|
|ABC
|--12345
|Content-Range: bytes 23-25/26
|Content-Type: text/plain
|
|XYZ
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.ByteRanges] should haveParts(
Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain`, "ABC")),
Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain`, "XYZ")))
}
"multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - {
"with one element" in {
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
"""--XYZABC
|content-disposition: form-data; name=email
|
|test@there.com
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.FormData] should haveParts(
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")))
}
"with a file" in {
Unmarshal {
HttpEntity.Default(
contentType = `multipart/form-data` withBoundary "XYZABC",
contentLength = 1, // not verified during unmarshalling
data = Source {
List(
ByteString {
"""--XYZABC
|Content-Disposition: form-data; name="email"
|
|test@there.com
|--XYZABC
|Content-Dispo""".stripMarginWithNewline("\r\n")
},
ByteString {
"""sition: form-data; name="userfile"; filename="test.dat"
|Content-Type: application/pdf
|Content-Transfer-Encoding: binary
|
|filecontent
|--XYZABC--""".stripMarginWithNewline("\r\n")
})
})
}.to[Multipart.FormData].flatMap(_.toStrict(1.second)) should haveParts(
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")),
Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(MediaTypes.`application/pdf`, "filecontent"),
Map("filename" -> "test.dat"), List(RawHeader("Content-Transfer-Encoding", "binary"))))
}
// TODO: reactivate after multipart/form-data unmarshalling integrity verification is implemented
//
// "reject illegal multipart content" in {
// val Left(MalformedContent(msg, _)) = HttpEntity(`multipart/form-data` withBoundary "XYZABC", "--noboundary--").as[MultipartFormData]
// msg shouldEqual "Missing start boundary"
// }
// "reject illegal form-data content" in {
// val Left(MalformedContent(msg, _)) = HttpEntity(`multipart/form-data` withBoundary "XYZABC",
// """|--XYZABC
// |content-disposition: form-data; named="email"
// |
// |test@there.com
// |--XYZABC--""".stripMargin).as[MultipartFormData]
// msg shouldEqual "Illegal multipart/form-data content: unnamed body part (no Content-Disposition header or no 'name' parameter)"
// }
}
}
override def afterAll() = system.shutdown()
def haveParts[T <: Multipart](parts: Multipart.BodyPart.Strict*): Matcher[Future[T]] =
equal(parts).matcher[Seq[Multipart.BodyPart.Strict]] compose { x
Await.result(x
.fast.flatMap {
_.parts
.mapAsync(1, _ toStrict 1.second)
.grouped(100)
.runWith(Sink.head)
}
.fast.recover { case _: NoSuchElementException Nil }, 1.second)
}
}

View file

@ -1,17 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.util
import akka.http.server.Route
private[util] abstract class ApplyConverterInstances {
[#implicit def hac1[[#T1#]] = new ApplyConverter[Tuple1[[#T1#]]] {
type In = ([#T1#]) ⇒ Route
def apply(fn: In): (Tuple1[[#T1#]]) ⇒ Route = {
case Tuple1([#t1#]) ⇒ fn([#t1#])
}
}#
]
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.util
private[util] abstract class ConstructFromTupleInstances {
[#implicit def instance1[[#T1#], R](construct: ([#T1#]) => R): ConstructFromTuple[Tuple1[[#T1#]], R] =
new ConstructFromTuple[Tuple1[[#T1#]], R] {
def apply(tup: Tuple1[[#T1#]]): R = construct([#tup._1#])
}#
]
}

View file

@ -1,24 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.util
import TupleOps.AppendOne
private[util] abstract class TupleAppendOneInstances {
type Aux[P, S, Out0] = AppendOne[P, S] { type Out = Out0 }
implicit def append0[T1]: Aux[Unit, T1, Tuple1[T1]] =
new AppendOne[Unit, T1] {
type Out = Tuple1[T1]
def apply(prefix: Unit, last: T1): Tuple1[T1] = Tuple1(last)
}
[1..21#implicit def append1[[#T1#], L]: Aux[Tuple1[[#T1#]], L, Tuple2[[#T1#], L]] =
new AppendOne[Tuple1[[#T1#]], L] {
type Out = Tuple2[[#T1#], L]
def apply(prefix: Tuple1[[#T1#]], last: L): Tuple2[[#T1#], L] = Tuple2([#prefix._1#], last)
}#
]
}

View file

@ -1,33 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server.util
import TupleOps.FoldLeft
import BinaryPolyFunc.Case
private[util] abstract class TupleFoldInstances {
type Aux[In, T, Op, Out0] = FoldLeft[In, T, Op] { type Out = Out0 }
implicit def t0[In, Op]: Aux[In, Unit, Op, In] =
new FoldLeft[In, Unit, Op] {
type Out = In
def apply(zero: In, tuple: Unit) = zero
}
implicit def t1[In, A, Op](implicit f: Case[In, A, Op]): Aux[In, Tuple1[A], Op, f.Out] =
new FoldLeft[In, Tuple1[A], Op] {
type Out = f.Out
def apply(zero: In, tuple: Tuple1[A]) = f(zero, tuple._1)
}
[2..22#implicit def t1[In, [2..#T0#], X, T1, Op](implicit fold: Aux[In, Tuple0[[2..#T0#]], Op, X], f: Case[X, T1, Op]): Aux[In, Tuple1[[#T1#]], Op, f.Out] =
new FoldLeft[In, Tuple1[[#T1#]], Op] {
type Out = f.Out
def apply(zero: In, t: Tuple1[[#T1#]]) =
f(fold(zero, Tuple0([2..#t._0#])), t._1)
}#
]
}

View file

@ -1,43 +0,0 @@
#######################################
# akka-http Reference Config File #
#######################################
# This is the reference config file that contains all the default settings.
# Make your edits/overrides in your application.conf.
akka.http.routing {
# Enables/disables the returning of more detailed error messages to the
# client in the error response
# Should be disabled for browser-facing APIs due to the risk of XSS attacks
# and (probably) enabled for internal or non-browser APIs
# (Note that akka-http will always produce log messages containing the full error details)
verbose-error-messages = off
# Enables/disables ETag and `If-Modified-Since` support for FileAndResourceDirectives
file-get-conditional = on
# Enables/disables the rendering of the "rendered by" footer in directory listings
render-vanity-footer = yes
# The maximum size between two requested ranges. Ranges with less space in between will be coalesced.
#
# When multiple ranges are requested, a server may coalesce any of the ranges that overlap or that are separated
# by a gap that is smaller than the overhead of sending multiple parts, regardless of the order in which the
# corresponding byte-range-spec appeared in the received Range header field. Since the typical overhead between
# parts of a multipart/byteranges payload is around 80 bytes, depending on the selected representation's
# media type and the chosen boundary parameter length, it can be less efficient to transfer many small
# disjoint parts than it is to transfer the entire selected representation.
range-coalescing-threshold = 80
# The maximum number of allowed ranges per request.
# Requests with more ranges will be rejected due to DOS suspicion.
range-count-limit = 16
# The maximum number of bytes per ByteString a decoding directive will produce
# for an entity data stream.
decode-max-bytes-per-chunk = 1m
# Fully qualified config path which holds the dispatcher configuration
# to be used by FlowMaterialiser when creating Actors for IO operations.
file-io-dispatcher = ${akka.stream.file-io-dispatcher}
}

View file

@ -1,99 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.client
import scala.collection.immutable
import scala.concurrent.{ Await, ExecutionContext }
import scala.concurrent.duration._
import scala.reflect.ClassTag
import akka.util.Timeout
import akka.event.{ Logging, LoggingAdapter }
import akka.http.marshalling._
import akka.http.model._
import headers.HttpCredentials
import HttpMethods._
trait RequestBuilding extends TransformerPipelineSupport {
type RequestTransformer = HttpRequest HttpRequest
class RequestBuilder(val method: HttpMethod) {
def apply(): HttpRequest =
apply("/")
def apply(uri: String): HttpRequest =
apply(uri, HttpEntity.Empty)
def apply[T](uri: String, content: T)(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
apply(uri, Some(content))
def apply[T](uri: String, content: Option[T])(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
apply(Uri(uri), content)
def apply(uri: String, entity: RequestEntity): HttpRequest =
apply(Uri(uri), entity)
def apply(uri: Uri): HttpRequest =
apply(uri, HttpEntity.Empty)
def apply[T](uri: Uri, content: T)(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
apply(uri, Some(content))
def apply[T](uri: Uri, content: Option[T])(implicit m: ToEntityMarshaller[T], timeout: Timeout = Timeout(1.second), ec: ExecutionContext): HttpRequest =
content match {
case None apply(uri, HttpEntity.Empty)
case Some(value)
val entity = Await.result(Marshal(value).to[RequestEntity], timeout.duration)
apply(uri, entity)
}
def apply(uri: Uri, entity: RequestEntity): HttpRequest =
HttpRequest(method, uri, Nil, entity)
}
val Get = new RequestBuilder(GET)
val Post = new RequestBuilder(POST)
val Put = new RequestBuilder(PUT)
val Patch = new RequestBuilder(PATCH)
val Delete = new RequestBuilder(DELETE)
val Options = new RequestBuilder(OPTIONS)
val Head = new RequestBuilder(HEAD)
// TODO: reactivate after HTTP message encoding has been ported
//def encode(encoder: Encoder): RequestTransformer = encoder.encode(_, flow)
def addHeader(header: HttpHeader): RequestTransformer = _.mapHeaders(header +: _)
def addHeader(headerName: String, headerValue: String): RequestTransformer =
HttpHeader.parse(headerName, headerValue) match {
case HttpHeader.ParsingResult.Ok(h, Nil) addHeader(h)
case result throw new IllegalArgumentException(result.errors.head.formatPretty)
}
def addHeaders(first: HttpHeader, more: HttpHeader*): RequestTransformer = _.mapHeaders(_ ++ (first +: more))
def mapHeaders(f: immutable.Seq[HttpHeader] immutable.Seq[HttpHeader]): RequestTransformer = _.mapHeaders(f)
def removeHeader(headerName: String): RequestTransformer =
_ mapHeaders (_ filterNot (_.name equalsIgnoreCase headerName))
def removeHeader[T <: HttpHeader: ClassTag]: RequestTransformer =
removeHeader(implicitly[ClassTag[T]].runtimeClass)
def removeHeader(clazz: Class[_]): RequestTransformer =
_ mapHeaders (_ filterNot clazz.isInstance)
def removeHeaders(names: String*): RequestTransformer =
_ mapHeaders (_ filterNot (header names exists (_ equalsIgnoreCase header.name)))
def addCredentials(credentials: HttpCredentials) = addHeader(headers.Authorization(credentials))
def logRequest(log: LoggingAdapter, level: Logging.LogLevel = Logging.DebugLevel) = logValue[HttpRequest](log, level)
def logRequest(logFun: HttpRequest Unit) = logValue[HttpRequest](logFun)
implicit def header2AddHeader(header: HttpHeader): RequestTransformer = addHeader(header)
}
object RequestBuilding extends RequestBuilding

View file

@ -1,49 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.client
import scala.concurrent.{ Future, ExecutionContext }
import akka.event.{ Logging, LoggingAdapter }
trait TransformerPipelineSupport {
def logValue[T](log: LoggingAdapter, level: Logging.LogLevel = Logging.DebugLevel): T T =
logValue { value log.log(level, value.toString) }
def logValue[T](logFun: T Unit): T T = { response
logFun(response)
response
}
implicit class WithTransformation[A](value: A) {
def ~>[B](f: A B): B = f(value)
}
implicit class WithTransformerConcatenation[A, B](f: A B) extends (A B) {
def apply(input: A) = f(input)
def ~>[AA, BB, R](g: AA BB)(implicit aux: TransformerAux[A, B, AA, BB, R]) =
new WithTransformerConcatenation[A, R](aux(f, g))
}
}
object TransformerPipelineSupport extends TransformerPipelineSupport
trait TransformerAux[A, B, AA, BB, R] {
def apply(f: A B, g: AA BB): A R
}
object TransformerAux {
implicit def aux1[A, B, C] = new TransformerAux[A, B, B, C, C] {
def apply(f: A B, g: B C): A C = f andThen g
}
implicit def aux2[A, B, C](implicit ec: ExecutionContext) =
new TransformerAux[A, Future[B], B, C, Future[C]] {
def apply(f: A Future[B], g: B C): A Future[C] = f(_).map(g)
}
implicit def aux3[A, B, C](implicit ec: ExecutionContext) =
new TransformerAux[A, Future[B], B, Future[C], Future[C]] {
def apply(f: A Future[B], g: B Future[C]): A Future[C] = f(_).flatMap(g)
}
}

View file

@ -1,8 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
/** Marker trait for A combined Encoder and Decoder */
trait Coder extends Encoder with Decoder

View file

@ -1,35 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.http.model.{ HttpRequest, HttpResponse, ResponseEntity, RequestEntity }
import akka.util.ByteString
import akka.stream.scaladsl.Flow
/** An abstraction to transform data bytes of HttpMessages or HttpEntities */
sealed trait DataMapper[T] {
def transformDataBytes(t: T, transformer: Flow[ByteString, ByteString, _]): T
}
object DataMapper {
implicit val mapRequestEntity: DataMapper[RequestEntity] =
new DataMapper[RequestEntity] {
def transformDataBytes(t: RequestEntity, transformer: Flow[ByteString, ByteString, _]): RequestEntity =
t.transformDataBytes(transformer)
}
implicit val mapResponseEntity: DataMapper[ResponseEntity] =
new DataMapper[ResponseEntity] {
def transformDataBytes(t: ResponseEntity, transformer: Flow[ByteString, ByteString, _]): ResponseEntity =
t.transformDataBytes(transformer)
}
implicit val mapRequest: DataMapper[HttpRequest] = mapMessage(mapRequestEntity)((m, f) m.withEntity(f(m.entity)))
implicit val mapResponse: DataMapper[HttpResponse] = mapMessage(mapResponseEntity)((m, f) m.withEntity(f(m.entity)))
def mapMessage[T, E](entityMapper: DataMapper[E])(mapEntity: (T, E E) T): DataMapper[T] =
new DataMapper[T] {
def transformDataBytes(t: T, transformer: Flow[ByteString, ByteString, _]): T =
mapEntity(t, entityMapper.transformDataBytes(_, transformer))
}
}

View file

@ -1,54 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.http.model._
import akka.stream.FlowMaterializer
import akka.stream.stage.Stage
import akka.util.ByteString
import headers.HttpEncoding
import akka.stream.scaladsl.{ Sink, Source, Flow }
import scala.concurrent.Future
trait Decoder {
def encoding: HttpEncoding
def decode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self =
if (message.headers exists Encoder.isContentEncodingHeader)
decodeData(message).withHeaders(message.headers filterNot Encoder.isContentEncodingHeader)
else message.self
def decodeData[T](t: T)(implicit mapper: DataMapper[T]): T = mapper.transformDataBytes(t, decoderFlow)
def maxBytesPerChunk: Int
def withMaxBytesPerChunk(maxBytesPerChunk: Int): Decoder
def decoderFlow: Flow[ByteString, ByteString, Unit]
def decode(input: ByteString)(implicit mat: FlowMaterializer): Future[ByteString] =
Source.single(input).via(decoderFlow).runWith(Sink.head)
}
object Decoder {
val MaxBytesPerChunkDefault: Int = 65536
}
/** A decoder that is implemented in terms of a [[Stage]] */
trait StreamDecoder extends Decoder { outer
protected def newDecompressorStage(maxBytesPerChunk: Int): () Stage[ByteString, ByteString]
def maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault
def withMaxBytesPerChunk(newMaxBytesPerChunk: Int): Decoder =
new StreamDecoder {
def encoding: HttpEncoding = outer.encoding
override def maxBytesPerChunk: Int = newMaxBytesPerChunk
def newDecompressorStage(maxBytesPerChunk: Int): () Stage[ByteString, ByteString] =
outer.newDecompressorStage(maxBytesPerChunk)
}
def decoderFlow: Flow[ByteString, ByteString, Unit] =
Flow[ByteString].transform(newDecompressorStage(maxBytesPerChunk))
}

View file

@ -1,138 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import java.util.zip.{ Inflater, Deflater }
import akka.stream.stage._
import akka.util.{ ByteStringBuilder, ByteString }
import scala.annotation.tailrec
import akka.http.util._
import akka.http.model._
import akka.http.model.headers.HttpEncodings
class Deflate(val messageFilter: HttpMessage Boolean) extends Coder with StreamDecoder {
val encoding = HttpEncodings.deflate
def newCompressor = new DeflateCompressor
def newDecompressorStage(maxBytesPerChunk: Int) = () new DeflateDecompressor(maxBytesPerChunk)
}
object Deflate extends Deflate(Encoder.DefaultFilter)
class DeflateCompressor extends Compressor {
protected lazy val deflater = new Deflater(Deflater.BEST_COMPRESSION, false)
override final def compressAndFlush(input: ByteString): ByteString = {
val buffer = newTempBuffer(input.size)
compressWithBuffer(input, buffer) ++ flushWithBuffer(buffer)
}
override final def compressAndFinish(input: ByteString): ByteString = {
val buffer = newTempBuffer(input.size)
compressWithBuffer(input, buffer) ++ finishWithBuffer(buffer)
}
override final def compress(input: ByteString): ByteString = compressWithBuffer(input, newTempBuffer())
override final def flush(): ByteString = flushWithBuffer(newTempBuffer())
override final def finish(): ByteString = finishWithBuffer(newTempBuffer())
protected def compressWithBuffer(input: ByteString, buffer: Array[Byte]): ByteString = {
assert(deflater.needsInput())
deflater.setInput(input.toArray)
drain(buffer)
}
protected def flushWithBuffer(buffer: Array[Byte]): ByteString = {
// trick the deflater into flushing: switch compression level
// FIXME: use proper APIs and SYNC_FLUSH when Java 6 support is dropped
deflater.deflate(EmptyByteArray, 0, 0)
deflater.setLevel(Deflater.NO_COMPRESSION)
val res1 = drain(buffer)
deflater.setLevel(Deflater.BEST_COMPRESSION)
val res2 = drain(buffer)
res1 ++ res2
}
protected def finishWithBuffer(buffer: Array[Byte]): ByteString = {
deflater.finish()
val res = drain(buffer)
deflater.end()
res
}
@tailrec
protected final def drain(buffer: Array[Byte], result: ByteStringBuilder = new ByteStringBuilder()): ByteString = {
val len = deflater.deflate(buffer)
if (len > 0) {
result ++= ByteString.fromArray(buffer, 0, len)
drain(buffer, result)
} else {
assert(deflater.needsInput())
result.result()
}
}
private def newTempBuffer(size: Int = 65536): Array[Byte] =
// The default size is somewhat arbitrary, we'd like to guess a better value but Deflater/zlib
// is buffering in an unpredictable manner.
// `compress` will only return any data if the buffered compressed data has some size in
// the region of 10000-50000 bytes.
// `flush` and `finish` will return any size depending on the previous input.
// This value will hopefully provide a good compromise between memory churn and
// excessive fragmentation of ByteStrings.
new Array[Byte](size)
}
class DeflateDecompressor(maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends DeflateDecompressorBase(maxBytesPerChunk) {
protected def createInflater() = new Inflater()
def initial: State = StartInflate
def afterInflate: State = StartInflate
protected def afterBytesRead(buffer: Array[Byte], offset: Int, length: Int): Unit = {}
protected def onTruncation(ctx: Context[ByteString]): SyncDirective = ctx.finish()
}
abstract class DeflateDecompressorBase(maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends ByteStringParserStage[ByteString] {
protected def createInflater(): Inflater
val inflater = createInflater()
protected def afterInflate: State
protected def afterBytesRead(buffer: Array[Byte], offset: Int, length: Int): Unit
/** Start inflating */
case object StartInflate extends State {
def onPush(data: ByteString, ctx: Context[ByteString]): SyncDirective = {
require(inflater.needsInput())
inflater.setInput(data.toArray)
becomeWithRemaining(Inflate()(data), ByteString.empty, ctx)
}
}
/** Inflate */
case class Inflate()(data: ByteString) extends IntermediateState {
override def onPull(ctx: Context[ByteString]): SyncDirective = {
val buffer = new Array[Byte](maxBytesPerChunk)
val read = inflater.inflate(buffer)
if (read > 0) {
afterBytesRead(buffer, 0, read)
ctx.push(ByteString.fromArray(buffer, 0, read))
} else {
val remaining = data.takeRight(inflater.getRemaining)
val next =
if (inflater.finished()) afterInflate
else StartInflate
becomeWithRemaining(next, remaining, ctx)
}
}
def onPush(elem: ByteString, ctx: Context[ByteString]): SyncDirective =
throw new IllegalStateException("Don't expect a new Element")
}
def becomeWithRemaining(next: State, remaining: ByteString, ctx: Context[ByteString]) = {
become(next)
if (remaining.isEmpty) current.onPull(ctx)
else current.onPush(remaining, ctx)
}
}

View file

@ -1,77 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.http.model._
import akka.http.util.StreamUtils
import akka.stream.stage.Stage
import akka.util.ByteString
import headers._
import akka.stream.scaladsl.Flow
trait Encoder {
def encoding: HttpEncoding
def messageFilter: HttpMessage Boolean
def encode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self =
if (messageFilter(message) && !message.headers.exists(Encoder.isContentEncodingHeader))
encodeData(message).withHeaders(`Content-Encoding`(encoding) +: message.headers)
else message.self
def encodeData[T](t: T)(implicit mapper: DataMapper[T]): T =
mapper.transformDataBytes(t, Flow[ByteString].transform(newEncodeTransformer))
def encode(input: ByteString): ByteString = newCompressor.compressAndFinish(input)
def newCompressor: Compressor
def newEncodeTransformer(): Stage[ByteString, ByteString] = {
val compressor = newCompressor
def encodeChunk(bytes: ByteString): ByteString = compressor.compressAndFlush(bytes)
def finish(): ByteString = compressor.finish()
StreamUtils.byteStringTransformer(encodeChunk, finish)
}
}
object Encoder {
val DefaultFilter: HttpMessage Boolean = {
case req: HttpRequest isCompressible(req)
case res @ HttpResponse(status, _, _, _) isCompressible(res) && status.isSuccess
}
private[coding] def isCompressible(msg: HttpMessage): Boolean =
msg.entity.contentType.mediaType.compressible
private[coding] val isContentEncodingHeader: HttpHeader Boolean = _.isInstanceOf[`Content-Encoding`]
}
/** A stateful object representing ongoing compression. */
abstract class Compressor {
/**
* Compresses the given input and returns compressed data. The implementation
* can and will choose to buffer output data to improve compression. Use
* `flush` or `compressAndFlush` to make sure that all input data has been
* compressed and pending output data has been returned.
*/
def compress(input: ByteString): ByteString
/**
* Flushes any output data and returns the currently remaining compressed data.
*/
def flush(): ByteString
/**
* Closes this compressed stream and return the remaining compressed data. After
* calling this method, this Compressor cannot be used any further.
*/
def finish(): ByteString
/** Combines `compress` + `flush` */
def compressAndFlush(input: ByteString): ByteString
/** Combines `compress` + `finish` */
def compressAndFinish(input: ByteString): ByteString
}

View file

@ -1,145 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.util.ByteString
import akka.stream.stage._
import akka.http.util.ByteReader
import java.util.zip.{ Inflater, CRC32, ZipException, Deflater }
import akka.http.model._
import headers.HttpEncodings
class Gzip(val messageFilter: HttpMessage Boolean) extends Coder with StreamDecoder {
val encoding = HttpEncodings.gzip
def newCompressor = new GzipCompressor
def newDecompressorStage(maxBytesPerChunk: Int) = () new GzipDecompressor(maxBytesPerChunk)
}
/**
* An encoder and decoder for the HTTP 'gzip' encoding.
*/
object Gzip extends Gzip(Encoder.DefaultFilter) {
def apply(messageFilter: HttpMessage Boolean) = new Gzip(messageFilter)
}
class GzipCompressor extends DeflateCompressor {
override protected lazy val deflater = new Deflater(Deflater.BEST_COMPRESSION, true)
private val checkSum = new CRC32 // CRC32 of uncompressed data
private var headerSent = false
private var bytesRead = 0L
override protected def compressWithBuffer(input: ByteString, buffer: Array[Byte]): ByteString = {
updateCrc(input)
header() ++ super.compressWithBuffer(input, buffer)
}
override protected def flushWithBuffer(buffer: Array[Byte]): ByteString = header() ++ super.flushWithBuffer(buffer)
override protected def finishWithBuffer(buffer: Array[Byte]): ByteString = super.finishWithBuffer(buffer) ++ trailer()
private def updateCrc(input: ByteString): Unit = {
checkSum.update(input.toArray)
bytesRead += input.length
}
private def header(): ByteString =
if (!headerSent) {
headerSent = true
GzipDecompressor.Header
} else ByteString.empty
private def trailer(): ByteString = {
def int32(i: Int): ByteString = ByteString(i, i >> 8, i >> 16, i >> 24)
val crc = checkSum.getValue.toInt
val tot = bytesRead.toInt // truncated to 32bit as specified in https://tools.ietf.org/html/rfc1952#section-2
val trailer = int32(crc) ++ int32(tot)
trailer
}
}
class GzipDecompressor(maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends DeflateDecompressorBase(maxBytesPerChunk) {
protected def createInflater(): Inflater = new Inflater(true)
def initial: State = Initial
/** No bytes were received yet */
case object Initial extends State {
def onPush(data: ByteString, ctx: Context[ByteString]): SyncDirective =
if (data.isEmpty) ctx.pull()
else becomeWithRemaining(ReadHeaders, data, ctx)
override def onPull(ctx: Context[ByteString]): SyncDirective =
if (ctx.isFinishing) {
ctx.finish()
} else super.onPull(ctx)
}
var crc32: CRC32 = new CRC32
protected def afterInflate: State = ReadTrailer
/** Reading the header bytes */
case object ReadHeaders extends ByteReadingState {
def read(reader: ByteReader, ctx: Context[ByteString]): SyncDirective = {
import reader._
if (readByte() != 0x1F || readByte() != 0x8B) fail("Not in GZIP format") // check magic header
if (readByte() != 8) fail("Unsupported GZIP compression method") // check compression method
val flags = readByte()
skip(6) // skip MTIME, XFL and OS fields
if ((flags & 4) > 0) skip(readShortLE()) // skip optional extra fields
if ((flags & 8) > 0) skipZeroTerminatedString() // skip optional file name
if ((flags & 16) > 0) skipZeroTerminatedString() // skip optional file comment
if ((flags & 2) > 0 && crc16(fromStartToHere) != readShortLE()) fail("Corrupt GZIP header")
inflater.reset()
crc32.reset()
becomeWithRemaining(StartInflate, remainingData, ctx)
}
}
protected def afterBytesRead(buffer: Array[Byte], offset: Int, length: Int): Unit =
crc32.update(buffer, offset, length)
/** Reading the trailer */
case object ReadTrailer extends ByteReadingState {
def read(reader: ByteReader, ctx: Context[ByteString]): SyncDirective = {
import reader._
if (readIntLE() != crc32.getValue.toInt) fail("Corrupt data (CRC32 checksum error)")
if (readIntLE() != inflater.getBytesWritten.toInt /* truncated to 32bit */ ) fail("Corrupt GZIP trailer ISIZE")
becomeWithRemaining(Initial, remainingData, ctx)
}
}
override def onUpstreamFinish(ctx: Context[ByteString]): TerminationDirective = ctx.absorbTermination()
private def crc16(data: ByteString) = {
val crc = new CRC32
crc.update(data.toArray)
crc.getValue.toInt & 0xFFFF
}
override protected def onTruncation(ctx: Context[ByteString]): SyncDirective = ctx.fail(new ZipException("Truncated GZIP stream"))
private def fail(msg: String) = throw new ZipException(msg)
}
/** INTERNAL API */
private[http] object GzipDecompressor {
// RFC 1952: http://tools.ietf.org/html/rfc1952 section 2.2
val Header = ByteString(
0x1F, // ID1
0x8B, // ID2
8, // CM = Deflate
0, // FLG
0, // MTIME 1
0, // MTIME 2
0, // MTIME 3
0, // MTIME 4
0, // XFL
0 // OS
)
}

View file

@ -1,39 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.coding
import akka.http.model._
import akka.http.util.StreamUtils
import akka.stream.stage.Stage
import akka.util.ByteString
import headers.HttpEncodings
/**
* An encoder and decoder for the HTTP 'identity' encoding.
*/
object NoCoding extends Coder with StreamDecoder {
val encoding = HttpEncodings.identity
override def encode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self = message.self
override def encodeData[T](t: T)(implicit mapper: DataMapper[T]): T = t
override def decode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self = message.self
override def decodeData[T](t: T)(implicit mapper: DataMapper[T]): T = t
val messageFilter: HttpMessage Boolean = _ false
def newCompressor = NoCodingCompressor
def newDecompressorStage(maxBytesPerChunk: Int): () Stage[ByteString, ByteString] =
() StreamUtils.limitByteChunksStage(maxBytesPerChunk)
}
object NoCodingCompressor extends Compressor {
def compress(input: ByteString): ByteString = input
def flush() = ByteString.empty
def finish() = ByteString.empty
def compressAndFlush(input: ByteString): ByteString = input
def compressAndFinish(input: ByteString): ByteString = input
}

View file

@ -1,39 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.common
import akka.http.unmarshalling.{ FromStringUnmarshaller FSU }
private[http] trait ToNameReceptacleEnhancements {
implicit def symbol2NR(symbol: Symbol) = new NameReceptacle[String](symbol.name)
implicit def string2NR(string: String) = new NameReceptacle[String](string)
}
object ToNameReceptacleEnhancements extends ToNameReceptacleEnhancements
class NameReceptacle[T](val name: String) {
def as[B] = new NameReceptacle[B](name)
def as[B](unmarshaller: FSU[B]) = new NameUnmarshallerReceptacle(name, unmarshaller)
def ? = new NameOptionReceptacle[T](name)
def ?[B](default: B) = new NameDefaultReceptacle(name, default)
def ![B](requiredValue: B) = new RequiredValueReceptacle(name, requiredValue)
}
class NameUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) {
def ? = new NameOptionUnmarshallerReceptacle[T](name, um)
def ?(default: T) = new NameDefaultUnmarshallerReceptacle(name, default, um)
def !(requiredValue: T) = new RequiredValueUnmarshallerReceptacle(name, requiredValue, um)
}
class NameOptionReceptacle[T](val name: String)
class NameDefaultReceptacle[T](val name: String, val default: T)
class RequiredValueReceptacle[T](val name: String, val requiredValue: T)
class NameOptionUnmarshallerReceptacle[T](val name: String, val um: FSU[T])
class NameDefaultUnmarshallerReceptacle[T](val name: String, val default: T, val um: FSU[T])
class RequiredValueUnmarshallerReceptacle[T](val name: String, val requiredValue: T, val um: FSU[T])

View file

@ -1,134 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.common
import scala.annotation.implicitNotFound
import scala.collection.immutable
import scala.concurrent.{ ExecutionContext, Future }
import scala.concurrent.duration._
import akka.stream.FlowMaterializer
import akka.http.util.FastFuture
import akka.http.unmarshalling._
import akka.http.model._
import FastFuture._
/**
* Read-only abstraction on top of `application/x-www-form-urlencoded` and multipart form data,
* allowing joint unmarshalling access to either kind, **if** you supply both, a [[FromStringUnmarshaller]]
* as well as a [[FromEntityUnmarshaller]] for the target type `T`.
* Note: In order to allow for random access to the field values streamed multipart form data are strictified!
* Don't use this abstraction on potentially unbounded forms (e.g. large file uploads).
*
* If you only need to consume one type of form (`application/x-www-form-urlencoded` *or* multipart) then
* simply unmarshal directly to the respective form abstraction ([[FormData]] or [[Multipart.FormData]])
* rather than going through [[StrictForm]].
*
* Simple usage example:
* {{{
* val strictFormFuture = Unmarshal(entity).to[StrictForm]
* val fooFieldUnmarshalled: Future[T] =
* strictFormFuture flatMap { form =>
* Unmarshal(form field "foo").to[T]
* }
* }}}
*/
sealed abstract class StrictForm {
def fields: immutable.Seq[(String, StrictForm.Field)]
def field(name: String): Option[StrictForm.Field] = fields collectFirst { case (`name`, field) field }
}
object StrictForm {
sealed trait Field
object Field {
private[StrictForm] final case class FromString(value: String) extends Field
private[StrictForm] final case class FromPart(value: Multipart.FormData.BodyPart.Strict) extends Field
implicit def unmarshaller[T](implicit um: FieldUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] =
Unmarshaller {
case FromString(value) um.unmarshalString(value)
case FromPart(value) um.unmarshalPart(value)
}
def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] =
Unmarshaller {
case FromString(value) fsu(value)
case FromPart(value) fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name))
}
@implicitNotFound("In order to unmarshal a `StrictForm.Field` to type `${T}` you need to supply a " +
"`FromStringUnmarshaller[${T}]` and/or a `FromEntityUnmarshaller[${T}]`")
sealed trait FieldUnmarshaller[T] {
def unmarshalString(value: String): Future[T]
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict): Future[T]
}
object FieldUnmarshaller extends LowPrioImplicits {
implicit def fromBoth[T](implicit fsu: FromStringUnmarshaller[T], feu: FromEntityUnmarshaller[T]) =
new FieldUnmarshaller[T] {
def unmarshalString(value: String) = fsu(value)
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict) = feu(value.entity)
}
}
sealed abstract class LowPrioImplicits {
implicit def fromFSU[T](implicit fsu: FromStringUnmarshaller[T]) =
new FieldUnmarshaller[T] {
def unmarshalString(value: String) = fsu(value)
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict) =
fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name))
}
implicit def fromFEU[T](implicit feu: FromEntityUnmarshaller[T]) =
new FieldUnmarshaller[T] {
def unmarshalString(value: String) = feu(HttpEntity(value))
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict) = feu(value.entity)
}
}
}
implicit def unmarshaller(implicit formDataUM: FromEntityUnmarshaller[FormData],
multipartUM: FromEntityUnmarshaller[Multipart.FormData],
ec: ExecutionContext, fm: FlowMaterializer): FromEntityUnmarshaller[StrictForm] = {
def tryUnmarshalToQueryForm(entity: HttpEntity): Future[StrictForm] =
for (formData formDataUM(entity).fast) yield {
new StrictForm {
val fields = formData.fields.map { case (name, value) name -> Field.FromString(value) }(collection.breakOut)
}
}
def tryUnmarshalToMultipartForm(entity: HttpEntity): Future[StrictForm] =
for {
multiPartFD multipartUM(entity).fast
strictMultiPartFD multiPartFD.toStrict(10.seconds).fast // TODO: make timeout configurable
} yield {
new StrictForm {
val fields = strictMultiPartFD.strictParts.map {
case x: Multipart.FormData.BodyPart.Strict x.name -> Field.FromPart(x)
}(collection.breakOut)
}
}
Unmarshaller { entity
tryUnmarshalToQueryForm(entity).fast.recoverWith {
case Unmarshaller.UnsupportedContentTypeException(supported1)
tryUnmarshalToMultipartForm(entity).fast.recoverWith {
case Unmarshaller.UnsupportedContentTypeException(supported2)
FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(supported1 ++ supported2))
}
}
}
}
/**
* Simple model for strict file content in a multipart form data part.
*/
final case class FileData(filename: Option[String], entity: HttpEntity.Strict)
object FileData {
implicit val unmarshaller: FromStrictFormFieldUnmarshaller[FileData] =
Unmarshaller strict {
case Field.FromString(_) throw Unmarshaller.UnsupportedContentTypeException(MediaTypes.`application/x-www-form-urlencoded`)
case Field.FromPart(part) FileData(part.filename, part.entity)
}
}
}

View file

@ -1,16 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.collection.immutable
import akka.http.model._
class EmptyValue[+T] private (val emptyValue: T)
object EmptyValue {
implicit def emptyEntity = new EmptyValue[UniversalEntity](HttpEntity.Empty)
implicit val emptyHeadersAndEntity = new EmptyValue[(immutable.Seq[HttpHeader], UniversalEntity)](Nil -> HttpEntity.Empty)
implicit val emptyResponse = new EmptyValue[HttpResponse](HttpResponse(entity = emptyEntity.emptyValue))
}

View file

@ -1,38 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.concurrent.{ ExecutionContext, Future }
import scala.util.{ Try, Failure, Success }
import akka.http.util.FastFuture
import FastFuture._
trait GenericMarshallers extends LowPriorityToResponseMarshallerImplicits {
implicit def throwableMarshaller[T]: Marshaller[Throwable, T] = Marshaller(FastFuture.failed)
implicit def optionMarshaller[A, B](implicit m: Marshaller[A, B], empty: EmptyValue[B]): Marshaller[Option[A], B] =
Marshaller {
case Some(value) m(value)
case None FastFuture.successful(Marshalling.Opaque(() empty.emptyValue) :: Nil)
}
implicit def eitherMarshaller[A1, A2, B](implicit m1: Marshaller[A1, B], m2: Marshaller[A2, B]): Marshaller[Either[A1, A2], B] =
Marshaller {
case Left(a1) m1(a1)
case Right(a2) m2(a2)
}
implicit def futureMarshaller[A, B](implicit m: Marshaller[A, B], ec: ExecutionContext): Marshaller[Future[A], B] =
Marshaller(_.fast.flatMap(m(_)))
implicit def tryMarshaller[A, B](implicit m: Marshaller[A, B]): Marshaller[Try[A], B] =
Marshaller {
case Success(value) m(value)
case Failure(error) FastFuture.failed(error)
}
}
object GenericMarshallers extends GenericMarshallers

View file

@ -1,76 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.concurrent.{ ExecutionContext, Future }
import akka.http.util.FastFuture
import akka.http.model.HttpCharsets._
import akka.http.model._
import FastFuture._
object Marshal {
def apply[T](value: T): Marshal[T] = new Marshal(value)
case class UnacceptableResponseContentTypeException(supported: Set[ContentType]) extends RuntimeException
private class MarshallingWeight(val weight: Float, val marshal: () HttpResponse)
}
class Marshal[A](val value: A) {
/**
* Marshals `value` using the first available [[Marshalling]] for `A` and `B` provided by the given [[Marshaller]].
* If the marshalling is flexible with regard to the used charset `UTF-8` is chosen.
*/
def to[B](implicit m: Marshaller[A, B], ec: ExecutionContext): Future[B] =
m(value).fast.map {
_.head match {
case Marshalling.WithFixedCharset(_, _, marshal) marshal()
case Marshalling.WithOpenCharset(_, marshal) marshal(HttpCharsets.`UTF-8`)
case Marshalling.Opaque(marshal) marshal()
}
}
/**
* Marshals `value` to an `HttpResponse` for the given `HttpRequest` with full content-negotiation.
*/
def toResponseFor(request: HttpRequest)(implicit m: ToResponseMarshaller[A], ec: ExecutionContext): Future[HttpResponse] = {
import akka.http.marshalling.Marshal._
val mediaRanges = request.acceptedMediaRanges // cache for performance
val charsetRanges = request.acceptedCharsetRanges // cache for performance
def qValueMT(mediaType: MediaType) = request.qValueForMediaType(mediaType, mediaRanges)
def qValueCS(charset: HttpCharset) = request.qValueForCharset(charset, charsetRanges)
m(value).fast.map { marshallings
val defaultMarshallingWeight = new MarshallingWeight(0f, { ()
val supportedContentTypes = marshallings collect {
case Marshalling.WithFixedCharset(mt, cs, _) ContentType(mt, cs)
case Marshalling.WithOpenCharset(mt, _) ContentType(mt)
}
throw UnacceptableResponseContentTypeException(supportedContentTypes.toSet)
})
def choose(acc: MarshallingWeight, mt: MediaType, cs: HttpCharset, marshal: () HttpResponse) = {
val weight = math.min(qValueMT(mt), qValueCS(cs))
if (weight > acc.weight) new MarshallingWeight(weight, marshal) else acc
}
val best = marshallings.foldLeft(defaultMarshallingWeight) {
case (acc, Marshalling.WithFixedCharset(mt, cs, marshal))
choose(acc, mt, cs, marshal)
case (acc, Marshalling.WithOpenCharset(mt, marshal))
def withCharset(cs: HttpCharset) = choose(acc, mt, cs, () marshal(cs))
// logic for choosing the charset adapted from http://tools.ietf.org/html/rfc7231#section-5.3.3
if (qValueCS(`UTF-8`) == 1f) withCharset(`UTF-8`) // prefer UTF-8 if fully accepted
else charsetRanges match {
// pick the charset which the highest q-value (head of charsetRanges) if it isn't explicitly rejected
case (HttpCharsetRange.One(cs, qValue)) :: _ if qValue > 0f withCharset(cs)
case _ acc
}
case (acc, Marshalling.Opaque(marshal))
if (acc.weight == 0f) new MarshallingWeight(Float.MinPositiveValue, marshal) else acc
}
best.marshal()
}
}
}

View file

@ -1,134 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.concurrent.{ Future, ExecutionContext }
import scala.util.control.NonFatal
import akka.http.util.FastFuture
import akka.http.model._
import FastFuture._
sealed abstract class Marshaller[-A, +B] extends (A Future[List[Marshalling[B]]]) {
def map[C](f: B C)(implicit ec: ExecutionContext): Marshaller[A, C] =
Marshaller[A, C](value this(value).fast map (_ map (_ map f)))
/**
* Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides
* the produced [[ContentType]] with another one.
* Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying
* marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal.
* For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]]
* that has a defined charset of UTF-8, since akka-http will never recode entities.
* If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]].
*/
def wrap[C, D >: B](contentType: ContentType)(f: C A)(implicit ec: ExecutionContext, mto: MediaTypeOverrider[D]): Marshaller[C, D] =
Marshaller { value
import Marshalling._
this(f(value)).fast map {
_ map {
case WithFixedCharset(_, cs, marshal) if contentType.hasOpenCharset || contentType.charset == cs
WithFixedCharset(contentType.mediaType, cs, () mto(marshal(), contentType.mediaType))
case WithOpenCharset(_, marshal) if contentType.hasOpenCharset
WithOpenCharset(contentType.mediaType, cs mto(marshal(cs), contentType.mediaType))
case WithOpenCharset(_, marshal)
WithFixedCharset(contentType.mediaType, contentType.charset, () mto(marshal(contentType.charset), contentType.mediaType))
case Opaque(marshal) if contentType.definedCharset.isEmpty Opaque(() mto(marshal(), contentType.mediaType))
case x sys.error(s"Illegal marshaller wrapping. Marshalling `$x` cannot be wrapped with ContentType `$contentType`")
}
}
}
override def compose[C](f: C A): Marshaller[C, B] = Marshaller(super.compose(f))
}
object Marshaller
extends GenericMarshallers
with PredefinedToEntityMarshallers
with PredefinedToResponseMarshallers
with PredefinedToRequestMarshallers {
/**
* Creates a [[Marshaller]] from the given function.
*/
def apply[A, B](f: A Future[List[Marshalling[B]]]): Marshaller[A, B] =
new Marshaller[A, B] {
def apply(value: A) =
try f(value)
catch { case NonFatal(e) FastFuture.failed(e) }
}
/**
* Helper for creating a [[Marshaller]] using the given function.
*/
def strict[A, B](f: A Marshalling[B]): Marshaller[A, B] =
Marshaller { a FastFuture.successful(f(a) :: Nil) }
/**
* Helper for creating a "super-marshaller" from a number of "sub-marshallers".
* Content-negotiation determines, which "sub-marshaller" eventually gets to do the job.
*/
def oneOf[A, B](marshallers: Marshaller[A, B]*)(implicit ec: ExecutionContext): Marshaller[A, B] =
Marshaller { a FastFuture.sequence(marshallers.map(_(a))).fast.map(_.flatten.toList) }
/**
* Helper for creating a "super-marshaller" from a number of values and a function producing "sub-marshallers"
* from these values. Content-negotiation determines, which "sub-marshaller" eventually gets to do the job.
*/
def oneOf[T, A, B](values: T*)(f: T Marshaller[A, B])(implicit ec: ExecutionContext): Marshaller[A, B] =
oneOf(values map f: _*)
/**
* Helper for creating a synchronous [[Marshaller]] to content with a fixed charset from the given function.
*/
def withFixedCharset[A, B](mediaType: MediaType, charset: HttpCharset)(marshal: A B): Marshaller[A, B] =
strict { value Marshalling.WithFixedCharset(mediaType, charset, () marshal(value)) }
/**
* Helper for creating a synchronous [[Marshaller]] to content with a negotiable charset from the given function.
*/
def withOpenCharset[A, B](mediaType: MediaType)(marshal: (A, HttpCharset) B): Marshaller[A, B] =
strict { value Marshalling.WithOpenCharset(mediaType, charset marshal(value, charset)) }
/**
* Helper for creating a synchronous [[Marshaller]] to non-negotiable content from the given function.
*/
def opaque[A, B](marshal: A B): Marshaller[A, B] =
strict { value Marshalling.Opaque(() marshal(value)) }
}
/**
* Describes one possible option for marshalling a given value.
*/
sealed trait Marshalling[+A] {
def map[B](f: A B): Marshalling[B]
}
object Marshalling {
/**
* A Marshalling to a specific MediaType and charset.
*/
final case class WithFixedCharset[A](mediaType: MediaType,
charset: HttpCharset,
marshal: () A) extends Marshalling[A] {
def map[B](f: A B): WithFixedCharset[B] = copy(marshal = () f(marshal()))
}
/**
* A Marshalling to a specific MediaType and a potentially flexible charset.
*/
final case class WithOpenCharset[A](mediaType: MediaType,
marshal: HttpCharset A) extends Marshalling[A] {
def map[B](f: A B): WithOpenCharset[B] = copy(marshal = cs f(marshal(cs)))
}
/**
* A Marshalling to an unknown MediaType and charset.
* Circumvents content negotiation.
*/
final case class Opaque[A](marshal: () A) extends Marshalling[A] {
def map[B](f: A B): Opaque[B] = copy(marshal = () f(marshal()))
}
}

View file

@ -1,30 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.collection.immutable
import akka.http.model._
sealed trait MediaTypeOverrider[T] {
def apply(value: T, mediaType: MediaType): T
}
object MediaTypeOverrider {
implicit def forEntity[T <: HttpEntity]: MediaTypeOverrider[T] = new MediaTypeOverrider[T] {
def apply(value: T, mediaType: MediaType) =
value.withContentType(value.contentType withMediaType mediaType).asInstanceOf[T] // can't be expressed in types
}
implicit def forHeadersAndEntity[T <: HttpEntity] = new MediaTypeOverrider[(immutable.Seq[HttpHeader], T)] {
def apply(value: (immutable.Seq[HttpHeader], T), mediaType: MediaType) =
value._1 -> value._2.withContentType(value._2.contentType withMediaType mediaType).asInstanceOf[T]
}
implicit val forResponse = new MediaTypeOverrider[HttpResponse] {
def apply(value: HttpResponse, mediaType: MediaType) =
value.mapEntity(forEntity(_: ResponseEntity, mediaType))
}
implicit val forRequest = new MediaTypeOverrider[HttpRequest] {
def apply(value: HttpRequest, mediaType: MediaType) =
value.mapEntity(forEntity(_: RequestEntity, mediaType))
}
}

View file

@ -1,48 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import akka.event.{ NoLogging, LoggingAdapter }
import scala.concurrent.forkjoin.ThreadLocalRandom
import akka.parboiled2.util.Base64
import akka.stream.scaladsl.FlattenStrategy
import akka.stream.scaladsl._
import akka.http.engine.rendering.BodyPartRenderer
import akka.http.util.FastFuture
import akka.http.model._
trait MultipartMarshallers {
protected val multipartBoundaryRandom: java.util.Random = ThreadLocalRandom.current()
/**
* Creates a new random 144-bit number and base64 encodes it (using a custom "safe" alphabet, yielding 24 characters).
*/
def randomBoundary: String = {
val array = new Array[Byte](18)
multipartBoundaryRandom.nextBytes(array)
Base64.custom.encodeToString(array, false)
}
implicit def multipartMarshaller[T <: Multipart](implicit log: LoggingAdapter = NoLogging): ToEntityMarshaller[T] =
Marshaller strict { value
val boundary = randomBoundary
val contentType = ContentType(value.mediaType withBoundary boundary)
Marshalling.WithOpenCharset(contentType.mediaType, { charset
value match {
case x: Multipart.Strict
val data = BodyPartRenderer.strict(x.strictParts, boundary, charset.nioCharset, partHeadersSizeHint = 128, log)
HttpEntity(contentType, data)
case _
val chunks = value.parts
.transform(() BodyPartRenderer.streamed(boundary, charset.nioCharset, partHeadersSizeHint = 128, log))
.flatten(FlattenStrategy.concat)
HttpEntity.Chunked(contentType, chunks)
}
})
}
}
object MultipartMarshallers extends MultipartMarshallers

View file

@ -1,68 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import java.nio.CharBuffer
import scala.concurrent.ExecutionContext
import akka.http.model.parser.CharacterClasses
import akka.http.model.MediaTypes._
import akka.http.model._
import akka.http.util.{ FastFuture, StringRendering }
import akka.util.ByteString
trait PredefinedToEntityMarshallers extends MultipartMarshallers {
implicit val ByteArrayMarshaller: ToEntityMarshaller[Array[Byte]] = byteArrayMarshaller(`application/octet-stream`)
def byteArrayMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[Array[Byte]] = {
val ct = ContentType(mediaType, charset)
Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes HttpEntity(ct, bytes) }
}
def byteArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Byte]] = {
val ct = ContentType(mediaType)
// since we don't want to recode we simply ignore the charset determined by content negotiation here
Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) HttpEntity(ct, bytes) }
}
implicit val ByteStringMarshaller: ToEntityMarshaller[ByteString] = byteStringMarshaller(`application/octet-stream`)
def byteStringMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[ByteString] = {
val ct = ContentType(mediaType, charset)
Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes HttpEntity(ct, bytes) }
}
def byteStringMarshaller(mediaType: MediaType): ToEntityMarshaller[ByteString] = {
val ct = ContentType(mediaType)
// since we don't want to recode we simply ignore the charset determined by content negotiation here
Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) HttpEntity(ct, bytes) }
}
implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`)
def charArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Char]] =
Marshaller.withOpenCharset(mediaType) { (value, charset)
if (value.length > 0) {
val charBuffer = CharBuffer.wrap(value)
val byteBuffer = charset.nioCharset.encode(charBuffer)
val array = new Array[Byte](byteBuffer.remaining())
byteBuffer.get(array)
HttpEntity(ContentType(mediaType, charset), array)
} else HttpEntity.Empty
}
implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`)
def stringMarshaller(mediaType: MediaType): ToEntityMarshaller[String] =
Marshaller.withOpenCharset(mediaType) { (s, cs) HttpEntity(ContentType(mediaType, cs), s) }
implicit val FormDataMarshaller: ToEntityMarshaller[FormData] =
Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { (formData, charset)
val query = Uri.Query(formData.fields: _*)
val string = UriRendering.renderQuery(new StringRendering, query, charset.nioCharset, CharacterClasses.unreserved).get
HttpEntity(ContentType(`application/x-www-form-urlencoded`, charset), string)
}
implicit val HttpEntityMarshaller: ToEntityMarshaller[MessageEntity] = Marshaller strict { value
Marshalling.WithFixedCharset(value.contentType.mediaType, value.contentType.charset, () value)
}
}
object PredefinedToEntityMarshallers extends PredefinedToEntityMarshallers

View file

@ -1,31 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.collection.immutable
import scala.concurrent.ExecutionContext
import akka.http.util.FastFuture
import akka.http.model._
import FastFuture._
trait PredefinedToRequestMarshallers {
private type TRM[T] = ToRequestMarshaller[T] // brevity alias
implicit val fromRequest: TRM[HttpRequest] = Marshaller.opaque(identity)
implicit def fromUri(implicit ec: ExecutionContext): TRM[Uri] =
Marshaller strict { uri Marshalling.Opaque(() HttpRequest(uri = uri)) }
implicit def fromMethodAndUriAndValue[S, T](implicit mt: ToEntityMarshaller[T],
ec: ExecutionContext): TRM[(HttpMethod, Uri, T)] =
fromMethodAndUriAndHeadersAndValue[T] compose { case (m, u, v) (m, u, Nil, v) }
implicit def fromMethodAndUriAndHeadersAndValue[T](implicit mt: ToEntityMarshaller[T],
ec: ExecutionContext): TRM[(HttpMethod, Uri, immutable.Seq[HttpHeader], T)] =
Marshaller { case (m, u, h, v) mt(v).fast map (_ map (_ map (HttpRequest(m, u, h, _)))) }
}
object PredefinedToRequestMarshallers extends PredefinedToRequestMarshallers

View file

@ -1,48 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.collection.immutable
import scala.concurrent.ExecutionContext
import akka.http.util.FastFuture._
import akka.http.model.MediaTypes._
import akka.http.model._
trait PredefinedToResponseMarshallers extends LowPriorityToResponseMarshallerImplicits {
private type TRM[T] = ToResponseMarshaller[T] // brevity alias
def fromToEntityMarshaller[T](status: StatusCode = StatusCodes.OK, headers: immutable.Seq[HttpHeader] = Nil)(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): ToResponseMarshaller[T] =
fromStatusCodeAndHeadersAndValue.compose(t (status, headers, t))
implicit val fromResponse: TRM[HttpResponse] = Marshaller.opaque(identity)
implicit val fromStatusCode: TRM[StatusCode] =
Marshaller.withOpenCharset(`text/plain`) { (status, charset)
HttpResponse(status, entity = HttpEntity(ContentType(`text/plain`, charset), status.defaultMessage))
}
implicit def fromStatusCodeAndValue[S, T](implicit sConv: S StatusCode, mt: ToEntityMarshaller[T],
ec: ExecutionContext): TRM[(S, T)] =
fromStatusCodeAndHeadersAndValue[T].compose { case (status, value) (sConv(status), Nil, value) }
implicit def fromStatusCodeConvertibleAndHeadersAndT[S, T](implicit sConv: S StatusCode, mt: ToEntityMarshaller[T],
ec: ExecutionContext): TRM[(S, immutable.Seq[HttpHeader], T)] =
fromStatusCodeAndHeadersAndValue[T].compose { case (status, headers, value) (sConv(status), headers, value) }
implicit def fromStatusCodeAndHeadersAndValue[T](implicit mt: ToEntityMarshaller[T],
ec: ExecutionContext): TRM[(StatusCode, immutable.Seq[HttpHeader], T)] =
Marshaller { case (status, headers, value) mt(value).fast map (_ map (_ map (HttpResponse(status, headers, _)))) }
}
trait LowPriorityToResponseMarshallerImplicits {
implicit def liftMarshallerConversion[T](m: ToEntityMarshaller[T])(implicit ec: ExecutionContext): ToResponseMarshaller[T] =
liftMarshaller(m, ec)
implicit def liftMarshaller[T](implicit m: ToEntityMarshaller[T], ec: ExecutionContext): ToResponseMarshaller[T] =
PredefinedToResponseMarshallers.fromToEntityMarshaller()
}
object PredefinedToResponseMarshallers extends PredefinedToResponseMarshallers

View file

@ -1,30 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.marshalling
import scala.concurrent.{ Future, ExecutionContext }
import akka.http.model._
/** Something that can later be marshalled into a response */
trait ToResponseMarshallable {
type T
def value: T
implicit def marshaller: ToResponseMarshaller[T]
def apply(request: HttpRequest)(implicit ec: ExecutionContext): Future[HttpResponse] =
Marshal(value).toResponseFor(request)
}
object ToResponseMarshallable {
implicit def apply[A](_value: A)(implicit _marshaller: ToResponseMarshaller[A]): ToResponseMarshallable =
new ToResponseMarshallable {
type T = A
def value: T = _value
def marshaller: ToResponseMarshaller[T] = _marshaller
}
implicit val marshaller: ToResponseMarshaller[ToResponseMarshallable] =
Marshaller { marshallable marshallable.marshaller(marshallable.value) }
}

View file

@ -1,15 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http
import scala.collection.immutable
import akka.http.model._
package object marshalling {
type ToEntityMarshaller[T] = Marshaller[T, MessageEntity]
type ToHeadersAndEntityMarshaller[T] = Marshaller[T, (immutable.Seq[HttpHeader], MessageEntity)]
type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]
type ToRequestMarshaller[T] = Marshaller[T, HttpRequest]
}

View file

@ -1,162 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.collection.immutable
import akka.http.server.directives.RouteDirectives
import akka.http.server.util._
import akka.http.util.FastFuture
import FastFuture._
/**
* A directive that provides a tuple of values of type `L` to create an inner route.
*/
abstract class Directive[L](implicit val ev: Tuple[L]) {
/**
* Calls the inner route with a tuple of extracted values of type `L`.
*
* `tapply` is short for "tuple-apply". Usually, you will use the regular `apply` method instead,
* which is added by an implicit conversion (see `Directive.addDirectiveApply`).
*/
def tapply(f: L Route): Route
/**
* Joins two directives into one which runs the second directive if the first one rejects.
*/
def |[R >: L](that: Directive[R]): Directive[R] =
recover(rejections directives.BasicDirectives.mapRejections(rejections ++ _) & that)(that.ev)
/**
* Joins two directives into one which extracts the concatenation of its base directive extractions.
* NOTE: Extraction joining is an O(N) operation with N being the number of extractions on the right-side.
*/
def &(magnet: ConjunctionMagnet[L]): magnet.Out = magnet(this)
/**
* Converts this directive into one which, instead of a tuple of type ``L``, creates an
* instance of type ``A`` (which is usually a case class).
*/
def as[A](constructor: ConstructFromTuple[L, A]): Directive1[A] = tmap(constructor)
/**
* Maps over this directive using the given function, which can produce either a tuple or any other value
* (which will then we wrapped into a [[Tuple1]]).
*/
def tmap[R](f: L R)(implicit tupler: Tupler[R]): Directive[tupler.Out] =
Directive[tupler.Out] { inner tapply { values inner(tupler(f(values))) } }(tupler.OutIsTuple)
/**
* Flatmaps this directive using the given function.
*/
def tflatMap[R: Tuple](f: L Directive[R]): Directive[R] =
Directive[R] { inner tapply { values f(values) tapply inner } }
/**
* Creates a new [[Directive0]], which passes if the given predicate matches the current
* extractions or rejects with the given rejections.
*/
def trequire(predicate: L Boolean, rejections: Rejection*): Directive0 =
tfilter(predicate, rejections: _*).tflatMap(_ Directive.Empty)
/**
* Creates a new directive of the same type, which passes if the given predicate matches the current
* extractions or rejects with the given rejections.
*/
def tfilter(predicate: L Boolean, rejections: Rejection*): Directive[L] =
Directive[L] { inner tapply { values ctx if (predicate(values)) inner(values)(ctx) else ctx.reject(rejections: _*) } }
/**
* Creates a new directive that is able to recover from rejections that were produced by `this` Directive
* **before the inner route was applied**.
*/
def recover[R >: L: Tuple](recovery: immutable.Seq[Rejection] Directive[R]): Directive[R] =
Directive[R] { inner
ctx
import ctx.executionContext
@volatile var rejectedFromInnerRoute = false
tapply({ list c rejectedFromInnerRoute = true; inner(list)(c) })(ctx).fast.flatMap {
case RouteResult.Rejected(rejections) if !rejectedFromInnerRoute recovery(rejections).tapply(inner)(ctx)
case x FastFuture.successful(x)
}
}
/**
* Variant of `recover` that only recovers from rejections handled by the given PartialFunction.
*/
def recoverPF[R >: L: Tuple](recovery: PartialFunction[immutable.Seq[Rejection], Directive[R]]): Directive[R] =
recover { rejections recovery.applyOrElse(rejections, (rejs: Seq[Rejection]) RouteDirectives.reject(rejs: _*)) }
}
object Directive {
/**
* Constructs a directive from a function literal.
* Note: [[Directive]] itself is sealed to keep the type monomorphic.
*/
def apply[T: Tuple](f: (T Route) Route): Directive[T] =
new Directive[T] { def tapply(inner: T Route) = f(inner) }
/**
* A Directive that always passes the request on to its inner route (i.e. does nothing).
*/
val Empty: Directive0 = Directive(_())
/**
* Adds `apply` to all Directives with 1 or more extractions,
* which allows specifying an n-ary function to receive the extractions instead of a Function1[TupleX, Route].
*/
implicit def addDirectiveApply[L](directive: Directive[L])(implicit hac: ApplyConverter[L]): hac.In Route =
f directive.tapply(hac(f))
/**
* Adds `apply` to Directive0. Note: The `apply` parameter is call-by-name to ensure consistent execution behavior
* with the directives producing extractions.
*/
implicit def addByNameNullaryApply(directive: Directive0): ( Route) Route =
r directive.tapply(_ r)
implicit class SingleValueModifiers[T](underlying: Directive1[T]) extends AnyRef {
def map[R](f: T R)(implicit tupler: Tupler[R]): Directive[tupler.Out] =
underlying.tmap { case Tuple1(value) f(value) }
def flatMap[R: Tuple](f: T Directive[R]): Directive[R] =
underlying.tflatMap { case Tuple1(value) f(value) }
def require(predicate: T Boolean, rejections: Rejection*): Directive0 =
underlying.filter(predicate, rejections: _*).tflatMap(_ Empty)
def filter(predicate: T Boolean, rejections: Rejection*): Directive1[T] =
underlying.tfilter({ case Tuple1(value) predicate(value) }, rejections: _*)
}
}
trait ConjunctionMagnet[L] {
type Out
def apply(underlying: Directive[L]): Out
}
object ConjunctionMagnet {
implicit def fromDirective[L, R](other: Directive[R])(implicit join: TupleOps.Join[L, R]): ConjunctionMagnet[L] { type Out = Directive[join.Out] } =
new ConjunctionMagnet[L] {
type Out = Directive[join.Out]
def apply(underlying: Directive[L]) =
Directive[join.Out] { inner
underlying.tapply { prefix other.tapply { suffix inner(join(prefix, suffix)) } }
}(Tuple.yes) // we know that join will only ever produce tuples
}
implicit def fromStandardRoute[L](route: StandardRoute) =
new ConjunctionMagnet[L] {
type Out = StandardRoute
def apply(underlying: Directive[L]) = StandardRoute(underlying.tapply(_ route))
}
implicit def fromRouteGenerator[T, R <: Route](generator: T R) =
new ConjunctionMagnet[Unit] {
type Out = RouteGenerator[T]
def apply(underlying: Directive0) = value underlying.tapply(_ generator(value))
}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import directives._
// FIXME: the comments are kept as a reminder which directives are not yet imported
trait Directives extends RouteConcatenation
with AuthenticationDirectives
with BasicDirectives
with CacheConditionDirectives
//with ChunkingDirectives
with CookieDirectives
with DebuggingDirectives
with CodingDirectives
with ExecutionDirectives
with FileAndResourceDirectives
with FormFieldDirectives
with FutureDirectives
with HeaderDirectives
with HostDirectives
with MarshallingDirectives
with MethodDirectives
with MiscDirectives
with ParameterDirectives
with PathDirectives
with RangeDirectives
with RespondWithDirectives
with RouteDirectives
with SchemeDirectives
with SecurityDirectives
with WebsocketDirectives
object Directives extends Directives

View file

@ -1,52 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.concurrent.ExecutionContext
import scala.util.control.NonFatal
import akka.http.model._
import StatusCodes._
trait ExceptionHandler extends ExceptionHandler.PF {
/**
* Creates a new [[ExceptionHandler]] which uses the given one as fallback for this one.
*/
def withFallback(that: ExceptionHandler): ExceptionHandler
/**
* "Seals" this handler by attaching a default handler as fallback if necessary.
*/
def seal(settings: RoutingSettings)(implicit ec: ExecutionContext): ExceptionHandler
}
object ExceptionHandler {
type PF = PartialFunction[Throwable, Route]
implicit def apply(pf: PF): ExceptionHandler = apply(knownToBeSealed = false)(pf)
private def apply(knownToBeSealed: Boolean)(pf: PF): ExceptionHandler =
new ExceptionHandler {
def isDefinedAt(error: Throwable) = pf.isDefinedAt(error)
def apply(error: Throwable) = pf(error)
def withFallback(that: ExceptionHandler): ExceptionHandler =
if (!knownToBeSealed) ExceptionHandler(knownToBeSealed = false)(this orElse that) else this
def seal(settings: RoutingSettings)(implicit ec: ExecutionContext): ExceptionHandler =
if (!knownToBeSealed) ExceptionHandler(knownToBeSealed = true)(this orElse default(settings)) else this
}
def default(settings: RoutingSettings)(implicit ec: ExecutionContext): ExceptionHandler =
apply(knownToBeSealed = true) {
case IllegalRequestException(info, status) ctx {
ctx.log.warning("Illegal request {}\n\t{}\n\tCompleting with '{}' response",
ctx.request, info.formatPretty, status)
ctx.complete(status, info.format(settings.verboseErrorMessages))
}
case NonFatal(e) ctx {
ctx.log.error(e, "Error during processing of request {}", ctx.request)
ctx.complete(InternalServerError)
}
}
}

View file

@ -1,475 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import java.util.UUID
import scala.util.matching.Regex
import scala.annotation.tailrec
import akka.http.server.util.Tuple
import akka.http.server.util.TupleOps._
import akka.http.common.NameOptionReceptacle
import akka.http.model.Uri.Path
import akka.http.util._
/**
* A PathMatcher tries to match a prefix of a given string and returns either a PathMatcher.Matched instance
* if matched, otherwise PathMatcher.Unmatched.
*/
abstract class PathMatcher[L](implicit val ev: Tuple[L]) extends (Path PathMatcher.Matching[L]) { self
import PathMatcher._
def / : PathMatcher[L] = this ~ PathMatchers.Slash
def /[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] =
this ~ PathMatchers.Slash ~ other
def |[R >: L: Tuple](other: PathMatcher[_ <: R]): PathMatcher[R] =
new PathMatcher[R] {
def apply(path: Path) = self(path) orElse other(path)
}
def ~[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] = {
implicit def joinProducesTuple = Tuple.yes[join.Out]
transform(_.andThen((restL, valuesL) other(restL).map(join(valuesL, _))))
}
def unary_!(): PathMatcher0 =
new PathMatcher[Unit] {
def apply(path: Path) = if (self(path) eq Unmatched) Matched(path, ()) else Unmatched
}
def transform[R: Tuple](f: Matching[L] Matching[R]): PathMatcher[R] =
new PathMatcher[R] { def apply(path: Path) = f(self(path)) }
def tmap[R: Tuple](f: L R): PathMatcher[R] = transform(_.map(f))
def tflatMap[R: Tuple](f: L Option[R]): PathMatcher[R] = transform(_.flatMap(f))
/**
* Same as ``repeat(min = count, max = count)``.
*/
def repeat(count: Int)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
repeat(min = count, max = count)
/**
* Same as ``repeat(min = count, max = count, separator = separator)``.
*/
def repeat(count: Int, separator: PathMatcher0)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
repeat(min = count, max = count, separator = separator)
/**
* Turns this ``PathMatcher`` into one that matches a number of times (with the given separator)
* and potentially extracts a ``List`` of the underlying matcher's extractions.
* If less than ``min`` applications of the underlying matcher have succeeded the produced matcher fails,
* otherwise it matches up to the given ``max`` number of applications.
* Note that it won't fail even if more than ``max`` applications could succeed!
* The "surplus" path elements will simply be left unmatched.
*
* The result type depends on the type of the underlying matcher:
*
* <table>
* <th><td>If a ``matcher`` is of type</td><td>then ``matcher.repeat(...)`` is of type</td></th>
* <tr><td>``PathMatcher0``</td><td>``PathMatcher0``</td></tr>
* <tr><td>``PathMatcher1[T]``</td><td>``PathMatcher1[List[T]``</td></tr>
* <tr><td>``PathMatcher[L :Tuple]``</td><td>``PathMatcher[List[L]]``</td></tr>
* </table>
*/
def repeat(min: Int, max: Int, separator: PathMatcher0 = PathMatchers.Neutral)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
require(min >= 0, "`min` must be >= 0")
require(max >= min, "`max` must be >= `min`")
def apply(path: Path) = rec(path, 1)
def rec(path: Path, count: Int): Matching[lift.Out] = {
def done = if (count >= min) Matched(path, lift()) else Unmatched
if (count <= max) {
self(path) match {
case Matched(remaining, extractions)
def done1 = if (count >= min) Matched(remaining, lift(extractions)) else Unmatched
separator(remaining) match {
case Matched(remaining2, _) rec(remaining2, count + 1) match {
case Matched(`remaining2`, _) done1 // we made no progress, so "go back" to before the separator
case Matched(rest, result) Matched(rest, lift(extractions, result))
case Unmatched Unmatched
}
case Unmatched done1
}
case Unmatched done
}
} else done
}
}
}
object PathMatcher extends ImplicitPathMatcherConstruction {
sealed abstract class Matching[+L: Tuple] {
def map[R: Tuple](f: L R): Matching[R]
def flatMap[R: Tuple](f: L Option[R]): Matching[R]
def andThen[R: Tuple](f: (Path, L) Matching[R]): Matching[R]
def orElse[R >: L](other: Matching[R]): Matching[R]
}
case class Matched[L: Tuple](pathRest: Path, extractions: L) extends Matching[L] {
def map[R: Tuple](f: L R) = Matched(pathRest, f(extractions))
def flatMap[R: Tuple](f: L Option[R]) = f(extractions) match {
case Some(valuesR) Matched(pathRest, valuesR)
case None Unmatched
}
def andThen[R: Tuple](f: (Path, L) Matching[R]) = f(pathRest, extractions)
def orElse[R >: L](other: Matching[R]) = this
}
object Matched { val Empty = Matched(Path.Empty, ()) }
case object Unmatched extends Matching[Nothing] {
def map[R: Tuple](f: Nothing R) = this
def flatMap[R: Tuple](f: Nothing Option[R]) = this
def andThen[R: Tuple](f: (Path, Nothing) Matching[R]) = this
def orElse[R](other: Matching[R]) = other
}
/**
* Creates a PathMatcher that always matches, consumes nothing and extracts the given Tuple of values.
*/
def provide[L: Tuple](extractions: L): PathMatcher[L] =
new PathMatcher[L] {
def apply(path: Path) = Matched(path, extractions)(ev)
}
/**
* Creates a PathMatcher that matches and consumes the given path prefix and extracts the given list of extractions.
* If the given prefix is empty the returned PathMatcher matches always and consumes nothing.
*/
def apply[L: Tuple](prefix: Path, extractions: L): PathMatcher[L] =
if (prefix.isEmpty) provide(extractions)
else new PathMatcher[L] {
def apply(path: Path) =
if (path startsWith prefix) Matched(path dropChars prefix.charCount, extractions)(ev)
else Unmatched
}
def apply[L](magnet: PathMatcher[L]): PathMatcher[L] = magnet
implicit class PathMatcher1Ops[T](matcher: PathMatcher1[T]) {
def map[R](f: T R): PathMatcher1[R] = matcher.tmap { case Tuple1(e) Tuple1(f(e)) }
def flatMap[R](f: T Option[R]): PathMatcher1[R] =
matcher.tflatMap { case Tuple1(e) f(e).map(x Tuple1(x)) }
}
implicit class EnhancedPathMatcher[L](underlying: PathMatcher[L]) {
def ?(implicit lift: PathMatcher.Lift[L, Option]): PathMatcher[lift.Out] =
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
def apply(path: Path) = underlying(path) match {
case Matched(rest, extractions) Matched(rest, lift(extractions))
case Unmatched Matched(path, lift())
}
}
}
sealed trait Lift[L, M[+_]] {
type Out
def OutIsTuple: Tuple[Out]
def apply(): Out
def apply(value: L): Out
def apply(value: L, more: Out): Out
}
object Lift extends LowLevelLiftImplicits {
trait MOps[M[+_]] {
def apply(): M[Nothing]
def apply[T](value: T): M[T]
def apply[T](value: T, more: M[T]): M[T]
}
object MOps {
implicit object OptionMOps extends MOps[Option] {
def apply(): Option[Nothing] = None
def apply[T](value: T): Option[T] = Some(value)
def apply[T](value: T, more: Option[T]): Option[T] = Some(value)
}
implicit object ListMOps extends MOps[List] {
def apply(): List[Nothing] = Nil
def apply[T](value: T): List[T] = value :: Nil
def apply[T](value: T, more: List[T]): List[T] = value :: more
}
}
implicit def liftUnit[M[+_]] = new Lift[Unit, M] {
type Out = Unit
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = ()
def apply(value: Unit) = value
def apply(value: Unit, more: Out) = value
}
implicit def liftSingleElement[A, M[+_]](implicit mops: MOps[M]) = new Lift[Tuple1[A], M] {
type Out = Tuple1[M[A]]
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = Tuple1(mops())
def apply(value: Tuple1[A]) = Tuple1(mops(value._1))
def apply(value: Tuple1[A], more: Out) = Tuple1(mops(value._1, more._1))
}
}
trait LowLevelLiftImplicits {
import Lift._
implicit def default[T, M[+_]](implicit mops: MOps[M]) = new Lift[T, M] {
type Out = Tuple1[M[T]]
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = Tuple1(mops())
def apply(value: T) = Tuple1(mops(value))
def apply(value: T, more: Out) = Tuple1(mops(value, more._1))
}
}
}
trait ImplicitPathMatcherConstruction {
import PathMatcher._
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* (if the path begins with a segment) and extracts a given value.
*/
implicit def stringExtractionPair2PathMatcher[T](tuple: (String, T)): PathMatcher1[T] =
PathMatcher(tuple._1 :: Path.Empty, Tuple1(tuple._2))
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* (if the path begins with a segment).
*/
implicit def segmentStringToPathMatcher(segment: String): PathMatcher0 =
PathMatcher(segment :: Path.Empty, ())
implicit def stringNameOptionReceptacle2PathMatcher(nr: NameOptionReceptacle[String]): PathMatcher0 =
PathMatcher(nr.name).?
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* if the path begins with a segment (a prefix of) which matches the given regex.
* Extracts either the complete match (if the regex doesn't contain a capture group) or
* the capture group (if the regex contains exactly one).
* If the regex contains more than one capture group the method throws an IllegalArgumentException.
*/
implicit def regex2PathMatcher(regex: Regex): PathMatcher1[String] = regex.groupCount match {
case 0 new PathMatcher1[String] {
def apply(path: Path) = path match {
case Path.Segment(segment, tail) regex findPrefixOf segment match {
case Some(m) Matched(segment.substring(m.length) :: tail, Tuple1(m))
case None Unmatched
}
case _ Unmatched
}
}
case 1 new PathMatcher1[String] {
def apply(path: Path) = path match {
case Path.Segment(segment, tail) regex findPrefixMatchOf segment match {
case Some(m) Matched(segment.substring(m.end) :: tail, Tuple1(m.group(1)))
case None Unmatched
}
case _ Unmatched
}
}
case _ throw new IllegalArgumentException("Path regex '" + regex.pattern.pattern +
"' must not contain more than one capturing group")
}
/**
* Creates a PathMatcher from the given Map of path segments (prefixes) to extracted values.
* If the unmatched path starts with a segment having one of the maps keys as a prefix
* the matcher consumes this path segment (prefix) and extracts the corresponding map value.
*/
implicit def valueMap2PathMatcher[T](valueMap: Map[String, T]): PathMatcher1[T] =
if (valueMap.isEmpty) PathMatchers.nothingMatcher
else valueMap.map { case (prefix, value) stringExtractionPair2PathMatcher(prefix, value) }.reduceLeft(_ | _)
}
trait PathMatchers {
import PathMatcher._
/**
* Converts a path string containing slashes into a PathMatcher that interprets slashes as
* path segment separators.
*/
def separateOnSlashes(string: String): PathMatcher0 = {
@tailrec def split(ix: Int = 0, matcher: PathMatcher0 = null): PathMatcher0 = {
val nextIx = string.indexOf('/', ix)
def append(m: PathMatcher0) = if (matcher eq null) m else matcher / m
if (nextIx < 0) append(string.substring(ix))
else split(nextIx + 1, append(string.substring(ix, nextIx)))
}
split()
}
/**
* A PathMatcher that matches a single slash character ('/').
*/
object Slash extends PathMatcher0 {
def apply(path: Path) = path match {
case Path.Slash(tail) Matched(tail, ())
case _ Unmatched
}
}
/**
* A PathMatcher that matches the very end of the requests URI path.
*/
object PathEnd extends PathMatcher0 {
def apply(path: Path) = path match {
case Path.Empty Matched.Empty
case _ Unmatched
}
}
/**
* A PathMatcher that matches and extracts the complete remaining,
* unmatched part of the request's URI path as an (encoded!) String.
* If you need access to the remaining unencoded elements of the path
* use the `RestPath` matcher!
*/
object Rest extends PathMatcher1[String] {
def apply(path: Path) = Matched(Path.Empty, Tuple1(path.toString))
}
/**
* A PathMatcher that matches and extracts the complete remaining,
* unmatched part of the request's URI path.
*/
object RestPath extends PathMatcher1[Path] {
def apply(path: Path) = Matched(Path.Empty, Tuple1(path))
}
/**
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Int value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
* than Int.MaxValue.
*/
object IntNumber extends NumberMatcher[Int](Int.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
/**
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Long value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
* than Long.MaxValue.
*/
object LongNumber extends NumberMatcher[Long](Long.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
/**
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Int value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
* than Int.MaxValue.
*/
object HexIntNumber extends NumberMatcher[Int](Int.MaxValue, 16) {
def fromChar(c: Char) = fromHexChar(c)
}
/**
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Long value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
* than Long.MaxValue.
*/
object HexLongNumber extends NumberMatcher[Long](Long.MaxValue, 16) {
def fromChar(c: Char) = fromHexChar(c)
}
// common implementation of Number matchers
abstract class NumberMatcher[@specialized(Int, Long) T](max: T, base: T)(implicit x: Integral[T])
extends PathMatcher1[T] {
import x._ // import implicit conversions for numeric operators
val minusOne = x.zero - x.one
val maxDivBase = max / base
def apply(path: Path) = path match {
case Path.Segment(segment, tail)
@tailrec def digits(ix: Int = 0, value: T = minusOne): Matching[Tuple1[T]] = {
val a = if (ix < segment.length) fromChar(segment charAt ix) else minusOne
if (a == minusOne) {
if (value == minusOne) Unmatched
else Matched(if (ix < segment.length) segment.substring(ix) :: tail else tail, Tuple1(value))
} else {
if (value == minusOne) digits(ix + 1, a)
else if (value <= maxDivBase && value * base <= max - a) // protect from overflow
digits(ix + 1, value * base + a)
else Unmatched
}
}
digits()
case _ Unmatched
}
def fromChar(c: Char): T
def fromDecimalChar(c: Char): T = if ('0' <= c && c <= '9') x.fromInt(c - '0') else minusOne
def fromHexChar(c: Char): T =
if ('0' <= c && c <= '9') x.fromInt(c - '0') else {
val cn = c | 0x20 // normalize to lowercase
if ('a' <= cn && cn <= 'f') x.fromInt(cn - 'a' + 10) else minusOne
}
}
/**
* A PathMatcher that matches and extracts a Double value. The matched string representation is the pure decimal,
* optionally signed form of a double value, i.e. without exponent.
*/
val DoubleNumber: PathMatcher1[Double] =
PathMatcher("""[+-]?\d*\.?\d*""".r) flatMap { string
try Some(java.lang.Double.parseDouble(string))
catch { case _: NumberFormatException None }
}
/**
* A PathMatcher that matches and extracts a java.util.UUID instance.
*/
val JavaUUID: PathMatcher1[UUID] =
PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r) flatMap { string
try Some(UUID.fromString(string))
catch { case _: IllegalArgumentException None }
}
/**
* A PathMatcher that always matches, doesn't consume anything and extracts nothing.
* Serves mainly as a neutral element in PathMatcher composition.
*/
val Neutral: PathMatcher0 = PathMatcher.provide(())
/**
* A PathMatcher that matches if the unmatched path starts with a path segment.
* If so the path segment is extracted as a String.
*/
object Segment extends PathMatcher1[String] {
def apply(path: Path) = path match {
case Path.Segment(segment, tail) Matched(tail, Tuple1(segment))
case _ Unmatched
}
}
/**
* A PathMatcher that matches up to 128 remaining segments as a List[String].
* This can also be no segments resulting in the empty list.
* If the path has a trailing slash this slash will *not* be matched.
*/
val Segments: PathMatcher1[List[String]] = Segments(min = 0, max = 128)
/**
* A PathMatcher that matches the given number of path segments (separated by slashes) as a List[String].
* If there are more than ``count`` segments present the remaining ones will be left unmatched.
* If the path has a trailing slash this slash will *not* be matched.
*/
def Segments(count: Int): PathMatcher1[List[String]] = Segment.repeat(count, separator = Slash)
/**
* A PathMatcher that matches between ``min`` and ``max`` (both inclusively) path segments (separated by slashes)
* as a List[String]. If there are more than ``count`` segments present the remaining ones will be left unmatched.
* If the path has a trailing slash this slash will *not* be matched.
*/
def Segments(min: Int, max: Int): PathMatcher1[List[String]] = Segment.repeat(min, max, separator = Slash)
/**
* A PathMatcher that never matches anything.
*/
def nothingMatcher[L: Tuple]: PathMatcher[L] =
new PathMatcher[L] {
def apply(p: Path) = Unmatched
}
}
object PathMatchers extends PathMatchers

View file

@ -1,204 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.collection.immutable
import akka.http.model._
import headers._
/**
* A rejection encapsulates a specific reason why a Route was not able to handle a request. Rejections are gathered
* up over the course of a Route evaluation and finally converted to [[spray.http.HttpResponse]]s by the
* `handleRejections` directive, if there was no way for the request to be completed.
*/
trait Rejection
/**
* Rejection created by method filters.
* Signals that the request was rejected because the HTTP method is unsupported.
*/
case class MethodRejection(supported: HttpMethod) extends Rejection
/**
* Rejection created by scheme filters.
* Signals that the request was rejected because the Uri scheme is unsupported.
*/
case class SchemeRejection(supported: String) extends Rejection
/**
* Rejection created by parameter filters.
* Signals that the request was rejected because a query parameter was not found.
*/
case class MissingQueryParamRejection(parameterName: String) extends Rejection
/**
* Rejection created by parameter filters.
* Signals that the request was rejected because a query parameter could not be interpreted.
*/
case class MalformedQueryParamRejection(parameterName: String, errorMsg: String,
cause: Option[Throwable] = None) extends Rejection
/**
* Rejection created by form field filters.
* Signals that the request was rejected because a form field was not found.
*/
case class MissingFormFieldRejection(fieldName: String) extends Rejection
/**
* Rejection created by form field filters.
* Signals that the request was rejected because a form field could not be interpreted.
*/
case class MalformedFormFieldRejection(fieldName: String, errorMsg: String,
cause: Option[Throwable] = None) extends Rejection
/**
* Rejection created by header directives.
* Signals that the request was rejected because a required header could not be found.
*/
case class MissingHeaderRejection(headerName: String) extends Rejection
/**
* Rejection created by header directives.
* Signals that the request was rejected because a header value is malformed.
*/
case class MalformedHeaderRejection(headerName: String, errorMsg: String,
cause: Option[Throwable] = None) extends Rejection
/**
* Rejection created by unmarshallers.
* Signals that the request was rejected because the requests content-type is unsupported.
*/
case class UnsupportedRequestContentTypeRejection(supported: Set[ContentTypeRange]) extends Rejection
/**
* Rejection created by decoding filters.
* Signals that the request was rejected because the requests content encoding is unsupported.
*/
case class UnsupportedRequestEncodingRejection(supported: HttpEncoding) extends Rejection
/**
* Rejection created by range directives.
* Signals that the request was rejected because the requests contains only unsatisfiable ByteRanges.
* The actualEntityLength gives the client a hint to create satisfiable ByteRanges.
*/
case class UnsatisfiableRangeRejection(unsatisfiableRanges: Seq[ByteRange], actualEntityLength: Long) extends Rejection
/**
* Rejection created by range directives.
* Signals that the request contains too many ranges. An irregular high number of ranges
* indicates a broken client or a denial of service attack.
*/
case class TooManyRangesRejection(maxRanges: Int) extends Rejection
/**
* Rejection created by unmarshallers.
* Signals that the request was rejected because unmarshalling failed with an error that wasn't
* an `IllegalArgumentException`. Usually that means that the request content was not of the expected format.
* Note that semantic issues with the request content (e.g. because some parameter was out of range)
* will usually trigger a `ValidationRejection` instead.
*/
case class MalformedRequestContentRejection(message: String, cause: Option[Throwable] = None) extends Rejection
/**
* Rejection created by unmarshallers.
* Signals that the request was rejected because an message body entity was expected but not supplied.
*/
case object RequestEntityExpectedRejection extends Rejection
/**
* Rejection created by marshallers.
* Signals that the request was rejected because the service is not capable of producing a response entity whose
* content type is accepted by the client
*/
case class UnacceptedResponseContentTypeRejection(supported: Set[ContentType]) extends Rejection
/**
* Rejection created by encoding filters.
* Signals that the request was rejected because the service is not capable of producing a response entity whose
* content encoding is accepted by the client
*/
case class UnacceptedResponseEncodingRejection(supported: Set[HttpEncoding]) extends Rejection
object UnacceptedResponseEncodingRejection {
def apply(supported: HttpEncoding): UnacceptedResponseEncodingRejection = UnacceptedResponseEncodingRejection(Set(supported))
}
/**
* Rejection created by an [[akka.http.server.authentication.HttpAuthenticator]].
* Signals that the request was rejected because the user could not be authenticated. The reason for the rejection is
* specified in the cause.
*/
case class AuthenticationFailedRejection(cause: AuthenticationFailedRejection.Cause,
challenge: HttpChallenge) extends Rejection
object AuthenticationFailedRejection {
/**
* Signals the cause of the failed authentication.
*/
sealed trait Cause
/**
* Signals the cause of the rejecting was that the user could not be authenticated, because the `WWW-Authenticate`
* header was not supplied.
*/
case object CredentialsMissing extends Cause
/**
* Signals the cause of the rejecting was that the user could not be authenticated, because the supplied credentials
* are invalid.
*/
case object CredentialsRejected extends Cause
}
/**
* Rejection created by the 'authorize' directive.
* Signals that the request was rejected because the user is not authorized.
*/
case object AuthorizationFailedRejection extends Rejection
/**
* Rejection created by the `cookie` directive.
* Signals that the request was rejected because a cookie was not found.
*/
case class MissingCookieRejection(cookieName: String) extends Rejection
/**
* Rejection created when a websocket request was expected but none was found.
*/
case object ExpectedWebsocketRequestRejection extends Rejection
/**
* Rejection created by the `validation` directive as well as for `IllegalArgumentExceptions`
* thrown by domain model constructors (e.g. via `require`).
* It signals that an expected value was semantically invalid.
*/
case class ValidationRejection(message: String, cause: Option[Throwable] = None) extends Rejection
/**
* A special Rejection that serves as a container for a transformation function on rejections.
* It is used by some directives to "cancel" rejections that are added by later directives of a similar type.
*
* Consider this route structure for example:
*
* put { reject(ValidationRejection("no") } ~ get { ... }
*
* If this structure is applied to a PUT request the list of rejections coming back contains three elements:
*
* 1. A ValidationRejection
* 2. A MethodRejection
* 3. A TransformationRejection holding a function filtering out the MethodRejection
*
* so that in the end the RejectionHandler will only see one rejection (the ValidationRejection), because the
* MethodRejection added by the ``get`` directive is cancelled by the ``put`` directive (since the HTTP method
* did indeed match eventually).
*/
case class TransformationRejection(transform: immutable.Seq[Rejection] immutable.Seq[Rejection]) extends Rejection
/**
* A Throwable wrapping a Rejection.
* Can be used for marshalling `Future[T]` or `Try[T]` instances, whose failure side is supposed to trigger a route
* rejection rather than an Exception that is handled by the nearest ExceptionHandler.
* (Custom marshallers can of course use it as well.)
*/
case class RejectionError(rejection: Rejection) extends RuntimeException

View file

@ -1,227 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.annotation.tailrec
import scala.reflect.ClassTag
import scala.collection.immutable
import scala.concurrent.ExecutionContext
import akka.http.model._
import StatusCodes._
import headers._
import directives.RouteDirectives._
import AuthenticationFailedRejection._
trait RejectionHandler extends (immutable.Seq[Rejection] Option[Route]) { self
import RejectionHandler._
/**
* Creates a new [[RejectionHandler]] which uses the given one as fallback for this one.
*/
def withFallback(that: RejectionHandler): RejectionHandler =
(this, that) match {
case (a: BuiltRejectionHandler, b: BuiltRejectionHandler)
new BuiltRejectionHandler(a.cases ++ b.cases, a.notFound orElse b.notFound, a.isSealed || b.isSealed)
case _ new RejectionHandler {
def apply(rejections: immutable.Seq[Rejection]): Option[Route] =
self(rejections) orElse that(rejections)
}
}
/**
* "Seals" this handler by attaching a default handler as fallback if necessary.
*/
def seal(implicit ec: ExecutionContext): RejectionHandler =
this match {
case x: BuiltRejectionHandler if x.isSealed x
case _ withFallback(default)
}
}
object RejectionHandler {
/**
* Creates a new [[RejectionHandler]] builder.
*/
def newBuilder(): Builder = new Builder
final class Builder {
private[this] val cases = new immutable.VectorBuilder[Handler]
private[this] var notFound: Option[Route] = None
private[this] var hasCatchAll: Boolean = false
/**
* Handles a single [[Rejection]] with the given partial function.
*/
def handle(pf: PartialFunction[Rejection, Route]): this.type = {
cases += CaseHandler(pf)
hasCatchAll ||= pf.isDefinedAt(PrivateRejection)
this
}
/**
* Handles several Rejections of the same type at the same time.
* The seq passed to the given function is guaranteed to be non-empty.
*/
def handleAll[T <: Rejection: ClassTag](f: immutable.Seq[T] Route): this.type = {
val runtimeClass = implicitly[ClassTag[T]].runtimeClass
cases += TypeHandler[T](runtimeClass, f)
hasCatchAll ||= runtimeClass == classOf[Rejection]
this
}
/**
* Handles the special "not found" case using the given [[Route]].
*/
def handleNotFound(route: Route): this.type = {
notFound = Some(route)
this
}
def result(): RejectionHandler =
new BuiltRejectionHandler(cases.result(), notFound, hasCatchAll && notFound.isDefined)
}
private sealed abstract class Handler
private final case class CaseHandler(pf: PartialFunction[Rejection, Route]) extends Handler
private final case class TypeHandler[T <: Rejection](
runtimeClass: Class[_], f: immutable.Seq[T] Route) extends Handler with PartialFunction[Rejection, T] {
def isDefinedAt(rejection: Rejection) = runtimeClass isInstance rejection
def apply(rejection: Rejection) = rejection.asInstanceOf[T]
}
private class BuiltRejectionHandler(val cases: Vector[Handler],
val notFound: Option[Route],
val isSealed: Boolean) extends RejectionHandler {
def apply(rejections: immutable.Seq[Rejection]): Option[Route] =
if (rejections.nonEmpty) {
@tailrec def rec(ix: Int): Option[Route] =
if (ix < cases.length) {
cases(ix) match {
case CaseHandler(pf)
val route = rejections collectFirst pf
if (route.isEmpty) rec(ix + 1) else route
case x @ TypeHandler(_, f)
val rejs = rejections collect x
if (rejs.isEmpty) rec(ix + 1) else Some(f(rejs))
}
} else None
rec(0)
} else notFound
}
/**
* Creates a new default [[RejectionHandler]] instance.
*/
def default(implicit ec: ExecutionContext) =
newBuilder()
.handleAll[SchemeRejection] { rejections
val schemes = rejections.map(_.supported).mkString(", ")
complete(BadRequest, "Uri scheme not allowed, supported schemes: " + schemes)
}
.handleAll[MethodRejection] { rejections
val (methods, names) = rejections.map(r r.supported -> r.supported.name).unzip
complete(MethodNotAllowed, List(Allow(methods)), "HTTP method not allowed, supported methods: " + names.mkString(", "))
}
.handle {
case AuthorizationFailedRejection
complete(Forbidden, "The supplied authentication is not authorized to access this resource")
}
.handle {
case MalformedFormFieldRejection(name, msg, _)
complete(BadRequest, "The form field '" + name + "' was malformed:\n" + msg)
}
.handle {
case MalformedHeaderRejection(headerName, msg, _)
complete(BadRequest, s"The value of HTTP header '$headerName' was malformed:\n" + msg)
}
.handle {
case MalformedQueryParamRejection(name, msg, _)
complete(BadRequest, "The query parameter '" + name + "' was malformed:\n" + msg)
}
.handle {
case MalformedRequestContentRejection(msg, _)
complete(BadRequest, "The request content was malformed:\n" + msg)
}
.handle {
case MissingCookieRejection(cookieName)
complete(BadRequest, "Request is missing required cookie '" + cookieName + '\'')
}
.handle {
case MissingFormFieldRejection(fieldName)
complete(BadRequest, "Request is missing required form field '" + fieldName + '\'')
}
.handle {
case MissingHeaderRejection(headerName)
complete(BadRequest, "Request is missing required HTTP header '" + headerName + '\'')
}
.handle {
case MissingQueryParamRejection(paramName)
complete(NotFound, "Request is missing required query parameter '" + paramName + '\'')
}
.handle {
case RequestEntityExpectedRejection
complete(BadRequest, "Request entity expected but not supplied")
}
.handle {
case TooManyRangesRejection(_)
complete(RequestedRangeNotSatisfiable, "Request contains too many ranges.")
}
.handle {
case UnsatisfiableRangeRejection(unsatisfiableRanges, actualEntityLength)
complete(RequestedRangeNotSatisfiable, List(`Content-Range`(ContentRange.Unsatisfiable(actualEntityLength))),
unsatisfiableRanges.mkString("None of the following requested Ranges were satisfiable:\n", "\n", ""))
}
.handleAll[AuthenticationFailedRejection] { rejections
val rejectionMessage = rejections.head.cause match {
case CredentialsMissing "The resource requires authentication, which was not supplied with the request"
case CredentialsRejected "The supplied authentication is invalid"
}
// Multiple challenges per WWW-Authenticate header are allowed per spec,
// however, it seems many browsers will ignore all challenges but the first.
// Therefore, multiple WWW-Authenticate headers are rendered, instead.
//
// See https://code.google.com/p/chromium/issues/detail?id=103220
// and https://bugzilla.mozilla.org/show_bug.cgi?id=669675
val authenticateHeaders = rejections.map(r `WWW-Authenticate`(r.challenge))
complete(Unauthorized, authenticateHeaders, rejectionMessage)
}
.handleAll[UnacceptedResponseContentTypeRejection] { rejections
val supported = rejections.flatMap(_.supported)
complete(NotAcceptable, "Resource representation is only available with these Content-Types:\n" +
supported.map(_.value).mkString("\n"))
}
.handleAll[UnacceptedResponseEncodingRejection] { rejections
val supported = rejections.flatMap(_.supported)
complete(NotAcceptable, "Resource representation is only available with these Content-Encodings:\n" +
supported.map(_.value).mkString("\n"))
}
.handleAll[UnsupportedRequestContentTypeRejection] { rejections
val supported = rejections.flatMap(_.supported).mkString(" or ")
complete(UnsupportedMediaType, "The request's Content-Type is not supported. Expected:\n" + supported)
}
.handleAll[UnsupportedRequestEncodingRejection] { rejections
val supported = rejections.map(_.supported.value).mkString(" or ")
complete(BadRequest, "The request's Content-Encoding is not supported. Expected:\n" + supported)
}
.handle { case ExpectedWebsocketRequestRejection complete(BadRequest, "Expected Websocket Upgrade request") }
.handle { case ValidationRejection(msg, _) complete(BadRequest, msg) }
.handle { case x sys.error("Unhandled rejection: " + x) }
.handleNotFound { complete(NotFound, "The requested resource could not be found.") }
.result()
/**
* Filters out all TransformationRejections from the given sequence and applies them (in order) to the
* remaining rejections.
*/
def applyTransformations(rejections: immutable.Seq[Rejection]): immutable.Seq[Rejection] = {
val (transformations, rest) = rejections.partition(_.isInstanceOf[TransformationRejection])
(rest.distinct /: transformations.asInstanceOf[Seq[TransformationRejection]]) {
case (remaining, transformation) transformation.transform(remaining)
}
}
private object PrivateRejection extends Rejection
}

View file

@ -1,115 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.concurrent.{ Future, ExecutionContext }
import akka.stream.FlowMaterializer
import akka.event.LoggingAdapter
import akka.http.marshalling.ToResponseMarshallable
import akka.http.model._
/**
* Immutable object encapsulating the context of an [[akka.http.model.HttpRequest]]
* as it flows through a akka-http Route structure.
*/
trait RequestContext {
/** The request this context represents. Modelled as a ``val`` so as to enable an ``import ctx.request._``. */
val request: HttpRequest
/** The unmatched path of this context. Modelled as a ``val`` so as to enable an ``import ctx.unmatchedPath._``. */
val unmatchedPath: Uri.Path
/**
* The default ExecutionContext to be used for scheduling asynchronous logic related to this request.
*/
implicit def executionContext: ExecutionContext
/**
* The default FlowMaterializer.
*/
implicit def flowMaterializer: FlowMaterializer
/**
* The default LoggingAdapter to be used for logging messages related to this request.
*/
def log: LoggingAdapter
/**
* The default RoutingSettings to be used for configuring directives.
*/
def settings: RoutingSettings
/**
* Returns a copy of this context with the given fields updated.
*/
def reconfigure(
executionContext: ExecutionContext = executionContext,
flowMaterializer: FlowMaterializer = flowMaterializer,
log: LoggingAdapter = log,
settings: RoutingSettings = settings): RequestContext
/**
* Completes the request with the given ToResponseMarshallable.
*/
def complete(obj: ToResponseMarshallable): Future[RouteResult]
/**
* Rejects the request with the given rejections.
*/
def reject(rejections: Rejection*): Future[RouteResult]
/**
* Bubbles the given error up the response chain where it is dealt with by the closest `handleExceptions`
* directive and its ``ExceptionHandler``, unless the error is a ``RejectionError``. In this case the
* wrapped rejection is unpacked and "executed".
*/
def fail(error: Throwable): Future[RouteResult]
/**
* Returns a copy of this context with the new HttpRequest.
*/
def withRequest(req: HttpRequest): RequestContext
/**
* Returns a copy of this context with the new HttpRequest.
*/
def withExecutionContext(ec: ExecutionContext): RequestContext
/**
* Returns a copy of this context with the new HttpRequest.
*/
def withFlowMaterializer(materializer: FlowMaterializer): RequestContext
/**
* Returns a copy of this context with the new LoggingAdapter.
*/
def withLog(log: LoggingAdapter): RequestContext
/**
* Returns a copy of this context with the new RoutingSettings.
*/
def withSettings(settings: RoutingSettings): RequestContext
/**
* Returns a copy of this context with the HttpRequest transformed by the given function.
*/
def mapRequest(f: HttpRequest HttpRequest): RequestContext
/**
* Returns a copy of this context with the unmatched path updated to the given one.
*/
def withUnmatchedPath(path: Uri.Path): RequestContext
/**
* Returns a copy of this context with the unmatchedPath transformed by the given function.
*/
def mapUnmatchedPath(f: Uri.Path Uri.Path): RequestContext
/**
* Removes a potentially existing Accept header from the request headers.
*/
def withAcceptAll: RequestContext
}

View file

@ -1,91 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.concurrent.{ Future, ExecutionContext }
import akka.stream.FlowMaterializer
import akka.event.LoggingAdapter
import akka.http.marshalling.{ Marshal, ToResponseMarshallable }
import akka.http.util.FastFuture
import akka.http.model._
import FastFuture._
/**
* INTERNAL API
*/
private[http] class RequestContextImpl(
val request: HttpRequest,
val unmatchedPath: Uri.Path,
val executionContext: ExecutionContext,
val flowMaterializer: FlowMaterializer,
val log: LoggingAdapter,
val settings: RoutingSettings) extends RequestContext {
def this(request: HttpRequest, log: LoggingAdapter, settings: RoutingSettings)(implicit ec: ExecutionContext, materializer: FlowMaterializer) =
this(request, request.uri.path, ec, materializer, log, settings)
def reconfigure(executionContext: ExecutionContext, flowMaterializer: FlowMaterializer, log: LoggingAdapter, settings: RoutingSettings): RequestContext =
copy(executionContext = executionContext, flowMaterializer = flowMaterializer, log = log, settings = settings)
override def complete(trm: ToResponseMarshallable): Future[RouteResult] =
trm(request)(executionContext)
.fast.map(res RouteResult.Complete(res))(executionContext)
.fast.recover {
case Marshal.UnacceptableResponseContentTypeException(supported)
RouteResult.Rejected(UnacceptedResponseContentTypeRejection(supported) :: Nil)
case RejectionError(rej) RouteResult.Rejected(rej :: Nil)
}(executionContext)
override def reject(rejections: Rejection*): Future[RouteResult] =
FastFuture.successful(RouteResult.Rejected(rejections.toList))
override def fail(error: Throwable): Future[RouteResult] =
FastFuture.failed(error)
override def withRequest(request: HttpRequest): RequestContext =
if (request != this.request) copy(request = request) else this
override def withExecutionContext(executionContext: ExecutionContext): RequestContext =
if (executionContext != this.executionContext) copy(executionContext = executionContext) else this
override def withFlowMaterializer(flowMaterializer: FlowMaterializer): RequestContext =
if (flowMaterializer != this.flowMaterializer) copy(flowMaterializer = flowMaterializer) else this
override def withLog(log: LoggingAdapter): RequestContext =
if (log != this.log) copy(log = log) else this
override def withSettings(settings: RoutingSettings): RequestContext =
if (settings != this.settings) copy(settings = settings) else this
override def mapRequest(f: HttpRequest HttpRequest): RequestContext =
copy(request = f(request))
override def withUnmatchedPath(path: Uri.Path): RequestContext =
if (path != unmatchedPath) copy(unmatchedPath = path) else this
override def mapUnmatchedPath(f: Uri.Path Uri.Path): RequestContext =
copy(unmatchedPath = f(unmatchedPath))
override def withAcceptAll: RequestContext = request.header[headers.Accept] match {
case Some(accept @ headers.Accept(ranges)) if !accept.acceptsAll
mapRequest(_.mapHeaders(_.map {
case `accept`
val acceptAll =
if (ranges.exists(_.isWildcard)) ranges.map(r if (r.isWildcard) MediaRanges.`*/*;q=MIN` else r)
else ranges :+ MediaRanges.`*/*;q=MIN`
accept.copy(mediaRanges = acceptAll)
case x x
}))
case _ this
}
private def copy(request: HttpRequest = request,
unmatchedPath: Uri.Path = unmatchedPath,
executionContext: ExecutionContext = executionContext,
flowMaterializer: FlowMaterializer = flowMaterializer,
log: LoggingAdapter = log,
settings: RoutingSettings = settings) =
new RequestContextImpl(request, unmatchedPath, executionContext, flowMaterializer, log, settings)
}

View file

@ -1,50 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.concurrent.Future
import akka.stream.scaladsl.Flow
import akka.http.model.{ HttpRequest, HttpResponse }
import akka.http.util.FastFuture._
object Route {
/**
* Helper for constructing a Route from a function literal.
*/
def apply(f: Route): Route = f
/**
* "Seals" a route by wrapping it with exception handling and rejection conversion.
*/
def seal(route: Route)(implicit setup: RoutingSetup): Route = {
import directives.ExecutionDirectives._
import setup._
handleExceptions(exceptionHandler.seal(settings)) {
handleRejections(rejectionHandler.seal) {
route
}
}
}
/**
* Turns a `Route` into an server flow.
*/
def handlerFlow(route: Route)(implicit setup: RoutingSetup): Flow[HttpRequest, HttpResponse, Unit] =
Flow[HttpRequest].mapAsync(1, asyncHandler(route))
/**
* Turns a `Route` into an async handler function.
*/
def asyncHandler(route: Route)(implicit setup: RoutingSetup): HttpRequest Future[HttpResponse] = {
import setup._
val sealedRoute = seal(route)
request
sealedRoute(new RequestContextImpl(request, routingLog.requestLog(request), setup.settings)).fast.map {
case RouteResult.Complete(response) response
case RouteResult.Rejected(rejected) throw new IllegalStateException(s"Unhandled rejections '$rejected', unsealed RejectionHandler?!")
}
}
}

View file

@ -1,34 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import akka.http.util.FastFuture
import FastFuture._
trait RouteConcatenation {
implicit def enhanceRouteWithConcatenation(route: Route) = new RouteConcatenation(route: Route)
class RouteConcatenation(route: Route) {
/**
* Returns a Route that chains two Routes. If the first Route rejects the request the second route is given a
* chance to act upon the request.
*/
def ~(other: Route): Route = { ctx
import ctx.executionContext
route(ctx).fast.flatMap {
case x: RouteResult.Complete FastFuture.successful(x)
case RouteResult.Rejected(outerRejections)
other(ctx).fast.map {
case x: RouteResult.Complete x
case RouteResult.Rejected(innerRejections) RouteResult.Rejected(outerRejections ++ innerRejections)
}
}
}
}
}
object RouteConcatenation extends RouteConcatenation

View file

@ -1,25 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import scala.collection.immutable
import akka.stream.scaladsl.Flow
import akka.http.model.{ HttpRequest, HttpResponse }
/**
* The result of handling a request.
*
* As a user you cannot create RouteResult instances directly.
* Instead, use the RequestContext to achieve the desired effect.
*/
sealed trait RouteResult
object RouteResult {
final case class Complete(response: HttpResponse) extends RouteResult
final case class Rejected(rejections: immutable.Seq[Rejection]) extends RouteResult
implicit def route2HandlerFlow(route: Route)(implicit setup: RoutingSetup): Flow[HttpRequest, HttpResponse, Unit] =
Route.handlerFlow(route)
}

View file

@ -1,32 +0,0 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.server
import com.typesafe.config.Config
import akka.actor.ActorRefFactory
import akka.http.util._
case class RoutingSettings(
verboseErrorMessages: Boolean,
fileGetConditional: Boolean,
renderVanityFooter: Boolean,
rangeCountLimit: Int,
rangeCoalescingThreshold: Long,
decodeMaxBytesPerChunk: Int,
fileIODispatcher: String)
object RoutingSettings extends SettingsCompanion[RoutingSettings]("akka.http.routing") {
def fromSubConfig(c: Config) = apply(
c getBoolean "verbose-error-messages",
c getBoolean "file-get-conditional",
c getBoolean "render-vanity-footer",
c getInt "range-count-limit",
c getBytes "range-coalescing-threshold",
c getIntBytes "decode-max-bytes-per-chunk",
c getString "file-io-dispatcher")
implicit def default(implicit refFactory: ActorRefFactory) =
apply(actorSystem)
}

Some files were not shown because too many files have changed in this diff Show more