diff --git a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala index 279ba35841..04c44e16a4 100644 --- a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala @@ -55,14 +55,12 @@ final case class Connection(tokens: immutable.Seq[String]) extends ModeledHeader } // http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-26#section-3.3.2 -private[http] object `Content-Length` extends ModeledCompanion +object `Content-Length` extends ModeledCompanion /** * Instances of this class will only be created transiently during header parsing and will never appear * in HttpMessage.header. To access the Content-Length, see subclasses of HttpEntity. - * - * INTERNAL API */ -private[http] final case class `Content-Length`(length: Long)(implicit ev: ProtectedHeaderCreation.Enabled) extends ModeledHeader { +final case class `Content-Length` private[http] (length: Long)(implicit ev: ProtectedHeaderCreation.Enabled) extends ModeledHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ length protected def companion = `Content-Length` } @@ -349,7 +347,11 @@ final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRang // http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-26#section-3.1.1.5 object `Content-Type` extends ModeledCompanion -final case class `Content-Type`(contentType: ContentType) extends japi.headers.ContentType with ModeledHeader { +/** + * Instances of this class will only be created transiently during header parsing and will never appear + * in HttpMessage.header. To access the Content-Type, see subclasses of HttpEntity. + */ +final case class `Content-Type` private[http] (contentType: ContentType) extends japi.headers.ContentType with ModeledHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ contentType protected def companion = `Content-Type` diff --git a/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala b/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala new file mode 100644 index 0000000000..5b8bfb1188 --- /dev/null +++ b/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala @@ -0,0 +1,213 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package io.akka.integrationtest.http + +import akka.actor.ActorSystem +import akka.http.model._ +import akka.http.model.ContentTypes +import akka.http.model.headers._ +import akka.http.model.parser.HeaderParser +import akka.stream.scaladsl2._ +import akka.util.ByteString +import com.typesafe.config.{ ConfigFactory, Config } +import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec } +import scala.concurrent.duration._ + +/** + * Integration test for external HTTP libraries that are built on top of + * Akka HTTP core. An example of an external library is Play Framework. + * The way these libraries use Akka HTTP core may be different to how + * normal users would use Akka HTTP core. For example the libraries may + * need direct access to some model objects that users who use Akka HTTP + * directly would not require. + * + * This test is designed to capture the needs of these external libaries. + * Each test gives a use case of an external library and then checks that + * it can be fulfilled. A typical example of a use case is converting + * between one library's HTTP model and the Akka HTTP core HTTP model. + * + * This test is located a different package (io.akka vs akka) in order to + * check for any visibility issues when Akka HTTP core is used by third + * party libraries. + */ +class HttpModelIntegrationSpec extends WordSpec with Matchers with BeforeAndAfterAll { + + val testConf: Config = ConfigFactory.parseString(""" + akka.event-handlers = ["akka.testkit.TestEventListener"] + akka.loglevel = WARNING""") + implicit val system = ActorSystem(getClass.getSimpleName, testConf) + import system.dispatcher + + override def afterAll() = system.shutdown() + + implicit val materializer = FlowMaterializer() + implicit val materializer1 = akka.stream.FlowMaterializer() // Needed by HttpEntity.toStrict + + "External HTTP libraries" should { + + "be able to get String headers and an Array[Byte] body out of an HttpRequest" in { + + // First create an incoming HttpRequest for the external HTTP library + // to deal with. We're going to convert this request into the library's + // own HTTP model. + + val request = HttpRequest( + method = HttpMethods.POST, + uri = Uri("/greeting"), + headers = List( + Host("localhost"), + RawHeader("Origin", "null")), + entity = HttpEntity.Default( + contentType = ContentTypes.`application/json`, + contentLength = 5, + FlowFrom(List(ByteString("hello"))).toPublisher())) + + // Our library uses a simple model of headers: a Seq[(String, String)]. + // The body is represented as an Array[Byte]. To get the headers in + // the form we want we first need to reconstruct the original headers + // by combining the request.headers value with the headers that are + // implicitly contained by the request.entity. We convert all the + // HttpHeaders by getting their name and value. We convert Content-Type + // and Content-Length by using the toString of their values. + + val partialTextHeaders: Seq[(String, String)] = request.headers.map(h ⇒ (h.name, h.value)) + val entityTextHeaders: Seq[(String, String)] = request.entity match { + case HttpEntity.Default(contentType, contentLength, _) ⇒ + Seq(("Content-Type", contentType.toString), ("Content-Length", contentLength.toString)) + case _ ⇒ + ??? + } + val textHeaders: Seq[(String, String)] = entityTextHeaders ++ partialTextHeaders + textHeaders shouldEqual Seq( + "Content-Type" -> "application/json; charset=UTF-8", + "Content-Length" -> "5", + "Host" -> "localhost", + "Origin" -> "null") + + // Finally convert the body into an Array[Byte]. + + val entityBytes: Array[Byte] = request.entity.toStrict(1.second).await(2.seconds).data.toArray + entityBytes.to[Seq] shouldEqual ByteString("hello").to[Seq] + } + + "be able to build an HttpResponse from String headers and Array[Byte] body" in { + + // External HTTP libraries (such as Play) will model HTTP differently + // to Akka HTTP. One model uses a Seq[(String, String)] for headers and + // an Array[Byte] for a body. The following data structures show an + // example simple model of an HTTP response. + + val textHeaders: Seq[(String, String)] = Seq( + "Content-Type" -> "text/plain", + "Content-Length" -> "3", + "X-Greeting" -> "Hello") + val byteArrayBody: Array[Byte] = "foo".getBytes + + // Now we need to convert this model to Akka HTTP's model. To do that + // we use Akka HTTP's HeaderParser to parse the headers, giving us a + // List[HttpHeader]. + + val rawHeaders = textHeaders.map { case (name, value) ⇒ RawHeader(name, value) } + val (parseErrors, convertedHeaders): (List[_], List[HttpHeader]) = HeaderParser.parseHeaders(rawHeaders.to[List]) + parseErrors shouldEqual Nil + + // Most of these headers are modeled by Akka HTTP as a Seq[HttpHeader], + // but the the Content-Type and Content-Length are special: their + // values relate to the HttpEntity and so they're modeled as part of + // the HttpEntity. These headers need to be stripped out of the main + // Seq[Header] and dealt with separately. + + val normalHeaders = convertedHeaders.filter { + case _: `Content-Type` ⇒ false + case _: `Content-Length` ⇒ false + case _ ⇒ true + } + normalHeaders.head shouldEqual RawHeader("X-Greeting", "Hello") + normalHeaders.tail shouldEqual Nil + + val contentType = convertedHeaders.collectFirst { + case ct: `Content-Type` ⇒ ct.contentType + } + contentType shouldEqual Some(ContentTypes.`text/plain`) + + val contentLength = convertedHeaders.collectFirst { + case cl: `Content-Length` ⇒ cl.length + } + contentLength shouldEqual Some(3) + + // We're going to model the HttpEntity as an HttpEntity.Default, so + // convert the body into a Publisher[ByteString]. + + val byteStringBody = ByteString(byteArrayBody) + val publisherBody = FlowFrom(List(byteStringBody)).toPublisher() + + // Finally we can create our HttpResponse. + + HttpResponse( + entity = HttpEntity.Default(contentType.get, contentLength.get, publisherBody)) + } + + "be able to wrap HttpHeaders with custom typed headers" in { + + // This HTTP model is typed. It uses Akka HTTP types internally, but + // no Akka HTTP types are visible to users. This typed model is a + // model that Play Framework may eventually move to. + + object ExampleLibrary { + + trait TypedHeader { + def name: String + def value: String + } + + private[ExampleLibrary] case class GenericHeader(internal: HttpHeader) extends TypedHeader { + def name: String = internal.name + def value: String = internal.value + } + private[ExampleLibrary] case class ContentTypeHeader(contentType: ContentType) extends TypedHeader { + def name: String = "Content-Type" + def value: String = contentType.toString + } + private[ExampleLibrary] case class ContentLengthHeader(length: Long) extends TypedHeader { + def name: String = "Content-Length" + def value: String = length.toString + } + + // Headers can be created from strings. + def header(name: String, value: String): TypedHeader = { + val parsedHeader = HeaderParser.parseHeader(RawHeader(name, value)).fold( + error ⇒ sys.error(s"Failed to parse: $error"), + parsed ⇒ parsed) + parsedHeader match { + case `Content-Type`(contentType) ⇒ ContentTypeHeader(contentType) + case `Content-Length`(length) ⇒ ContentLengthHeader(length) + case _ ⇒ GenericHeader(parsedHeader) + } + } + + // Or they can be created more directly, without parsing. + def contentType(contentType: ContentType): TypedHeader = + new ContentTypeHeader(contentType) + def contentLength(length: Long): TypedHeader = + new ContentLengthHeader(length) + + } + + // Users of ExampleLibrary should be able to create headers by + // parsing strings + ExampleLibrary.header("X-Y-Z", "abc") + ExampleLibrary.header("Host", "null") + ExampleLibrary.header("Content-Length", "3") + + // Users should also be able to create headers directly. Internally + // we want avoid parsing when possible (for efficiency), so it's good + // to be able to directly create a ContentType and ContentLength + // headers. + ExampleLibrary.contentLength(3) + ExampleLibrary.contentType(ContentTypes.`text/plain`) + } + + } +}