+hco various smaller cleanups and helper additions
This commit is contained in:
parent
2ccf028a94
commit
5fce301c28
6 changed files with 48 additions and 204 deletions
|
|
@ -61,6 +61,11 @@ sealed trait HttpEntity extends japi.HttpEntity {
|
|||
})
|
||||
.toFuture(materializer)
|
||||
|
||||
/**
|
||||
* Creates a copy of this HttpEntity with the `contentType` overridden with the given one.
|
||||
*/
|
||||
def withContentType(contentType: ContentType): HttpEntity
|
||||
|
||||
/** Java API */
|
||||
def getDataBytes(materializer: FlowMaterializer): Publisher[ByteString] = dataBytes(materializer)
|
||||
|
||||
|
|
@ -102,6 +107,7 @@ object HttpEntity {
|
|||
* Close-delimited entities are not `Regular` as they exists primarily for backwards compatibility with HTTP/1.0.
|
||||
*/
|
||||
sealed trait Regular extends japi.HttpEntityRegular with HttpEntity {
|
||||
def withContentType(contentType: ContentType): HttpEntity.Regular
|
||||
override def isRegular: Boolean = true
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +126,8 @@ object HttpEntity {
|
|||
|
||||
override def toStrict(timeout: FiniteDuration, materializer: FlowMaterializer)(implicit ec: ExecutionContext): Future[Strict] =
|
||||
Future.successful(this)
|
||||
|
||||
def withContentType(contentType: ContentType): Strict = copy(contentType = contentType)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,6 +141,8 @@ object HttpEntity {
|
|||
override def isDefault: Boolean = true
|
||||
|
||||
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data
|
||||
|
||||
def withContentType(contentType: ContentType): Default = copy(contentType = contentType)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -145,6 +155,8 @@ object HttpEntity {
|
|||
override def isCloseDelimited: Boolean = true
|
||||
|
||||
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data
|
||||
|
||||
def withContentType(contentType: ContentType): CloseDelimited = copy(contentType = contentType)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,6 +169,8 @@ object HttpEntity {
|
|||
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] =
|
||||
Flow(chunks).map(_.data).filter(_.nonEmpty).toPublisher(materializer)
|
||||
|
||||
def withContentType(contentType: ContentType): Chunked = copy(contentType = contentType)
|
||||
|
||||
/** Java API */
|
||||
def getChunks: Publisher[japi.ChunkStreamPart] = chunks.asInstanceOf[Publisher[japi.ChunkStreamPart]]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,40 +221,6 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET,
|
|||
case x ⇒ x collectFirst { case r if r matches encoding ⇒ r.qValue } getOrElse 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether one of the given content-types is accepted by the client.
|
||||
* If a given ContentType does not define a charset an accepted charset is selected, i.e. the method guarantees
|
||||
* that, if a ContentType instance is returned within the option, it will contain a defined charset.
|
||||
*/
|
||||
def acceptableContentType(contentTypes: IndexedSeq[ContentType]): Option[ContentType] = {
|
||||
val mediaRanges = acceptedMediaRanges // cache for performance
|
||||
val charsetRanges = acceptedCharsetRanges // cache for performance
|
||||
|
||||
@tailrec def findBest(ix: Int = 0, result: ContentType = null, maxQ: Float = 0f): Option[ContentType] =
|
||||
if (ix < contentTypes.size) {
|
||||
val ct = contentTypes(ix)
|
||||
val q = qValueForMediaType(ct.mediaType, mediaRanges)
|
||||
if (q > maxQ && (ct.noCharsetDefined || isCharsetAccepted(ct.charset, charsetRanges))) findBest(ix + 1, ct, q)
|
||||
else findBest(ix + 1, result, maxQ)
|
||||
} else Option(result)
|
||||
|
||||
findBest() flatMap { ct ⇒
|
||||
def withCharset(cs: HttpCharset) = Some(ContentType(ct.mediaType, cs))
|
||||
|
||||
// logic for choosing the charset adapted from http://tools.ietf.org/html/rfc7231#section-5.3.3
|
||||
if (ct.isCharsetDefined) Some(ct) // if there is already an acceptable charset chosen we are done
|
||||
else if (qValueForCharset(`UTF-8`, charsetRanges) == 1f) withCharset(`UTF-8`) // prefer UTF-8 if fully accepted
|
||||
else charsetRanges match { // ranges are sorted by descending q-value,
|
||||
case (HttpCharsetRange.One(cs, qValue)) :: _ ⇒ // so we only need to look at the first one
|
||||
if (qValue == 1f) withCharset(cs) // if the client has high preference for this charset, pick it
|
||||
else if (qValueForCharset(`ISO-8859-1`, charsetRanges) == 1f) withCharset(`ISO-8859-1`) // give some more preference to `ISO-8859-1`
|
||||
else if (qValue > 0f) withCharset(cs) // ok, simply choose the first one if the client doesn't reject it
|
||||
else None
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this request can be safely retried, which is the case only of the request method is idempotent.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ package akka.http.rendering
|
|||
import java.net.InetSocketAddress
|
||||
import org.reactivestreams.Publisher
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.util.ByteString
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
|
@ -30,7 +29,7 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
|
|||
|
||||
final class HttpRequestRenderer extends Transformer[RequestRenderingContext, Publisher[ByteString]] {
|
||||
|
||||
def onNext(ctx: RequestRenderingContext): immutable.Seq[Publisher[ByteString]] = {
|
||||
def onNext(ctx: RequestRenderingContext): List[Publisher[ByteString]] = {
|
||||
val r = new ByteStringRendering(requestHeaderSizeHint)
|
||||
import ctx.request._
|
||||
|
||||
|
|
@ -74,11 +73,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
|
|||
case x: `Raw-Request-URI` ⇒ // we never render this header
|
||||
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
|
||||
|
||||
case x: RawHeader if x.lowercaseName == "content-type" ||
|
||||
x.lowercaseName == "content-length" ||
|
||||
x.lowercaseName == "transfer-encoding" ||
|
||||
x.lowercaseName == "host" ||
|
||||
x.lowercaseName == "user-agent" ⇒
|
||||
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
|
||||
(x is "host") || (x is "user-agent") ⇒
|
||||
suppressionWarning(log, x, "illegal RawHeader")
|
||||
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
|
||||
|
||||
|
|
@ -97,30 +93,31 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
|
|||
r ~~ CrLf
|
||||
}
|
||||
|
||||
def completeRequestRendering(): immutable.Seq[Publisher[ByteString]] =
|
||||
def completeRequestRendering(): List[Publisher[ByteString]] =
|
||||
entity match {
|
||||
case HttpEntity.Strict(contentType, data) ⇒
|
||||
case x if x.isKnownEmpty ⇒
|
||||
renderContentLength(0)
|
||||
SynchronousPublisherFromIterable(r.get :: Nil) :: Nil
|
||||
|
||||
case HttpEntity.Strict(_, data) ⇒
|
||||
renderContentLength(data.length)
|
||||
SynchronousPublisherFromIterable(r.get :: data :: Nil) :: Nil
|
||||
|
||||
case HttpEntity.Default(contentType, contentLength, data) ⇒
|
||||
case HttpEntity.Default(_, contentLength, data) ⇒
|
||||
renderContentLength(contentLength)
|
||||
renderByteStrings(r,
|
||||
Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer),
|
||||
materializer)
|
||||
|
||||
case HttpEntity.Chunked(contentType, chunks) ⇒
|
||||
r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf ~~ CrLf
|
||||
case HttpEntity.Chunked(_, chunks) ⇒
|
||||
r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf ~~ CrLf
|
||||
renderByteStrings(r, Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer), materializer)
|
||||
}
|
||||
|
||||
renderRequestLine()
|
||||
renderHeaders(headers.toList)
|
||||
renderEntityContentType(r, entity)
|
||||
if (entity.isKnownEmpty) {
|
||||
renderContentLength(0)
|
||||
SynchronousPublisherFromIterable(r.get :: Nil) :: Nil
|
||||
} else completeRequestRendering()
|
||||
completeRequestRendering()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package akka.http.rendering
|
|||
|
||||
import org.reactivestreams.Publisher
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.util.ByteString
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
|
@ -59,14 +58,14 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
|||
|
||||
override def isComplete = close
|
||||
|
||||
def onNext(ctx: ResponseRenderingContext): immutable.Seq[Publisher[ByteString]] = {
|
||||
def onNext(ctx: ResponseRenderingContext): List[Publisher[ByteString]] = {
|
||||
val r = new ByteStringRendering(responseHeaderSizeHint)
|
||||
|
||||
import ctx.response._
|
||||
val noEntity = entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD
|
||||
|
||||
def renderStatusLine(): Unit =
|
||||
if (status eq StatusCodes.OK) r ~~ DefaultStatusLine else r ~~ StatusLineStart ~~ status ~~ CrLf
|
||||
if (status eq StatusCodes.OK) r ~~ DefaultStatusLineBytes else r ~~ StatusLineStartBytes ~~ status ~~ CrLf
|
||||
|
||||
def render(h: HttpHeader) = r ~~ h ~~ CrLf
|
||||
|
||||
|
|
@ -94,12 +93,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
|||
render(x)
|
||||
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true)
|
||||
|
||||
case x: RawHeader if x.lowercaseName == "content-type" ||
|
||||
x.lowercaseName == "content-length" ||
|
||||
x.lowercaseName == "transfer-encoding" ||
|
||||
x.lowercaseName == "date" ||
|
||||
x.lowercaseName == "server" ||
|
||||
x.lowercaseName == "connection" ⇒
|
||||
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
|
||||
(x is "date") || (x is "server") || (x is "connection") ⇒
|
||||
suppressionWarning(log, x, "illegal RawHeader")
|
||||
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen)
|
||||
|
||||
|
|
@ -115,31 +110,31 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
|||
ctx.closeAfterResponseCompletion || // request wants to close
|
||||
(connHeader != null && connHeader.hasClose) // application wants to close
|
||||
ctx.requestProtocol match {
|
||||
case `HTTP/1.0` if !close ⇒ r ~~ Connection ~~ KeepAlive ~~ CrLf
|
||||
case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ Close ~~ CrLf
|
||||
case `HTTP/1.0` if !close ⇒ r ~~ Connection ~~ KeepAliveBytes ~~ CrLf
|
||||
case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ CloseBytes ~~ CrLf
|
||||
case _ ⇒ // no need for rendering
|
||||
}
|
||||
}
|
||||
|
||||
def byteStrings(entityBytes: ⇒ Publisher[ByteString]): immutable.Seq[Publisher[ByteString]] =
|
||||
def byteStrings(entityBytes: ⇒ Publisher[ByteString]): List[Publisher[ByteString]] =
|
||||
renderByteStrings(r, entityBytes, materializer, skipEntity = noEntity)
|
||||
|
||||
def completeResponseRendering(entity: HttpEntity): immutable.Seq[Publisher[ByteString]] =
|
||||
def completeResponseRendering(entity: HttpEntity): List[Publisher[ByteString]] =
|
||||
entity match {
|
||||
case HttpEntity.Strict(contentType, data) ⇒
|
||||
case HttpEntity.Strict(_, data) ⇒
|
||||
renderHeaders(headers.toList)
|
||||
renderEntityContentType(r, entity)
|
||||
r ~~ `Content-Length` ~~ data.length ~~ CrLf ~~ CrLf
|
||||
val entityBytes = if (noEntity) Nil else data :: Nil
|
||||
SynchronousPublisherFromIterable(r.get :: entityBytes) :: Nil
|
||||
|
||||
case HttpEntity.Default(contentType, contentLength, data) ⇒
|
||||
case HttpEntity.Default(_, contentLength, data) ⇒
|
||||
renderHeaders(headers.toList)
|
||||
renderEntityContentType(r, entity)
|
||||
r ~~ `Content-Length` ~~ contentLength ~~ CrLf ~~ CrLf
|
||||
byteStrings(Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer))
|
||||
|
||||
case HttpEntity.CloseDelimited(contentType, data) ⇒
|
||||
case HttpEntity.CloseDelimited(_, data) ⇒
|
||||
renderHeaders(headers.toList, alwaysClose = true)
|
||||
renderEntityContentType(r, entity)
|
||||
r ~~ CrLf
|
||||
|
|
@ -152,7 +147,7 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
|||
renderHeaders(headers.toList)
|
||||
renderEntityContentType(r, entity)
|
||||
if (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD)
|
||||
r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf
|
||||
r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf
|
||||
r ~~ CrLf
|
||||
byteStrings(Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
package akka.http.rendering
|
||||
|
||||
import org.reactivestreams.Publisher
|
||||
import scala.collection.immutable
|
||||
import akka.parboiled2.CharUtils
|
||||
import akka.util.ByteString
|
||||
import akka.event.LoggingAdapter
|
||||
|
|
@ -19,11 +18,11 @@ import akka.http.util._
|
|||
* INTERNAL API
|
||||
*/
|
||||
private object RenderSupport {
|
||||
val DefaultStatusLine = "HTTP/1.1 200 OK\r\n".getAsciiBytes
|
||||
val StatusLineStart = "HTTP/1.1 ".getAsciiBytes
|
||||
val Chunked = "chunked".getAsciiBytes
|
||||
val KeepAlive = "Keep-Alive".getAsciiBytes
|
||||
val Close = "close".getAsciiBytes
|
||||
val DefaultStatusLineBytes = "HTTP/1.1 200 OK\r\n".getAsciiBytes
|
||||
val StatusLineStartBytes = "HTTP/1.1 ".getAsciiBytes
|
||||
val ChunkedBytes = "chunked".getAsciiBytes
|
||||
val KeepAliveBytes = "Keep-Alive".getAsciiBytes
|
||||
val CloseBytes = "close".getAsciiBytes
|
||||
|
||||
def CrLf = Rendering.CrLf
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ private object RenderSupport {
|
|||
r ~~ headers.`Content-Type` ~~ entity.contentType ~~ CrLf
|
||||
|
||||
def renderByteStrings(r: ByteStringRendering, entityBytes: ⇒ Publisher[ByteString], materializer: FlowMaterializer,
|
||||
skipEntity: Boolean = false): immutable.Seq[Publisher[ByteString]] = {
|
||||
skipEntity: Boolean = false): List[Publisher[ByteString]] = {
|
||||
val messageStart = SynchronousPublisherFromIterable(r.get :: Nil)
|
||||
val messageBytes =
|
||||
if (!skipEntity) Flow(messageStart).concat(entityBytes).toPublisher(materializer)
|
||||
|
|
@ -46,7 +45,7 @@ private object RenderSupport {
|
|||
|
||||
class ChunkTransformer extends Transformer[HttpEntity.ChunkStreamPart, ByteString] {
|
||||
var lastChunkSeen = false
|
||||
def onNext(chunk: HttpEntity.ChunkStreamPart): immutable.Seq[ByteString] = {
|
||||
def onNext(chunk: HttpEntity.ChunkStreamPart): List[ByteString] = {
|
||||
if (chunk.isLastChunk) lastChunkSeen = true
|
||||
renderChunk(chunk) :: Nil
|
||||
}
|
||||
|
|
@ -56,14 +55,14 @@ private object RenderSupport {
|
|||
|
||||
class CheckContentLengthTransformer(length: Long) extends Transformer[ByteString, ByteString] {
|
||||
var sent = 0L
|
||||
def onNext(elem: ByteString): immutable.Seq[ByteString] = {
|
||||
def onNext(elem: ByteString): List[ByteString] = {
|
||||
sent += elem.length
|
||||
if (sent > length)
|
||||
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to more bytes")
|
||||
elem :: Nil
|
||||
}
|
||||
|
||||
override def onTermination(e: Option[Throwable]): immutable.Seq[ByteString] = {
|
||||
override def onTermination(e: Option[Throwable]): List[ByteString] = {
|
||||
if (sent < length)
|
||||
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to ${length - sent} bytes less")
|
||||
Nil
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.model
|
||||
|
||||
import org.scalatest.{ Matchers, FreeSpec }
|
||||
import akka.http.model.parser.HeaderParser
|
||||
import headers._
|
||||
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(':')
|
||||
HeaderParser.parseHeader(RawHeader(name.trim, value.trim)) match {
|
||||
case Right(header) ⇒ header
|
||||
case Left(err) ⇒ fail(err.formatPretty)
|
||||
}
|
||||
}
|
||||
} else Nil
|
||||
val request = HttpRequest(headers = headers)
|
||||
body(contentTypes ⇒ request.acceptableContentType(Vector(contentTypes: _*)))
|
||||
}
|
||||
|
||||
def reject = equal(None)
|
||||
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset)))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue