Skip to content

Commit

Permalink
Merge branch 'release-0.18.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
rossabaker committed Aug 14, 2018
2 parents d2df1f2 + f6f8a9c commit d4541d1
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 4 deletions.
11 changes: 11 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,17 @@ lazy val json4sJackson = libraryProject("json4s-jackson")
)
.dependsOn(json4s % "compile;test->test")

lazy val playJson = libraryProject("play-json")
.settings(
description := "Provides Play json codecs for http4s",
libraryDependencies ++= Seq(
jawnPlay,
Http4sPlugin.playJson
),
mimaPreviousArtifacts := Set.empty // remove me once merged
)
.dependsOn(jawn % "compile;test->test")

lazy val scalaXml = libraryProject("scala-xml")
.settings(
description := "Provides scala-xml codecs for http4s",
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/org/http4s/headers/Referer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package headers
import org.http4s.parser.HttpHeaderParser
import org.http4s.util.Writer

object Referer extends HeaderKey.Internal[Referer] {
object Referer extends HeaderKey.Internal[Referer] with HeaderKey.Singleton {
override def parse(s: String): ParseResult[Referer] =
HttpHeaderParser.REFERER(s)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.http4s.play

object PlayEntityCodec extends PlayEntityDecoder with PlayEntityEncoder
14 changes: 14 additions & 0 deletions play-json/src/main/scala/org/http4s/play/PlayEntityDecoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.http4s.play

import cats.effect.Sync
import org.http4s.EntityDecoder
import play.api.libs.json.Reads

/**
* Derive [[EntityDecoder]] if implicit [[Reads]] is in the scope without need to explicitly call `jsonOf`
*/
trait PlayEntityDecoder {
implicit def playEntityDecoder[F[_]: Sync, A: Reads]: EntityDecoder[F, A] = jsonOf[F, A]
}

object PlayEntityDecoder extends PlayEntityDecoder
15 changes: 15 additions & 0 deletions play-json/src/main/scala/org/http4s/play/PlayEntityEncoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.http4s.play

import cats.Applicative
import play.api.libs.json.Writes
import org.http4s.EntityEncoder

/**
* Derive [[EntityEncoder]] if implicit [[Writes]] is in the scope without need to explicitly call `jsonEncoderOf`
*/
trait PlayEntityEncoder {
implicit def playEntityEncoder[F[_]: Applicative, A: Writes]: EntityEncoder[F, A] =
jsonEncoderOf(EntityEncoder.stringEncoder[F], implicitly, implicitly)
}

object PlayEntityEncoder extends PlayEntityEncoder
68 changes: 68 additions & 0 deletions play-json/src/main/scala/org/http4s/play/PlayInstances.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.http4s.play

import _root_.jawn.support.play.Parser.facade
import cats.Applicative
import cats.effect.Sync
import fs2.Chunk
import org.http4s.headers.`Content-Type`
import org.http4s.{
DecodeResult,
EntityDecoder,
EntityEncoder,
InvalidMessageBodyFailure,
MediaType,
Message,
Uri,
jawn
}
import play.api.libs.json._

trait PlayInstances {

def jsonOf[F[_]: Sync, A](implicit decoder: Reads[A]): EntityDecoder[F, A] =
jsonDecoder[F].flatMapR { json =>
decoder
.reads(json)
.fold(
_ =>
DecodeResult.failure(InvalidMessageBodyFailure(s"Could not decode JSON: $json", None)),
DecodeResult.success(_)
)
}

implicit def jsonDecoder[F[_]: Sync]: EntityDecoder[F, JsValue] =
jawn.jawnDecoder[F, JsValue]

def jsonEncoderOf[F[_]: EntityEncoder[?[_], String]: Applicative, A: Writes]
: EntityEncoder[F, A] =
jsonEncoder[F].contramap[A](Json.toJson(_))

implicit def jsonEncoder[F[_]: Applicative]: EntityEncoder[F, JsValue] =
EntityEncoder[F, Chunk[Byte]]
.contramap[JsValue] { json =>
val bytes = json.toString.getBytes("UTF8")
Chunk.bytes(bytes)
}
.withContentType(`Content-Type`(MediaType.application.json))

implicit val writesUri: Writes[Uri] =
Writes.contravariantfunctorWrites.contramap[String, Uri](implicitly[Writes[String]], _.toString)

implicit val readsUri: Reads[Uri] =
implicitly[Reads[String]].flatMap { str =>
Uri
.fromString(str)
.fold(
_ =>
new Reads[Uri] {
def reads(json: JsValue): JsResult[Uri] = JsError("Invalid uri")
},
Reads.pure(_)
)
}

implicit class MessageSyntax[F[_]: Sync](self: Message[F]) {
def decodeJson[A: Reads]: F[A] =
self.as(implicitly, jsonOf[F, A])
}
}
3 changes: 3 additions & 0 deletions play-json/src/main/scala/org/http4s/play/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.http4s

package object play extends PlayInstances
90 changes: 90 additions & 0 deletions play-json/src/test/scala/org/http4s/play/PlaySpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.http4s
package play.test // Get out of play package so we can import custom instances

import _root_.play.api.libs.json._
import cats.effect.IO
import cats.effect.laws.util.TestContext
import org.http4s.headers.`Content-Type`
import org.http4s.jawn.JawnDecodeSupportSpec
import org.http4s.play._

// Originally based on CirceSpec
class PlaySpec extends JawnDecodeSupportSpec[JsValue] {
implicit val testContext = TestContext()

testJsonDecoder(jsonDecoder)

sealed case class Foo(bar: Int)
val foo = Foo(42)
implicit val format: OFormat[Foo] = Json.format[Foo]

"json encoder" should {
val json: JsValue = Json.obj("test" -> JsString("PlaySupport"))

"have json content type" in {
jsonEncoder.headers.get(`Content-Type`) must_== Some(
`Content-Type`(MediaType.application.json))
}

"write JSON" in {
writeToString(json) must_== ("""{"test":"PlaySupport"}""")
}

}

"jsonEncoderOf" should {
"have json content type" in {
jsonEncoderOf[IO, Foo].headers.get(`Content-Type`) must_== Some(
`Content-Type`(MediaType.application.json))
}

"write compact JSON" in {
writeToString(foo)(jsonEncoderOf[IO, Foo]) must_== ("""{"bar":42}""")
}

}

"jsonOf" should {
"decode JSON from a Play decoder" in {
val result = jsonOf[IO, Foo].decode(
Request[IO]().withEntity(Json.obj("bar" -> JsNumber(42)): JsValue),
strict = true)
result.value.unsafeRunSync must_== Right(Foo(42))
}
}

"Uri codec" should {
"round trip" in {
// TODO would benefit from Arbitrary[Uri]
val uri = Uri.uri("http://www.example.com/")

Json.fromJson[Uri](Json.toJson(uri)).asOpt must_== (Some(uri))
}
}

"Message[F].decodeJson[A]" should {
"decode json from a message" in {
val req = Request[IO]().withEntity(Json.toJson(foo))
req.decodeJson[Foo] must returnValue(foo)
}

"fail on invalid json" in {
val req = Request[IO]().withEntity(Json.toJson(List(13, 14)))
req.decodeJson[Foo].attempt.unsafeRunSync must beLeft
}
}

"PlayEntityCodec" should {
"decode json without defining EntityDecoder" in {
import org.http4s.play.PlayEntityDecoder._
val request = Request[IO]().withEntity(Json.obj("bar" -> JsNumber(42)): JsValue)
val result = request.as[Foo]
result.unsafeRunSync must_== Foo(42)
}

"encode without defining EntityEncoder using default printer" in {
import org.http4s.play.PlayEntityEncoder._
writeToString(foo) must_== """{"bar":42}"""
}
}
}
3 changes: 2 additions & 1 deletion project/Http4sPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,6 @@ object Http4sPlugin extends AutoPlugin {
}
}


lazy val alpnBoot = "org.mortbay.jetty.alpn" % "alpn-boot" % "8.1.12.v20180117"
lazy val argonaut = "io.argonaut" %% "argonaut" % "6.2.2"
lazy val asyncHttpClient = "org.asynchttpclient" % "async-http-client" % "2.5.2"
Expand All @@ -302,6 +301,7 @@ object Http4sPlugin extends AutoPlugin {
lazy val javaxServletApi = "javax.servlet" % "javax.servlet-api" % "3.1.0"
lazy val jawnJson4s = "org.spire-math" %% "jawn-json4s" % "0.12.2"
lazy val jawnFs2 = "org.http4s" %% "jawn-fs2" % "0.13.0-M1"
lazy val jawnPlay = "org.spire-math" %% "jawn-play" % "0.12.2"
lazy val jettyRunner = "org.eclipse.jetty" % "jetty-runner" % jettyServer.revision
lazy val jettyServer = "org.eclipse.jetty" % "jetty-server" % "9.4.11.v20180605"
lazy val jettyServlet = "org.eclipse.jetty" % "jetty-servlet" % jettyServer.revision
Expand All @@ -315,6 +315,7 @@ object Http4sPlugin extends AutoPlugin {
lazy val metricsJson = "io.dropwizard.metrics" % "metrics-json" % metricsCore.revision
lazy val mockito = "org.mockito" % "mockito-core" % "2.21.0"
lazy val okhttp = "com.squareup.okhttp3" % "okhttp" % "3.11.0"
lazy val playJson = "com.typesafe.play" %% "play-json" % "2.6.9"
lazy val prometheusClient = "io.prometheus" % "simpleclient_common" % "0.5.0"
lazy val prometheusHotspot = "io.prometheus" % "simpleclient_hotspot" % prometheusClient.revision
lazy val parboiled = "org.http4s" %% "parboiled" % "1.0.0"
Expand Down
3 changes: 3 additions & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
sbt.version=1.2.1
<<<<<<< HEAD

=======
>>>>>>> release-0.18.x
10 changes: 9 additions & 1 deletion tests/src/test/scala/org/http4s/headers/RefererSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.http4s.headers

import org.http4s.Uri
import cats.effect.IO
import org.http4s.{Headers, Request, Uri}

class RefererSpec extends HeaderLaws {
checkAll("Referer", headerLaws(`Retry-After`))
Expand All @@ -26,4 +27,11 @@ class RefererSpec extends HeaderLaws {
Referer.parse("../../index.html").right.map(_.uri) must beRight(getUri("../../index.html"))
}
}

"should be extractable" in {
val referer = Referer(getUri("http://localhost:8080"))
val request = Request[IO](headers = Headers(referer))

request.headers.get(Referer) should beSome(referer)
}
}
2 changes: 1 addition & 1 deletion twirl/src/main/scala/org/http4s/twirl/TwirlInstances.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package twirl

import org.http4s.headers.`Content-Type`
import org.http4s.MediaType
import play.twirl.api._
import _root_.play.twirl.api._

trait TwirlInstances {
implicit def htmlContentEncoder[F[_]](
Expand Down
14 changes: 14 additions & 0 deletions website/src/hugo/content/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ Maintenance branches are merged before each new release. This change log is
ordered chronologically, so each release contains all changes described below
it.

# v0.18.16
* Fix regression for `AutoSlash` when nested in a `Router` [#1948](https://github.com/http4s/http4s/pull/1948)
* Respect `redactHeadersWhen` in `Logger` middleware [#1952](https://github.com/http4s/http4s/pull/1952)
* Capture `BufferPoolsExports` in prometheus server middleware [#1977](https://github.com/http4s/http4s/pull/1977)
* Make `Referer` header extractable [#1984](https://github.com/http4s/http4s/pull/1984)
* Log server startup banner in a single call to prevent interspersion [#1985](https://github.com/http4s/http4s/pull/1985)
* Add support module for play-json [#1946](https://github.com/http4s/http4s/pull/1946)
* Dependency upgrades:
* cats-1.2.0
* metrics-4.0.3
* okhttp-3.11.0
* prometheus-client-0.5.0
* scodec-bits-1.1.6

# v0.18.15 (2018-07-05)
* Bugfix for `AutoSlash` Middleware in Router [#1937](https://github.com/http4s/http4s/pull/1937)
* Add `StaticHeaders` middleware that appends static headers to a service [#1939](https://github.com/http4s/http4s/pull/1939)
Expand Down

0 comments on commit d4541d1

Please sign in to comment.