=ht* #17279 rename akka-http-* modules where agreed
This commit is contained in:
parent
20530be054
commit
5859c39f8b
140 changed files with 330 additions and 11018 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(_))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 ⇒ }
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 +: _)
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
23
akka-http-tests-java8/src/main/resources/web/calculator.html
Normal file
23
akka-http-tests-java8/src/main/resources/web/calculator.html
Normal 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>
|
||||
12
akka-http-tests-java8/src/test/java/AllJavaTests.java
Normal file
12
akka-http-tests-java8/src/test/java/AllJavaTests.java
Normal 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 {
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<p>Lorem ipsum!</p>
|
||||
|
|
@ -1 +0,0 @@
|
|||
XyZ
|
||||
|
|
@ -1 +0,0 @@
|
|||
123
|
||||
|
|
@ -1 +0,0 @@
|
|||
123
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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>Ha“llo</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: _*))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 '<username>-password' as credentials</li>
|
||||
<li><a href="/crash">/crash</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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!!!"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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`)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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#])
|
||||
}
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -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#])
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -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}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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  = 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])
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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?!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue