Merge pull request #15391 from spray/w/small-http-core-changes
+hco Small akka-http-core changes as a preparation for the Java API
This commit is contained in:
commit
0b45bb8e47
26 changed files with 232 additions and 159 deletions
|
|
@ -60,14 +60,14 @@ sealed trait HttpEntity {
|
|||
}
|
||||
|
||||
object HttpEntity {
|
||||
implicit def apply(string: String): Regular = apply(ContentTypes.`text/plain(UTF-8)`, string)
|
||||
implicit def apply(bytes: Array[Byte]): Regular = apply(ContentTypes.`application/octet-stream`, bytes)
|
||||
implicit def apply(data: ByteString): Regular = apply(ContentTypes.`application/octet-stream`, data)
|
||||
def apply(contentType: ContentType, string: String): Regular =
|
||||
implicit def apply(string: String): Strict = apply(ContentTypes.`text/plain(UTF-8)`, string)
|
||||
implicit def apply(bytes: Array[Byte]): Strict = apply(ContentTypes.`application/octet-stream`, bytes)
|
||||
implicit def apply(data: ByteString): Strict = apply(ContentTypes.`application/octet-stream`, data)
|
||||
def apply(contentType: ContentType, string: String): Strict =
|
||||
if (string.isEmpty) empty(contentType) else apply(contentType, ByteString(string.getBytes(contentType.charset.nioCharset)))
|
||||
def apply(contentType: ContentType, bytes: Array[Byte]): Regular =
|
||||
def apply(contentType: ContentType, bytes: Array[Byte]): Strict =
|
||||
if (bytes.length == 0) empty(contentType) else apply(contentType, ByteString(bytes))
|
||||
def apply(contentType: ContentType, data: ByteString): Regular =
|
||||
def apply(contentType: ContentType, data: ByteString): Strict =
|
||||
if (data.isEmpty) empty(contentType) else Strict(contentType, data)
|
||||
def apply(contentType: ContentType, contentLength: Long, data: Producer[ByteString]): Regular =
|
||||
if (contentLength == 0) empty(contentType) else Default(contentType, contentLength, data)
|
||||
|
|
@ -78,7 +78,7 @@ object HttpEntity {
|
|||
else empty(contentType)
|
||||
}
|
||||
|
||||
val Empty = Strict(ContentTypes.NoContentType, data = ByteString.empty)
|
||||
val Empty: Strict = Strict(ContentTypes.NoContentType, data = ByteString.empty)
|
||||
|
||||
def empty(contentType: ContentType): Strict =
|
||||
if (contentType == Empty.contentType) Empty
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ object MultipartFormData {
|
|||
|
||||
def apply(fields: Map[String, BodyPart]): MultipartFormData = apply {
|
||||
fields.map {
|
||||
case (key, value) ⇒ value.copy(headers = `Content-Disposition`(ContentDispositionType.`form-data`, Map("name" -> key)) +: value.headers)
|
||||
case (key, value) ⇒ value.copy(headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> key)) +: value.headers)
|
||||
}(collection.breakOut): _*
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,8 +282,4 @@ final case class HttpResponse(status: StatusCode = StatusCodes.OK,
|
|||
|
||||
def withHeadersAndEntity(headers: immutable.Seq[HttpHeader], entity: HttpEntity) =
|
||||
if ((headers eq this.headers) && (entity eq this.entity)) this else copy(headers = headers, entity = entity)
|
||||
}
|
||||
|
||||
object HttpResponse {
|
||||
def apply(status: StatusCode, entity: HttpEntity): HttpResponse = HttpResponse(status, Nil, entity)
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import akka.http.util._
|
|||
sealed abstract class MediaRange extends Renderable with WithQValue[MediaRange] {
|
||||
def value: String
|
||||
def mainType: String
|
||||
def parameters: Map[String, String]
|
||||
def params: Map[String, String]
|
||||
def qValue: Float
|
||||
def matches(mediaType: MediaType): Boolean
|
||||
def isApplication = false
|
||||
|
|
@ -23,10 +23,10 @@ sealed abstract class MediaRange extends Renderable with WithQValue[MediaRange]
|
|||
def isVideo = false
|
||||
|
||||
/**
|
||||
* Returns a copy of this instance with the parameters replaced by the given ones.
|
||||
* Returns a copy of this instance with the params replaced by the given ones.
|
||||
* If the given map contains a "q" value the `qValue` member is (also) updated.
|
||||
*/
|
||||
def withParameters(parameters: Map[String, String]): MediaRange
|
||||
def withParams(params: Map[String, String]): MediaRange
|
||||
|
||||
/**
|
||||
* Constructs a `ContentTypeRange` from this instance and the given charset.
|
||||
|
|
@ -35,22 +35,22 @@ sealed abstract class MediaRange extends Renderable with WithQValue[MediaRange]
|
|||
}
|
||||
|
||||
object MediaRange {
|
||||
private[http] def splitOffQValue(parameters: Map[String, String], defaultQ: Float = 1.0f): (Map[String, String], Float) =
|
||||
parameters.get("q") match {
|
||||
case Some(x) ⇒ (parameters - "q") -> (try x.toFloat catch { case _: NumberFormatException ⇒ 1.0f })
|
||||
case None ⇒ parameters -> defaultQ
|
||||
private[http] def splitOffQValue(params: Map[String, String], defaultQ: Float = 1.0f): (Map[String, String], Float) =
|
||||
params.get("q") match {
|
||||
case Some(x) ⇒ (params - "q") -> (try x.toFloat catch { case _: NumberFormatException ⇒ 1.0f })
|
||||
case None ⇒ params -> defaultQ
|
||||
}
|
||||
|
||||
private final case class Custom(mainType: String, parameters: Map[String, String], qValue: Float)
|
||||
private final case class Custom(mainType: String, params: Map[String, String], qValue: Float)
|
||||
extends MediaRange with ValueRenderable {
|
||||
require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0")
|
||||
def matches(mediaType: MediaType) = mainType == "*" || mediaType.mainType == mainType
|
||||
def withParameters(parameters: Map[String, String]) = custom(mainType, parameters, qValue)
|
||||
def withQValue(qValue: Float) = if (qValue != this.qValue) custom(mainType, parameters, qValue) else this
|
||||
def withParams(params: Map[String, String]) = custom(mainType, params, qValue)
|
||||
def withQValue(qValue: Float) = if (qValue != this.qValue) custom(mainType, params, qValue) else this
|
||||
def render[R <: Rendering](r: R): r.type = {
|
||||
r ~~ mainType ~~ '/' ~~ '*'
|
||||
if (qValue < 1.0f) r ~~ ";q=" ~~ qValue
|
||||
if (parameters.nonEmpty) parameters foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v }
|
||||
if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v }
|
||||
r
|
||||
}
|
||||
override def isApplication = mainType == "application"
|
||||
|
|
@ -62,15 +62,15 @@ object MediaRange {
|
|||
override def isVideo = mainType == "video"
|
||||
}
|
||||
|
||||
def custom(mainType: String, parameters: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = {
|
||||
val (params, q) = splitOffQValue(parameters, qValue)
|
||||
Custom(mainType.toLowerCase, params, q)
|
||||
def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = {
|
||||
val (ps, q) = splitOffQValue(params, qValue)
|
||||
Custom(mainType.toLowerCase, ps, q)
|
||||
}
|
||||
|
||||
final case class One(mediaType: MediaType, qValue: Float) extends MediaRange with ValueRenderable {
|
||||
require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0")
|
||||
def mainType = mediaType.mainType
|
||||
def parameters = mediaType.parameters
|
||||
def params = mediaType.params
|
||||
override def isApplication = mediaType.isApplication
|
||||
override def isAudio = mediaType.isAudio
|
||||
override def isImage = mediaType.isImage
|
||||
|
|
@ -80,7 +80,7 @@ object MediaRange {
|
|||
override def isVideo = mediaType.isVideo
|
||||
def matches(mediaType: MediaType) =
|
||||
this.mediaType.mainType == mediaType.mainType && this.mediaType.subType == mediaType.subType
|
||||
def withParameters(parameters: Map[String, String]) = copy(mediaType = mediaType.withParameters(parameters))
|
||||
def withParams(params: Map[String, String]) = copy(mediaType = mediaType.withParams(params))
|
||||
def withQValue(qValue: Float) = copy(qValue = qValue)
|
||||
def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ mediaType ~~ ";q=" ~~ qValue else r ~~ mediaType
|
||||
}
|
||||
|
|
@ -94,10 +94,10 @@ object MediaRanges extends ObjectRegistry[String, MediaRange] {
|
|||
sealed abstract case class PredefinedMediaRange(value: String) extends MediaRange with LazyValueBytesRenderable {
|
||||
val mainType = value takeWhile (_ != '/')
|
||||
register(mainType, this)
|
||||
def parameters = Map.empty
|
||||
def params = Map.empty
|
||||
def qValue = 1.0f
|
||||
def withParameters(parameters: Map[String, String]) = MediaRange.custom(mainType, parameters)
|
||||
def withQValue(qValue: Float) = if (qValue != 1.0f) MediaRange.custom(mainType, parameters, qValue) else this
|
||||
def withParams(params: Map[String, String]) = MediaRange.custom(mainType, params)
|
||||
def withQValue(qValue: Float) = if (qValue != 1.0f) MediaRange.custom(mainType, params, qValue) else this
|
||||
}
|
||||
|
||||
val `*/*` = new PredefinedMediaRange("*/*") {
|
||||
|
|
@ -138,7 +138,7 @@ sealed abstract case class MediaType private[http] (value: String)(val mainType:
|
|||
val compressible: Boolean,
|
||||
val binary: Boolean,
|
||||
val fileExtensions: immutable.Seq[String],
|
||||
val parameters: Map[String, String])
|
||||
val params: Map[String, String])
|
||||
extends LazyValueBytesRenderable with WithQValue[MediaRange] {
|
||||
def isApplication = false
|
||||
def isAudio = false
|
||||
|
|
@ -149,9 +149,9 @@ sealed abstract case class MediaType private[http] (value: String)(val mainType:
|
|||
def isVideo = false
|
||||
|
||||
/**
|
||||
* Returns a copy of this instance with the parameters replaced by the given ones.
|
||||
* Returns a copy of this instance with the params replaced by the given ones.
|
||||
*/
|
||||
def withParameters(parameters: Map[String, String]): MediaType
|
||||
def withParams(params: Map[String, String]): MediaType
|
||||
|
||||
/**
|
||||
* Constructs a `ContentType` from this instance and the given charset.
|
||||
|
|
@ -161,24 +161,24 @@ sealed abstract case class MediaType private[http] (value: String)(val mainType:
|
|||
def withQValue(qValue: Float): MediaRange = MediaRange(this, qValue.toFloat)
|
||||
}
|
||||
|
||||
class MultipartMediaType private[http] (_value: String, _subType: String, _parameters: Map[String, String])
|
||||
extends MediaType(_value)("multipart", _subType, compressible = true, binary = true, Nil, _parameters) {
|
||||
class MultipartMediaType private[http] (_value: String, _subType: String, _params: Map[String, String])
|
||||
extends MediaType(_value)("multipart", _subType, compressible = true, binary = true, Nil, _params) {
|
||||
override def isMultipart = true
|
||||
def withBoundary(boundary: String): MultipartMediaType = withParameters {
|
||||
if (boundary.isEmpty) parameters - "boundary" else parameters.updated("boundary", boundary)
|
||||
def withBoundary(boundary: String): MultipartMediaType = withParams {
|
||||
if (boundary.isEmpty) params - "boundary" else params.updated("boundary", boundary)
|
||||
}
|
||||
def withParameters(parameters: Map[String, String]) = MediaTypes.multipart(subType, parameters)
|
||||
def withParams(params: Map[String, String]) = MediaTypes.multipart(subType, params)
|
||||
}
|
||||
|
||||
sealed abstract class NonMultipartMediaType private[http] (_value: String, _mainType: String, _subType: String,
|
||||
_compressible: Boolean, _binary: Boolean,
|
||||
_fileExtensions: immutable.Seq[String],
|
||||
_parameters: Map[String, String])
|
||||
extends MediaType(_value)(_mainType, _subType, _compressible, _binary, _fileExtensions, _parameters) {
|
||||
_params: Map[String, String])
|
||||
extends MediaType(_value)(_mainType, _subType, _compressible, _binary, _fileExtensions, _params) {
|
||||
private[http] def this(mainType: String, subType: String, compressible: Boolean, binary: Boolean, fileExtensions: immutable.Seq[String]) =
|
||||
this(mainType + '/' + subType, mainType, subType, compressible, binary, fileExtensions, Map.empty)
|
||||
def withParameters(parameters: Map[String, String]) =
|
||||
MediaType.custom(mainType, subType, compressible, binary, fileExtensions, parameters)
|
||||
def withParams(params: Map[String, String]) =
|
||||
MediaType.custom(mainType, subType, compressible, binary, fileExtensions, params)
|
||||
}
|
||||
|
||||
object MediaType {
|
||||
|
|
@ -188,13 +188,13 @@ object MediaType {
|
|||
* your custom Marshallers and Unmarshallers.
|
||||
*/
|
||||
def custom(mainType: String, subType: String, compressible: Boolean = false, binary: Boolean = false,
|
||||
fileExtensions: immutable.Seq[String] = Nil, parameters: Map[String, String] = Map.empty,
|
||||
fileExtensions: immutable.Seq[String] = Nil, params: Map[String, String] = Map.empty,
|
||||
allowArbitrarySubtypes: Boolean = false): MediaType = {
|
||||
require(mainType != "multipart", "Cannot create a MultipartMediaType here, use `multipart.apply` instead!")
|
||||
require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!")
|
||||
val r = new StringRendering ~~ mainType ~~ '/' ~~ subType
|
||||
if (parameters.nonEmpty) parameters foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v }
|
||||
new NonMultipartMediaType(r.get, mainType, subType, compressible, binary, fileExtensions, parameters) {
|
||||
if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v }
|
||||
new NonMultipartMediaType(r.get, mainType, subType, compressible, binary, fileExtensions, params) {
|
||||
override def isApplication = mainType == "application"
|
||||
override def isAudio = mainType == "audio"
|
||||
override def isImage = mainType == "image"
|
||||
|
|
@ -374,19 +374,19 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
|
|||
val `message/rfc822` = msg("rfc822", "eml", "mht", "mhtml", "mime")
|
||||
|
||||
object multipart {
|
||||
def apply(subType: String, parameters: Map[String, String]): MultipartMediaType = {
|
||||
def apply(subType: String, params: Map[String, String]): MultipartMediaType = {
|
||||
require(subType != "*", "Cannot create a MediaRange here, use MediaRanges.`multipart/*` instead!")
|
||||
val r = new StringRendering ~~ "multipart/" ~~ subType
|
||||
if (parameters.nonEmpty) parameters foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v }
|
||||
new MultipartMediaType(r.get, subType, parameters)
|
||||
if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v }
|
||||
new MultipartMediaType(r.get, subType, params)
|
||||
}
|
||||
def mixed (parameters: Map[String, String]) = apply("mixed", parameters)
|
||||
def alternative(parameters: Map[String, String]) = apply("alternative", parameters)
|
||||
def related (parameters: Map[String, String]) = apply("related", parameters)
|
||||
def `form-data`(parameters: Map[String, String]) = apply("form-data", parameters)
|
||||
def signed (parameters: Map[String, String]) = apply("signed", parameters)
|
||||
def encrypted (parameters: Map[String, String]) = apply("encrypted", parameters)
|
||||
def byteRanges (parameters: Map[String, String]) = apply("byteranges", parameters)
|
||||
def mixed (params: Map[String, String]) = apply("mixed", params)
|
||||
def alternative(params: Map[String, String]) = apply("alternative", params)
|
||||
def related (params: Map[String, String]) = apply("related", params)
|
||||
def `form-data`(params: Map[String, String]) = apply("form-data", params)
|
||||
def signed (params: Map[String, String]) = apply("signed", params)
|
||||
def encrypted (params: Map[String, String]) = apply("encrypted", params)
|
||||
def byteRanges (params: Map[String, String]) = apply("byteranges", params)
|
||||
}
|
||||
|
||||
val `multipart/mixed` = multipart.mixed(Map.empty)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ final case class BodyPart(entity: HttpEntity, headers: immutable.Seq[HttpHeader]
|
|||
|
||||
def dispositionParameterValue(parameter: String): Option[String] =
|
||||
headers.collectFirst {
|
||||
case `Content-Disposition`(ContentDispositionType.`form-data`, parameters) if parameters.contains(parameter) ⇒
|
||||
case `Content-Disposition`(ContentDispositionTypes.`form-data`, parameters) if parameters.contains(parameter) ⇒
|
||||
parameters(parameter)
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +77,6 @@ object BodyPart {
|
|||
}
|
||||
|
||||
def apply(entity: HttpEntity, fieldName: String): BodyPart = apply(entity, fieldName, Map.empty[String, String])
|
||||
def apply(entity: HttpEntity, fieldName: String, parameters: Map[String, String]): BodyPart =
|
||||
BodyPart(entity, immutable.Seq(`Content-Disposition`(ContentDispositionType.`form-data`, parameters.updated("name", fieldName))))
|
||||
def apply(entity: HttpEntity, fieldName: String, params: Map[String, String]): BodyPart =
|
||||
BodyPart(entity, immutable.Seq(`Content-Disposition`(ContentDispositionTypes.`form-data`, params.updated("name", fieldName))))
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import akka.http.model.parser.UriParser
|
|||
import akka.http.model.parser.CharacterClasses._
|
||||
import akka.http.util._
|
||||
import Uri._
|
||||
import java.net.{ Inet4Address, Inet6Address, InetAddress }
|
||||
|
||||
/**
|
||||
* An immutable model of an internet URI as defined by http://tools.ietf.org/html/rfc3986.
|
||||
|
|
@ -308,6 +309,7 @@ object Uri {
|
|||
def address: String
|
||||
def isEmpty: Boolean
|
||||
def toOption: Option[NonEmptyHost]
|
||||
def inetAddresses: immutable.Seq[InetAddress]
|
||||
def equalsIgnoreCase(other: Host): Boolean
|
||||
override def toString() = UriRendering.HostRenderer.render(new StringRendering, this).get
|
||||
}
|
||||
|
|
@ -316,50 +318,70 @@ object Uri {
|
|||
def address: String = ""
|
||||
def isEmpty = true
|
||||
def toOption = None
|
||||
def inetAddresses: immutable.Seq[InetAddress] = Nil
|
||||
|
||||
def equalsIgnoreCase(other: Host): Boolean = other eq this
|
||||
}
|
||||
def apply(string: String, charset: Charset = UTF8, mode: Uri.ParsingMode = Uri.ParsingMode.Relaxed): Host =
|
||||
if (!string.isEmpty) new UriParser(string, UTF8, mode).parseHost() else Empty
|
||||
|
||||
def apply(address: InetAddress): Host = address match {
|
||||
case ipv4: Inet4Address ⇒ apply(ipv4)
|
||||
case ipv6: Inet6Address ⇒ apply(ipv6)
|
||||
case _ ⇒ throw new IllegalArgumentException(s"Unexpected address type(${address.getClass.getSimpleName}): $address")
|
||||
}
|
||||
def apply(address: Inet4Address): IPv4Host = IPv4Host(address.getAddress, address.getHostAddress)
|
||||
def apply(address: Inet6Address): IPv6Host = IPv6Host(address.getAddress, address.getHostAddress)
|
||||
}
|
||||
sealed abstract class NonEmptyHost extends Host {
|
||||
def isEmpty = false
|
||||
def toOption = Some(this)
|
||||
}
|
||||
final case class IPv4Host(bytes: immutable.Seq[Byte], address: String) extends NonEmptyHost {
|
||||
final case class IPv4Host private[http] (bytes: immutable.Seq[Byte], address: String) extends NonEmptyHost {
|
||||
require(bytes.length == 4, "bytes array must have length 4")
|
||||
require(!address.isEmpty, "address must not be empty")
|
||||
def equalsIgnoreCase(other: Host): Boolean = other match {
|
||||
case IPv4Host(`bytes`, _) ⇒ true
|
||||
case _ ⇒ false
|
||||
}
|
||||
|
||||
def inetAddresses = immutable.Seq(InetAddress.getByAddress(bytes.toArray))
|
||||
}
|
||||
object IPv4Host {
|
||||
def apply(address: String): IPv4Host = apply(address.split('.').map(_.toInt.toByte))
|
||||
def apply(byte1: Byte, byte2: Byte, byte3: Byte, byte4: Byte): IPv4Host = apply(Array(byte1, byte2, byte3, byte4))
|
||||
def apply(bytes: Array[Byte]): IPv4Host = apply(bytes, bytes.map(_ & 0xFF).mkString("."))
|
||||
def apply(bytes: Array[Byte], address: String): IPv4Host = apply(immutable.Seq(bytes: _*), address)
|
||||
|
||||
private[http] def apply(bytes: Array[Byte], address: String): IPv4Host = IPv4Host(immutable.Seq(bytes: _*), address)
|
||||
}
|
||||
final case class IPv6Host(bytes: immutable.Seq[Byte], address: String) extends NonEmptyHost {
|
||||
final case class IPv6Host private (bytes: immutable.Seq[Byte], address: String) extends NonEmptyHost {
|
||||
require(bytes.length == 16, "bytes array must have length 16")
|
||||
require(!address.isEmpty, "address must not be empty")
|
||||
def equalsIgnoreCase(other: Host): Boolean = other match {
|
||||
case IPv6Host(`bytes`, _) ⇒ true
|
||||
case _ ⇒ false
|
||||
}
|
||||
|
||||
def inetAddresses = immutable.Seq(InetAddress.getByAddress(bytes.toArray))
|
||||
}
|
||||
object IPv6Host {
|
||||
def apply(bytes: String, address: String): IPv6Host = {
|
||||
def apply(bytes: Array[Byte]): IPv6Host = Host(InetAddress.getByAddress(bytes).asInstanceOf[Inet6Address])
|
||||
def apply(bytes: immutable.Seq[Byte]): IPv6Host = apply(bytes.toArray)
|
||||
|
||||
private[http] def apply(bytes: String, address: String): IPv6Host = {
|
||||
import CharUtils.{ hexValue ⇒ hex }
|
||||
require(bytes.length == 32, "`bytes` must be a 32 character hex string")
|
||||
apply(bytes.toCharArray.grouped(2).map(s ⇒ (hex(s(0)) * 16 + hex(s(1))).toByte).toArray, address)
|
||||
}
|
||||
def apply(bytes: Array[Byte], address: String): IPv6Host = apply(immutable.Seq(bytes: _*), address)
|
||||
private[http] def apply(bytes: Array[Byte], address: String): IPv6Host = apply(immutable.Seq(bytes: _*), address)
|
||||
}
|
||||
final case class NamedHost(address: String) extends NonEmptyHost {
|
||||
def equalsIgnoreCase(other: Host): Boolean = other match {
|
||||
case NamedHost(otherAddress) ⇒ address equalsIgnoreCase otherAddress
|
||||
case _ ⇒ false
|
||||
}
|
||||
|
||||
def inetAddresses = InetAddress.getAllByName(address).toList
|
||||
}
|
||||
|
||||
sealed abstract class Path {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import akka.http.util.{ Rendering, SingletonValueRenderable, Renderable }
|
|||
|
||||
sealed trait ContentDispositionType extends Renderable
|
||||
|
||||
object ContentDispositionType {
|
||||
case object inline extends ContentDispositionType with SingletonValueRenderable
|
||||
case object attachment extends ContentDispositionType with SingletonValueRenderable
|
||||
case object `form-data` extends ContentDispositionType with SingletonValueRenderable
|
||||
object ContentDispositionTypes {
|
||||
protected abstract class Predefined extends ContentDispositionType with SingletonValueRenderable {
|
||||
def name: String = value
|
||||
}
|
||||
|
||||
case object inline extends Predefined
|
||||
case object attachment extends Predefined
|
||||
case object `form-data` extends Predefined
|
||||
final case class Ext(name: String) extends ContentDispositionType {
|
||||
def render[R <: Rendering](r: R): r.type = r ~~ name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,23 @@ import akka.parboiled2.util.Base64
|
|||
import akka.http.model.HttpCharsets._
|
||||
import akka.http.util.{ Rendering, ValueRenderable }
|
||||
|
||||
sealed trait HttpCredentials extends ValueRenderable
|
||||
sealed trait HttpCredentials extends ValueRenderable {
|
||||
def scheme: String
|
||||
def token: String
|
||||
def params: Map[String, String]
|
||||
}
|
||||
|
||||
final case class BasicHttpCredentials(username: String, password: String) extends HttpCredentials {
|
||||
def render[R <: Rendering](r: R): r.type = {
|
||||
val cookie = {
|
||||
val userPass = username + ':' + password
|
||||
val bytes = userPass.getBytes(`ISO-8859-1`.nioCharset)
|
||||
val cookie = Base64.rfc2045.encodeToChar(bytes, false)
|
||||
r ~~ "Basic " ~~ cookie
|
||||
Base64.rfc2045.encodeToChar(bytes, false)
|
||||
}
|
||||
def render[R <: Rendering](r: R): r.type = r ~~ "Basic " ~~ cookie
|
||||
|
||||
def scheme: String = "Basic"
|
||||
def token = cookie.toString
|
||||
def params = Map.empty
|
||||
}
|
||||
|
||||
object BasicHttpCredentials {
|
||||
|
|
@ -33,6 +41,9 @@ object BasicHttpCredentials {
|
|||
|
||||
final case class OAuth2BearerToken(token: String) extends HttpCredentials {
|
||||
def render[R <: Rendering](r: R): r.type = r ~~ "Bearer " ~~ token
|
||||
|
||||
def scheme: String = "Bearer"
|
||||
def params: Map[String, String] = Map.empty
|
||||
}
|
||||
|
||||
final case class GenericHttpCredentials(scheme: String, token: String,
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ abstract class HttpOriginRange extends ValueRenderable {
|
|||
def matches(origin: HttpOrigin): Boolean
|
||||
}
|
||||
object HttpOriginRange {
|
||||
implicit val originsRenderer: Renderer[immutable.Seq[HttpOrigin]] = Renderer.seqRenderer(" ", "null")
|
||||
|
||||
case object `*` extends HttpOriginRange {
|
||||
def matches(origin: HttpOrigin) = true
|
||||
def render[R <: Rendering](r: R): r.type = r ~~ '*'
|
||||
|
|
@ -34,6 +32,8 @@ final case class HttpOrigin(scheme: String, host: Host) extends ValueRenderable
|
|||
def render[R <: Rendering](r: R): r.type = host.renderValue(r ~~ scheme ~~ "://")
|
||||
}
|
||||
object HttpOrigin {
|
||||
implicit val originsRenderer: Renderer[immutable.Seq[HttpOrigin]] = Renderer.seqRenderer(" ", "null")
|
||||
|
||||
implicit def apply(str: String): HttpOrigin = {
|
||||
val parser = new UriParser(str, UTF8, Uri.ParsingMode.Relaxed)
|
||||
parser.parseOrigin()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import akka.http.util._
|
|||
import UriRendering.UriRenderer
|
||||
|
||||
final case class LinkValue(uri: Uri, params: immutable.Seq[LinkParam]) extends ValueRenderable {
|
||||
import LinkParam.paramsRenderer
|
||||
def render[R <: Rendering](r: R): r.type = {
|
||||
r ~~ '<' ~~ uri ~~ '>'
|
||||
if (params.nonEmpty) r ~~ "; " ~~ params
|
||||
|
|
@ -24,10 +23,11 @@ object LinkValue {
|
|||
}
|
||||
|
||||
sealed abstract class LinkParam extends ToStringRenderable
|
||||
|
||||
object LinkParam {
|
||||
implicit val paramsRenderer: Renderer[immutable.Seq[LinkParam]] = Renderer.seqRenderer(separator = "; ")
|
||||
}
|
||||
|
||||
object LinkParams {
|
||||
private val reserved = CharPredicate(" ,;")
|
||||
|
||||
// A few convenience rels
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import akka.http.util.{ Rendering, ValueRenderable }
|
|||
|
||||
sealed trait RangeUnit extends ValueRenderable
|
||||
|
||||
object RangeUnit {
|
||||
object RangeUnits {
|
||||
object Bytes extends RangeUnit {
|
||||
def render[R <: Rendering](r: R): r.type = r ~~ "bytes"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,21 @@ package akka.http.model.headers
|
|||
|
||||
import akka.http.util.{ Rendering, SingletonValueRenderable, Renderable }
|
||||
|
||||
sealed trait TransferEncoding extends Renderable
|
||||
sealed trait TransferEncoding extends Renderable {
|
||||
def name: String
|
||||
def params: Map[String, String]
|
||||
}
|
||||
|
||||
object TransferEncoding {
|
||||
case object chunked extends TransferEncoding with SingletonValueRenderable
|
||||
case object compress extends TransferEncoding with SingletonValueRenderable
|
||||
case object deflate extends TransferEncoding with SingletonValueRenderable
|
||||
case object gzip extends TransferEncoding with SingletonValueRenderable
|
||||
object TransferEncodings {
|
||||
protected abstract class Predefined extends TransferEncoding with SingletonValueRenderable {
|
||||
def name: String = value
|
||||
def params: Map[String, String] = Map.empty
|
||||
}
|
||||
|
||||
case object chunked extends Predefined
|
||||
case object compress extends Predefined
|
||||
case object deflate extends Predefined
|
||||
case object gzip extends Predefined
|
||||
final case class Extension(name: String, params: Map[String, String] = Map.empty) extends TransferEncoding {
|
||||
def render[R <: Rendering](r: R): r.type = {
|
||||
r ~~ name
|
||||
|
|
|
|||
|
|
@ -215,10 +215,10 @@ final case class Connection(tokens: immutable.Seq[String]) extends ModeledHeader
|
|||
|
||||
// http://tools.ietf.org/html/rfc6266
|
||||
object `Content-Disposition` extends ModeledCompanion
|
||||
final case class `Content-Disposition`(dispositionType: ContentDispositionType, parameters: Map[String, String] = Map.empty) extends ModeledHeader {
|
||||
final case class `Content-Disposition`(dispositionType: ContentDispositionType, params: Map[String, String] = Map.empty) extends ModeledHeader {
|
||||
protected def renderValue[R <: Rendering](r: R): r.type = {
|
||||
r ~~ dispositionType
|
||||
parameters foreach { case (k, v) ⇒ r ~~ "; " ~~ k ~~ '=' ~~# v }
|
||||
params foreach { case (k, v) ⇒ r ~~ "; " ~~ k ~~ '=' ~~# v }
|
||||
r
|
||||
}
|
||||
protected def companion = `Content-Disposition`
|
||||
|
|
@ -245,7 +245,7 @@ final case class `Content-Length`(length: Long)(implicit ev: AllowProtectedHeade
|
|||
|
||||
// http://tools.ietf.org/html/rfc7233#section-4.2
|
||||
object `Content-Range` extends ModeledCompanion {
|
||||
def apply(byteContentRange: ByteContentRange): `Content-Range` = apply(RangeUnit.Bytes, byteContentRange)
|
||||
def apply(byteContentRange: ByteContentRange): `Content-Range` = apply(RangeUnits.Bytes, byteContentRange)
|
||||
}
|
||||
final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRange) extends ModeledHeader {
|
||||
protected def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ ' ' ~~ contentRange
|
||||
|
|
@ -398,7 +398,6 @@ object Origin extends ModeledCompanion {
|
|||
def apply(first: HttpOrigin, more: HttpOrigin*): Origin = apply(immutable.Seq(first +: more: _*))
|
||||
}
|
||||
final case class Origin(origins: immutable.Seq[HttpOrigin]) extends ModeledHeader {
|
||||
import HttpOriginRange.originsRenderer
|
||||
protected def renderValue[R <: Rendering](r: R): r.type = r ~~ origins
|
||||
protected def companion = Origin
|
||||
}
|
||||
|
|
@ -406,7 +405,7 @@ final case class Origin(origins: immutable.Seq[HttpOrigin]) extends ModeledHeade
|
|||
// http://tools.ietf.org/html/rfc7233#section-3.1
|
||||
object Range extends ModeledCompanion {
|
||||
def apply(first: ByteRange, more: ByteRange*): Range = apply(immutable.Seq(first +: more: _*))
|
||||
def apply(ranges: immutable.Seq[ByteRange]): Range = Range(RangeUnit.Bytes, ranges)
|
||||
def apply(ranges: immutable.Seq[ByteRange]): Range = Range(RangeUnits.Bytes, ranges)
|
||||
private[http] implicit val rangesRenderer = Renderer.defaultSeqRenderer[ByteRange] // cache
|
||||
}
|
||||
final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) extends ModeledHeader {
|
||||
|
|
@ -477,7 +476,7 @@ object `Transfer-Encoding` extends ModeledCompanion {
|
|||
final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding])(implicit ev: AllowProtectedHeaderCreation.Enabled) extends ModeledHeader {
|
||||
import `Transfer-Encoding`.encodingsRenderer
|
||||
require(encodings.nonEmpty, "encodings must not be empty")
|
||||
def hasChunked: Boolean = encodings contains TransferEncoding.chunked
|
||||
def hasChunked: Boolean = encodings contains TransferEncodings.chunked
|
||||
protected def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings
|
||||
protected def companion = `Transfer-Encoding`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonAc
|
|||
|
||||
// http://tools.ietf.org/html/rfc7231#section-5.3.2
|
||||
def accept = rule {
|
||||
zeroOrMore(`media-range-decl`).separatedBy(listSep) ~ EOI ~> (Accept(_: _*))
|
||||
|
||||
zeroOrMore(`media-range-decl`).separatedBy(listSep) ~ EOI ~> (Accept(_))
|
||||
}
|
||||
|
||||
def `media-range-decl` = rule {
|
||||
|
|
@ -21,7 +22,7 @@ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonAc
|
|||
if (sub == "*") {
|
||||
val mainLower = main.toLowerCase
|
||||
MediaRanges.getForKey(mainLower) match {
|
||||
case Some(registered) ⇒ if (params.isEmpty) registered else registered.withParameters(params.toMap)
|
||||
case Some(registered) ⇒ if (params.isEmpty) registered else registered.withParams(params.toMap)
|
||||
case None ⇒ MediaRange.custom(mainLower, params.toMap)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,21 +11,21 @@ private[parser] trait CommonActions {
|
|||
|
||||
type StringMapBuilder = scala.collection.mutable.Builder[(String, String), Map[String, String]]
|
||||
|
||||
def getMediaType(mainType: String, subType: String, parameters: Map[String, String]): MediaType = {
|
||||
def getMediaType(mainType: String, subType: String, params: Map[String, String]): MediaType = {
|
||||
mainType.toLowerCase match {
|
||||
case "multipart" ⇒ subType.toLowerCase match {
|
||||
case "mixed" ⇒ multipart.mixed(parameters)
|
||||
case "alternative" ⇒ multipart.alternative(parameters)
|
||||
case "related" ⇒ multipart.related(parameters)
|
||||
case "form-data" ⇒ multipart.`form-data`(parameters)
|
||||
case "signed" ⇒ multipart.signed(parameters)
|
||||
case "encrypted" ⇒ multipart.encrypted(parameters)
|
||||
case custom ⇒ multipart(custom, parameters)
|
||||
case "mixed" ⇒ multipart.mixed(params)
|
||||
case "alternative" ⇒ multipart.alternative(params)
|
||||
case "related" ⇒ multipart.related(params)
|
||||
case "form-data" ⇒ multipart.`form-data`(params)
|
||||
case "signed" ⇒ multipart.signed(params)
|
||||
case "encrypted" ⇒ multipart.encrypted(params)
|
||||
case custom ⇒ multipart(custom, params)
|
||||
}
|
||||
case mainLower ⇒
|
||||
MediaTypes.getForKey((mainLower, subType.toLowerCase)) match {
|
||||
case Some(registered) ⇒ if (parameters.isEmpty) registered else registered.withParameters(parameters)
|
||||
case None ⇒ MediaType.custom(mainType, subType, parameters = parameters, allowArbitrarySubtypes = true)
|
||||
case Some(registered) ⇒ if (params.isEmpty) registered else registered.withParams(params)
|
||||
case None ⇒ MediaType.custom(mainType, subType, params = params, allowArbitrarySubtypes = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ private[parser] trait CommonRules { this: Parser with StringBuilding ⇒
|
|||
|
||||
def `byte-ranges-specifier` = rule { `bytes-unit` ~ ws('=') ~ `byte-range-set` }
|
||||
|
||||
def `bytes-unit` = rule { "bytes" ~ OWS ~ push(RangeUnit.Bytes) }
|
||||
def `bytes-unit` = rule { "bytes" ~ OWS ~ push(RangeUnits.Bytes) }
|
||||
|
||||
def `complete-length` = rule { longNumberCapped }
|
||||
|
||||
|
|
@ -319,7 +319,7 @@ private[parser] trait CommonRules { this: Parser with StringBuilding ⇒
|
|||
|
||||
def `other-range-set` = rule { oneOrMore(VCHAR) ~ OWS }
|
||||
|
||||
def `other-range-unit` = rule { token ~> RangeUnit.Other }
|
||||
def `other-range-unit` = rule { token ~> RangeUnits.Other }
|
||||
|
||||
def `other-ranges-specifier` = rule { `other-range-unit` ~ ws('=') ~ `other-range-set` }
|
||||
|
||||
|
|
@ -353,14 +353,14 @@ private[parser] trait CommonRules { this: Parser with StringBuilding ⇒
|
|||
// ******************************************************************************************
|
||||
|
||||
def `transfer-coding` = rule(
|
||||
ignoreCase("chunked") ~ OWS ~ push(TransferEncoding.chunked)
|
||||
| ignoreCase("gzip") ~ OWS ~ push(TransferEncoding.gzip)
|
||||
| ignoreCase("deflate") ~ OWS ~ push(TransferEncoding.deflate)
|
||||
| ignoreCase("compress") ~ OWS ~ push(TransferEncoding.compress)
|
||||
ignoreCase("chunked") ~ OWS ~ push(TransferEncodings.chunked)
|
||||
| ignoreCase("gzip") ~ OWS ~ push(TransferEncodings.gzip)
|
||||
| ignoreCase("deflate") ~ OWS ~ push(TransferEncodings.deflate)
|
||||
| ignoreCase("compress") ~ OWS ~ push(TransferEncodings.compress)
|
||||
| `transfer-extension`)
|
||||
|
||||
def `transfer-extension` = rule {
|
||||
token ~ zeroOrMore(ws(';') ~ `transfer-parameter`) ~> (_.toMap) ~> (TransferEncoding.Extension(_, _))
|
||||
token ~ zeroOrMore(ws(';') ~ `transfer-parameter`) ~> (_.toMap) ~> (TransferEncodings.Extension(_, _))
|
||||
}
|
||||
|
||||
def `transfer-parameter` = rule { token ~ ws('=') ~ word ~> (_ -> _) }
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ private[parser] trait ContentDispositionHeader { this: Parser with CommonRules w
|
|||
}
|
||||
|
||||
def `disposition-type` = rule(
|
||||
ignoreCase("inline") ~ OWS ~ push(ContentDispositionType.inline)
|
||||
| ignoreCase("attachment") ~ OWS ~ push(ContentDispositionType.attachment)
|
||||
| ignoreCase("form-data") ~ OWS ~ push(ContentDispositionType.`form-data`)
|
||||
| `disp-ext-type` ~> (ContentDispositionType.Ext(_)))
|
||||
ignoreCase("inline") ~ OWS ~ push(ContentDispositionTypes.inline)
|
||||
| ignoreCase("attachment") ~ OWS ~ push(ContentDispositionTypes.attachment)
|
||||
| ignoreCase("form-data") ~ OWS ~ push(ContentDispositionTypes.`form-data`)
|
||||
| `disp-ext-type` ~> (ContentDispositionTypes.Ext(_)))
|
||||
|
||||
def `disp-ext-type` = rule { token }
|
||||
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ private[parser] trait LinkHeader { this: Parser with CommonRules with CommonActi
|
|||
}
|
||||
|
||||
def `link-param` = rule(
|
||||
ws("rel") ~ ws('=') ~ `relation-types` ~> LinkParam.rel
|
||||
| ws("anchor") ~ ws('=') ~ ws('"') ~ UriReference('"') ~ ws('"') ~> LinkParam.anchor
|
||||
| ws("rev") ~ ws('=') ~ `relation-types` ~> LinkParam.rev
|
||||
| ws("hreflang") ~ ws('=') ~ language ~> LinkParam.hreflang
|
||||
| ws("media") ~ ws('=') ~ word ~> LinkParam.media
|
||||
| ws("title") ~ ws('=') ~ word ~> LinkParam.title
|
||||
| ws("title*") ~ ws('=') ~ word ~> LinkParam.`title*` // support full `ext-value` notation from http://tools.ietf.org/html/rfc5987#section-3.2.1
|
||||
| ws("type") ~ ws('=') ~ (ws('"') ~ `link-media-type` ~ ws('"') | `link-media-type`) ~> LinkParam.`type`)
|
||||
ws("rel") ~ ws('=') ~ `relation-types` ~> LinkParams.rel
|
||||
| ws("anchor") ~ ws('=') ~ ws('"') ~ UriReference('"') ~ ws('"') ~> LinkParams.anchor
|
||||
| ws("rev") ~ ws('=') ~ `relation-types` ~> LinkParams.rev
|
||||
| ws("hreflang") ~ ws('=') ~ language ~> LinkParams.hreflang
|
||||
| ws("media") ~ ws('=') ~ word ~> LinkParams.media
|
||||
| ws("title") ~ ws('=') ~ word ~> LinkParams.title
|
||||
| ws("title*") ~ ws('=') ~ word ~> LinkParams.`title*` // support full `ext-value` notation from http://tools.ietf.org/html/rfc5987#section-3.2.1
|
||||
| ws("type") ~ ws('=') ~ (ws('"') ~ `link-media-type` ~ ws('"') | `link-media-type`) ~> LinkParams.`type`)
|
||||
// TODO: support `link-extension`
|
||||
|
||||
def `relation-types` = rule(
|
||||
|
|
@ -69,12 +69,12 @@ private[parser] trait LinkHeader { this: Parser with CommonRules with CommonActi
|
|||
seenMedia: Boolean = false, seenTitle: Boolean = false, seenTitleS: Boolean = false,
|
||||
seenType: Boolean = false): Seq[LinkParam] =
|
||||
params match {
|
||||
case Seq((x: LinkParam.rel), tail @ _*) ⇒ sanitize(tail, if (seenRel) result else result :+ x, seenRel = true, seenMedia, seenTitle, seenTitleS, seenType)
|
||||
case Seq((x: LinkParam.media), tail @ _*) ⇒ sanitize(tail, if (seenMedia) result else result :+ x, seenRel, seenMedia = true, seenTitle, seenTitleS, seenType)
|
||||
case Seq((x: LinkParam.title), tail @ _*) ⇒ sanitize(tail, if (seenTitle) result else result :+ x, seenRel, seenMedia, seenTitle = true, seenTitleS, seenType)
|
||||
case Seq((x: LinkParam.`title*`), tail @ _*) ⇒ sanitize(tail, if (seenTitleS) result else result :+ x, seenRel, seenMedia, seenTitle, seenTitleS = true, seenType)
|
||||
case Seq((x: LinkParam.`type`), tail @ _*) ⇒ sanitize(tail, if (seenType) result else result :+ x, seenRel, seenMedia, seenTitle, seenTitleS, seenType = true)
|
||||
case Seq(head, tail @ _*) ⇒ sanitize(tail, result :+ head, seenRel, seenMedia, seenTitle, seenTitleS, seenType)
|
||||
case Nil ⇒ result
|
||||
case Seq((x: LinkParams.rel), tail @ _*) ⇒ sanitize(tail, if (seenRel) result else result :+ x, seenRel = true, seenMedia, seenTitle, seenTitleS, seenType)
|
||||
case Seq((x: LinkParams.media), tail @ _*) ⇒ sanitize(tail, if (seenMedia) result else result :+ x, seenRel, seenMedia = true, seenTitle, seenTitleS, seenType)
|
||||
case Seq((x: LinkParams.title), tail @ _*) ⇒ sanitize(tail, if (seenTitle) result else result :+ x, seenRel, seenMedia, seenTitle = true, seenTitleS, seenType)
|
||||
case Seq((x: LinkParams.`title*`), tail @ _*) ⇒ sanitize(tail, if (seenTitleS) result else result :+ x, seenRel, seenMedia, seenTitle, seenTitleS = true, seenType)
|
||||
case Seq((x: LinkParams.`type`), tail @ _*) ⇒ sanitize(tail, if (seenType) result else result :+ x, seenRel, seenMedia, seenTitle, seenTitleS, seenType = true)
|
||||
case Seq(head, tail @ _*) ⇒ sanitize(tail, result :+ head, seenRel, seenMedia, seenTitle, seenTitleS, seenType)
|
||||
case Nil ⇒ result
|
||||
}
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ private[http] class HttpServerPipeline(settings: ServerSettings,
|
|||
def errorResponse(status: StatusCode, info: ErrorInfo): ResponseRenderingContext = {
|
||||
log.warning("Illegal request, responding with status '{}': {}", status, info.formatPretty)
|
||||
val msg = if (settings.verboseErrorMessages) info.formatPretty else info.summary
|
||||
ResponseRenderingContext(HttpResponse(status, msg), closeAfterResponseCompletion = true)
|
||||
ResponseRenderingContext(HttpResponse(status, entity = msg), closeAfterResponseCompletion = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ final case class DateTime private (year: Int, // the year
|
|||
def weekdayStr: String = DateTime.weekday(weekday)
|
||||
|
||||
/**
|
||||
* The day of the month as a 3 letter abbreviation:
|
||||
* The month as a 3 letter abbreviation:
|
||||
* `Jan`, `Feb`, `Mar`, `Apr`, `May`, `Jun`, `Jul`, `Aug`, `Sep`, `Oct`, `Nov` or `Dec`
|
||||
*/
|
||||
def monthStr: String = DateTime.month(month - 1)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ object TestServer extends App {
|
|||
case HttpRequest(GET, Uri.Path("/"), _, _, _) ⇒ index
|
||||
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) ⇒ HttpResponse(entity = "PONG!")
|
||||
case HttpRequest(GET, Uri.Path("/crash"), _, _, _) ⇒ sys.error("BOOM!")
|
||||
case _: HttpRequest ⇒ HttpResponse(404, "Unknown resource!")
|
||||
case _: HttpRequest ⇒ HttpResponse(404, entity = "Unknown resource!")
|
||||
}
|
||||
|
||||
val materializer = FlowMaterializer(MaterializerSettings())
|
||||
|
|
@ -62,4 +62,4 @@ object TestServer extends App {
|
|||
</ul>
|
||||
</body>
|
||||
</html>.toString()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package akka.http.model
|
|||
import org.scalatest.{ Matchers, WordSpec }
|
||||
import akka.parboiled2.UTF8
|
||||
import Uri._
|
||||
import java.net.InetAddress
|
||||
|
||||
class UriSpec extends WordSpec with Matchers {
|
||||
|
||||
|
|
@ -21,6 +22,23 @@ class UriSpec extends WordSpec with Matchers {
|
|||
Host("3.0.0.0") shouldEqual IPv4Host("3.0.0.0")
|
||||
Host("30.0.0.0") shouldEqual IPv4Host("30.0.0.0")
|
||||
}
|
||||
"support inetAddresses round-trip for Inet4Addresses" in {
|
||||
def roundTrip(ip: String): Unit = {
|
||||
val inetAddr = InetAddress.getByName(ip)
|
||||
val addr = Host(inetAddr)
|
||||
addr shouldEqual IPv4Host(ip)
|
||||
addr.inetAddresses shouldEqual Seq(inetAddr)
|
||||
}
|
||||
|
||||
roundTrip("192.0.2.16")
|
||||
roundTrip("192.0.2.16")
|
||||
roundTrip("255.0.0.0")
|
||||
roundTrip("0.0.0.0")
|
||||
roundTrip("1.0.0.0")
|
||||
roundTrip("2.0.0.0")
|
||||
roundTrip("3.0.0.0")
|
||||
roundTrip("30.0.0.0")
|
||||
}
|
||||
|
||||
"parse correctly from IPv6 literals (RFC2732)" in {
|
||||
// various
|
||||
|
|
@ -79,6 +97,22 @@ class UriSpec extends WordSpec with Matchers {
|
|||
Host("[a:b:c::12:1]") shouldEqual IPv6Host("000a000b000c00000000000000120001", "a:b:c::12:1")
|
||||
Host("[a:b::0:1:2:3]") shouldEqual IPv6Host("000a000b000000000000000100020003", "a:b::0:1:2:3")
|
||||
}
|
||||
"support inetAddresses round-trip for Inet6Addresses" in {
|
||||
def fromAddress(address: String): IPv6Host = Host(s"[$address]").asInstanceOf[IPv6Host]
|
||||
def roundTrip(ip: String): Unit = {
|
||||
val inetAddr = InetAddress.getByName(ip)
|
||||
val addr = Host(inetAddr)
|
||||
addr equalsIgnoreCase fromAddress(ip) should be(true)
|
||||
addr.inetAddresses shouldEqual Seq(inetAddr)
|
||||
}
|
||||
|
||||
roundTrip("1:1:1::1:1:1:1")
|
||||
roundTrip("::1:2:3:4:5:6:7")
|
||||
roundTrip("2001:0DB8:0100:F101:0210:A4FF:FEE3:9566")
|
||||
roundTrip("2001:0db8:100:f101:0:0:0:1")
|
||||
roundTrip("abcd::12")
|
||||
roundTrip("::192.9.5.5")
|
||||
}
|
||||
|
||||
"parse correctly from NamedHost literals" in {
|
||||
Host("www.spray.io") shouldEqual NamedHost("www.spray.io")
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
"Accept: */*, text/*; foo=bar, custom/custom; bar=\"b>az\"" =!=
|
||||
Accept(`*/*`,
|
||||
MediaRange.custom("text", Map("foo" -> "bar")),
|
||||
MediaType.custom("custom", "custom", parameters = Map("bar" -> "b>az")))
|
||||
MediaType.custom("custom", "custom", params = Map("bar" -> "b>az")))
|
||||
"Accept: application/*+xml; version=2" =!=
|
||||
Accept(MediaType.custom("application", "*+xml", parameters = Map("version" -> "2")))
|
||||
Accept(MediaType.custom("application", "*+xml", params = Map("version" -> "2")))
|
||||
}
|
||||
|
||||
"Accept-Charset" in {
|
||||
|
|
@ -88,8 +88,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
}
|
||||
|
||||
"Accept-Ranges" in {
|
||||
"Accept-Ranges: bytes" =!= `Accept-Ranges`(RangeUnit.Bytes)
|
||||
"Accept-Ranges: bytes, sausages" =!= `Accept-Ranges`(RangeUnit.Bytes, RangeUnit.Other("sausages"))
|
||||
"Accept-Ranges: bytes" =!= `Accept-Ranges`(RangeUnits.Bytes)
|
||||
"Accept-Ranges: bytes, sausages" =!= `Accept-Ranges`(RangeUnits.Bytes, RangeUnits.Other("sausages"))
|
||||
"Accept-Ranges: none" =!= `Accept-Ranges`()
|
||||
}
|
||||
|
||||
|
|
@ -153,9 +153,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
}
|
||||
|
||||
"Content-Disposition" in {
|
||||
"Content-Disposition: form-data" =!= `Content-Disposition`(ContentDispositionType.`form-data`)
|
||||
"Content-Disposition: form-data" =!= `Content-Disposition`(ContentDispositionTypes.`form-data`)
|
||||
"Content-Disposition: attachment; name=field1; filename=\"file/txt\"" =!=
|
||||
`Content-Disposition`(ContentDispositionType.attachment, Map("name" -> "field1", "filename" -> "file/txt"))
|
||||
`Content-Disposition`(ContentDispositionTypes.attachment, Map("name" -> "field1", "filename" -> "file/txt"))
|
||||
}
|
||||
|
||||
"Content-Encoding" in {
|
||||
|
|
@ -175,7 +175,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
"Content-Type: text/plain; charset=utf8" =!=
|
||||
`Content-Type`(ContentType(`text/plain`, `UTF-8`)).renderedTo("text/plain; charset=UTF-8")
|
||||
"Content-Type: text/xml; version=3; charset=windows-1252" =!=
|
||||
`Content-Type`(ContentType(MediaType.custom("text", "xml", parameters = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252")))
|
||||
`Content-Type`(ContentType(MediaType.custom("text", "xml", params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252")))
|
||||
"Content-Type: text/plain; charset=fancy-pants" =!=
|
||||
ErrorInfo("Illegal HTTP header 'Content-Type': Unsupported charset", "fancy-pants")
|
||||
"Content-Type: multipart/mixed; boundary=ABC123" =!=
|
||||
|
|
@ -274,28 +274,28 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
}
|
||||
|
||||
"Link" in {
|
||||
"Link: </?page=2>; rel=next" =!= Link(Uri("/?page=2"), LinkParam.next)
|
||||
"Link: <https://spray.io>; rel=next" =!= Link(Uri("https://spray.io"), LinkParam.next)
|
||||
"Link: </?page=2>; rel=next" =!= Link(Uri("/?page=2"), LinkParams.next)
|
||||
"Link: <https://spray.io>; rel=next" =!= Link(Uri("https://spray.io"), LinkParams.next)
|
||||
"""Link: </>; rel=prev, </page/2>; rel="next"""" =!=
|
||||
Link(LinkValue(Uri("/"), LinkParam.prev), LinkValue(Uri("/page/2"), LinkParam.next)).renderedTo("</>; rel=prev, </page/2>; rel=next")
|
||||
Link(LinkValue(Uri("/"), LinkParams.prev), LinkValue(Uri("/page/2"), LinkParams.next)).renderedTo("</>; rel=prev, </page/2>; rel=next")
|
||||
|
||||
"""Link: </>; rel="x.y-z http://spray.io"""" =!= Link(Uri("/"), LinkParam.rel("x.y-z http://spray.io"))
|
||||
"""Link: </>; title="My Title"""" =!= Link(Uri("/"), LinkParam.title("My Title"))
|
||||
"""Link: </>; rel=next; title="My Title"""" =!= Link(Uri("/"), LinkParam.next, LinkParam.title("My Title"))
|
||||
"""Link: </>; anchor="http://example.com"""" =!= Link(Uri("/"), LinkParam.anchor(Uri("http://example.com")))
|
||||
"""Link: </>; rel="x.y-z http://spray.io"""" =!= Link(Uri("/"), LinkParams.rel("x.y-z http://spray.io"))
|
||||
"""Link: </>; title="My Title"""" =!= Link(Uri("/"), LinkParams.title("My Title"))
|
||||
"""Link: </>; rel=next; title="My Title"""" =!= Link(Uri("/"), LinkParams.next, LinkParams.title("My Title"))
|
||||
"""Link: </>; anchor="http://example.com"""" =!= Link(Uri("/"), LinkParams.anchor(Uri("http://example.com")))
|
||||
"""Link: </>; rev=foo; hreflang=de-de; media=print; type=application/json""" =!=
|
||||
Link(Uri("/"), LinkParam.rev("foo"), LinkParam.hreflang(Language("de", "de")), LinkParam.media("print"), LinkParam.`type`(`application/json`))
|
||||
Link(Uri("/"), LinkParams.rev("foo"), LinkParams.hreflang(Language("de", "de")), LinkParams.media("print"), LinkParams.`type`(`application/json`))
|
||||
|
||||
/* RFC 5988 examples */
|
||||
"""Link: <http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"""" =!=
|
||||
Link(Uri("http://example.com/TheBook/chapter2"), LinkParam.rel("previous"), LinkParam.title("previous chapter"))
|
||||
Link(Uri("http://example.com/TheBook/chapter2"), LinkParams.rel("previous"), LinkParams.title("previous chapter"))
|
||||
.renderedTo("""<http://example.com/TheBook/chapter2>; rel=previous; title="previous chapter"""")
|
||||
|
||||
"""Link: </>; rel="http://example.net/foo"""" =!= Link(Uri("/"), LinkParam.rel("http://example.net/foo"))
|
||||
"""Link: </>; rel="http://example.net/foo"""" =!= Link(Uri("/"), LinkParams.rel("http://example.net/foo"))
|
||||
.renderedTo("</>; rel=http://example.net/foo")
|
||||
|
||||
"""Link: <http://example.org/>; rel="start http://example.net/relation/other"""" =!= Link(Uri("http://example.org/"),
|
||||
LinkParam.rel("start http://example.net/relation/other"))
|
||||
LinkParams.rel("start http://example.net/relation/other"))
|
||||
|
||||
// only one 'rel=' is allowed, http://tools.ietf.org/html/rfc5988#section-5.3 requires any subsequent ones to be skipped
|
||||
"Link: </>; rel=prev; rel=next" =!=> "</>; rel=prev"
|
||||
|
|
@ -321,8 +321,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
}
|
||||
|
||||
"Transfer-Encoding" in {
|
||||
"Transfer-Encoding: chunked" =!= `Transfer-Encoding`(TransferEncoding.chunked)
|
||||
"Transfer-Encoding: gzip" =!= `Transfer-Encoding`(TransferEncoding.gzip)
|
||||
"Transfer-Encoding: chunked" =!= `Transfer-Encoding`(TransferEncodings.chunked)
|
||||
"Transfer-Encoding: gzip" =!= `Transfer-Encoding`(TransferEncodings.gzip)
|
||||
}
|
||||
|
||||
"Range" in {
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
|
|
||||
|"""
|
||||
val baseRequest = HttpRequest(PATCH, "/data", List(Host("ping"), `Content-Type`(`application/pdf`),
|
||||
Connection("lalelu"), `Transfer-Encoding`(TransferEncoding.chunked)))
|
||||
Connection("lalelu"), `Transfer-Encoding`(TransferEncodings.chunked)))
|
||||
|
||||
"request start" in new Test {
|
||||
Seq(start, "rest") should generalMultiParseTo(
|
||||
|
|
@ -220,7 +220,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
|
|
||||
|"""
|
||||
val baseRequest = HttpRequest(PATCH, "/data", List(Host("ping"), Connection("lalelu"),
|
||||
`Transfer-Encoding`(TransferEncoding.chunked)), HttpEntity.Chunked(`application/octet-stream`, producer()))
|
||||
`Transfer-Encoding`(TransferEncodings.chunked)), HttpEntity.Chunked(`application/octet-stream`, producer()))
|
||||
|
||||
"an illegal char after chunk size" in new Test {
|
||||
Seq(start,
|
||||
|
|
|
|||
|
|
@ -126,14 +126,14 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|
|||
}
|
||||
"with one chunk and incorrect (too large) Content-Length" in new TestSetup() {
|
||||
the[RuntimeException] thrownBy {
|
||||
HttpResponse(200, Default(ContentTypes.`application/json`, 10,
|
||||
HttpResponse(200, entity = Default(ContentTypes.`application/json`, 10,
|
||||
producer(ByteString("body123")))) should renderTo("")
|
||||
} should have message "Response had declared Content-Length 10 but entity chunk stream amounts to 3 bytes less"
|
||||
}
|
||||
|
||||
"with one chunk and incorrect (too small) Content-Length" in new TestSetup() {
|
||||
the[RuntimeException] thrownBy {
|
||||
HttpResponse(200, Default(ContentTypes.`application/json`, 5,
|
||||
HttpResponse(200, entity = Default(ContentTypes.`application/json`, 5,
|
||||
producer(ByteString("body123")))) should renderTo("")
|
||||
} should have message "Response had declared Content-Length 5 but entity chunk stream amounts to more bytes"
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|
|||
"a response with a CloseDelimited body" - {
|
||||
"without data" in new TestSetup() {
|
||||
ResponseRenderingContext(
|
||||
HttpResponse(200, CloseDelimited(ContentTypes.`application/json`,
|
||||
HttpResponse(200, entity = CloseDelimited(ContentTypes.`application/json`,
|
||||
producer(ByteString.empty)))) should renderTo(
|
||||
"""HTTP/1.1 200 OK
|
||||
|Server: akka-http/1.0.0
|
||||
|
|
@ -154,7 +154,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|
|||
}
|
||||
"consisting of two parts" in new TestSetup() {
|
||||
ResponseRenderingContext(
|
||||
HttpResponse(200, CloseDelimited(ContentTypes.`application/json`,
|
||||
HttpResponse(200, entity = CloseDelimited(ContentTypes.`application/json`,
|
||||
producer(ByteString("abc"), ByteString("defg"))))) should renderTo(
|
||||
"""HTTP/1.1 200 OK
|
||||
|Server: akka-http/1.0.0
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ package akka.stream
|
|||
import akka.stream.testkit.StreamTestKit
|
||||
import akka.stream.scaladsl.Flow
|
||||
import org.reactivestreams.api.Producer
|
||||
import akka.stream.testkit.OnSubscribe
|
||||
import akka.stream.testkit.OnError
|
||||
import scala.concurrent.Promise
|
||||
|
||||
class FlowConcatSpec extends TwoStreamsSetup {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue