Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a http4s0.23 version to help with cats effect 3 upgrades. #220

Merged
merged 5 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Scala-Pact is intended for Scala developers who are looking for a better way to

If you are just starting out on your pact journey in scala, we recommend checking out [pact4s](https://github.com/jbwheatley/pact4s). This is built directly on top of pact-jvm, and provides support for writing and verifying contracts using [scalaTest](https://github.com/scalatest/scalatest), [weaver-test](https://github.com/disneystreaming/weaver-test), and [munit-cats-effect-3](https://github.com/typelevel/munit-cats-effect).

## Latest version is 3.3.1
## Latest version is 3.3.2-RC1

Scala-Pact currently only supports [v2 of the pact specification](https://github.com/pact-foundation/pact-specification/tree/version-2). Support for v3 is a future goal of the project.

Expand All @@ -16,7 +16,7 @@ Before this version, the project versioning did not follow semantic versioning.

Scala-Pact now has two branches based on SBT requirements.

#### SBT 1.x compatible (Latest 3.3.1)
#### SBT 1.x compatible (Latest 3.3.2-RC1)

All development going forward begins at `2.3.x` and resides on the `master` branch.
For the sake of the maintainer's sanity, version 2.3.x and beyond will only support Scala 2.12 and SBT 1.x or greater. The project is currently cross-compiled across scala 2.12.12 and 2.13.4.
Expand Down Expand Up @@ -52,14 +52,14 @@ import com.itv.scalapact.plugin._
enablePlugins(ScalaPactPlugin)

libraryDependencies ++= Seq(
"com.itv" %% "scalapact-scalatest-suite" % "3.3.1" % "test",
"com.itv" %% "scalapact-scalatest-suite" % "3.3.2-RC1" % "test",
"org.scalatest" %% "scalatest" % "3.0.5" % "test"
)
```

Add this line to your `project/plugins.sbt` file to install the plugin:
```scala
addSbtPlugin("com.itv" % "sbt-scalapact" % "3.3.1")
addSbtPlugin("com.itv" % "sbt-scalapact" % "3.3.2-RC1")
```

Both the import and the plugin come pre-packaged with the latest JSON and Http libraries (http4s 0.21.x, and circe 0.13.x).
Expand All @@ -75,27 +75,27 @@ import com.itv.scalapact.plugin._
enablePlugins(ScalaPactPlugin)

libraryDependencies ++= Seq(
"com.itv" %% "scalapact-circe-0-13" % "3.3.1" % "test",
"com.itv" %% "scalapact-http4s-0-21" % "3.3.1" % "test",
"com.itv" %% "scalapact-scalatest" % "3.3.1" % "test",
"com.itv" %% "scalapact-circe-0-13" % "3.3.2-RC1" % "test",
"com.itv" %% "scalapact-http4s-0-21" % "3.3.2-RC1" % "test",
"com.itv" %% "scalapact-scalatest" % "3.3.2-RC1" % "test",
"org.scalatest" %% "scalatest" % "3.0.5" % "test"
)
```

Add this line to your `project/plugins.sbt` file to install the plugin:
```scala
addSbtPlugin("com.itv" % "sbt-scalapact" % "3.3.1")
addSbtPlugin("com.itv" % "sbt-scalapact" % "3.3.2-RC1")
```
This version of the plugin comes pre-packaged with the latest JSON and Http libraries.
Thanks to the way SBT works, that one plugin line will work in most cases, but if you're still having conflicts, you can also do this to use your preferred libraries:

```scala
libraryDependencies ++= Seq(
"com.itv" %% "scalapact-argonaut-6-2" % "3.3.1",
"com.itv" %% "scalapact-http4s-0-21" % "3.3.1"
"com.itv" %% "scalapact-argonaut-6-2" % "3.3.2-RC1",
"com.itv" %% "scalapact-http4s-0-21" % "3.3.2-RC1"
)

addSbtPlugin("com.itv" % "sbt-scalapact-nodeps" % "3.3.1")
addSbtPlugin("com.itv" % "sbt-scalapact-nodeps" % "3.3.2-RC1")
```

In your test suite, you will need the following imports:
Expand Down
25 changes: 20 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ def compilerOptionsVersion(scalaVersion: String) =
case _ => Nil
})

lazy val scalaVersion212: String = "2.12.12"
lazy val scalaVersion213: String = "2.13.4"
lazy val scalaVersion212: String = "2.12.13"
lazy val scalaVersion213: String = "2.13.6"
lazy val supportedScalaVersions = List(scalaVersion212, scalaVersion213)

ThisBuild / scalaVersion := scalaVersion212
Expand All @@ -78,7 +78,7 @@ lazy val commonSettings = Seq(
crossScalaVersions := supportedScalaVersions,
scalacOptions ++= compilerOptionsVersion(scalaVersion.value),
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.0.8" % "test"
"org.scalatest" %% "scalatest" % "3.0.9" % "test"
),
wartremoverWarnings in (Compile, compile) ++= Warts.allBut(
Wart.Any,
Expand Down Expand Up @@ -184,6 +184,21 @@ lazy val http4s021 =
)
.dependsOn(shared)

lazy val http4s023 =
(project in file("scalapact-http4s-0-23"))
.settings(commonSettings: _*)
.settings(publishSettings: _*)
.settings(
name := "scalapact-http4s-0-23",
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-blaze-server" % "0.23.0-RC1" exclude("org.scala-lang.modules", "scala-xml"),
"org.http4s" %% "http4s-blaze-client" % "0.23.0-RC1" exclude("org.scala-lang.modules", "scala-xml"),
"org.http4s" %% "http4s-dsl" % "0.23.0-RC1",
"com.github.tomakehurst" % "wiremock" % "2.25.1" % "test"
)
)
.dependsOn(shared)

lazy val testShared =
(project in file("scalapact-test-shared"))
.settings(commonSettings: _*)
Expand Down Expand Up @@ -316,7 +331,7 @@ lazy val testsWithDeps =
.settings(
libraryDependencies ++= Seq(
"org.scalaj" %% "scalaj-http" % "2.4.2" % "test",
"org.json4s" %% "json4s-native" % "3.6.9" % "test",
"org.json4s" %% "json4s-native" % "3.6.11" % "test",
"com.github.tomakehurst" % "wiremock" % "1.56" % "test",
"fr.hmil" %% "roshttp" % "2.1.0" % "test",
"io.argonaut" %% "argonaut" % "6.2.5"
Expand Down Expand Up @@ -351,7 +366,7 @@ lazy val scalaPactProject =
crossScalaVersions := Nil
)
.aggregate(shared, core, pluginShared, plugin, pluginNoDeps, framework, testShared)
.aggregate(http4s021)
.aggregate(http4s021, http4s023)
.aggregate(argonaut62, circe13)
.aggregate(standalone, frameworkWithDeps)
.aggregate(docs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.itv.scalapact

import com.itv.scalapact.http4s23.impl.HttpInstances

package object http extends HttpInstances
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.itv.scalapact.http4s23.impl

import cats.effect.{IO, Resource}
import com.itv.scalapact.shared.http.{SimpleRequest, SimpleResponse, SslContextMap}
import com.itv.scalapact.shared.utils.PactLogger
import org.http4s._
import org.http4s.blaze.client.BlazeClientBuilder
import org.http4s.client.Client
import org.http4s.headers.`User-Agent`

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

object Http4sClientHelper {

def defaultClient: Resource[IO, Client[IO]] =
buildPooledBlazeHttpClient(1, Duration(5, SECONDS), None)

def buildPooledBlazeHttpClient(maxTotalConnections: Int, clientTimeout: Duration, sslContextName: Option[String])(
implicit sslContextMap: SslContextMap
): Resource[IO, Client[IO]] = {
val sslContext = sslContextMap(sslContextName)
val builder = BlazeClientBuilder[IO](ExecutionContext.Implicits.global)
.withMaxTotalConnections(maxTotalConnections)
.withRequestTimeout(clientTimeout)
.withUserAgentOption(Option(`User-Agent`(ProductId("scala-pact", Option(BuildInfo.version)))))

PactLogger.debug(
s"Creating http4s client: connections $maxTotalConnections, timeout $clientTimeout, sslContextName: $sslContextName, sslContextMap: $sslContextMap"
)

sslContext.fold(builder)(s => builder.withSslContext(s)).resource
}

def doRequest(request: SimpleRequest, httpClient: Resource[IO, Client[IO]]): IO[SimpleResponse] =
for {
request <- Http4sRequestResponseFactory.buildRequest(request)
_ <- IO(PactLogger.message(s"cURL for request: ${request.asCurl()}"))
response <- httpClient.use { c =>
c.run(request).use { r: Response[IO] =>
r.bodyText.compile.toVector
.map(_.mkString)
.map { b =>
SimpleResponse(r.status.code, r.headers.toMap, Some(b))
}
}
}
} yield response
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.itv.scalapact.http4s23.impl

import java.nio.charset.StandardCharsets

import cats.effect.IO
import com.itv.scalapact.shared.http.HttpMethod._
import com.itv.scalapact.shared.http.{HttpMethod, SimpleRequest}
import fs2.Chunk
import org.http4s.Method
import org.http4s._
import scodec.bits.ByteVector

object Http4sRequestResponseFactory {

implicit val enc: EntityEncoder[IO, String] =
EntityEncoder.simple[IO, String]() { str =>
Chunk.byteVector(ByteVector(str.getBytes(StandardCharsets.UTF_8)))
}

def buildUri(baseUrl: String, endpoint: String): IO[Uri] =
IO.fromEither(Uri.fromString(baseUrl + endpoint))

def intToStatus(status: IntAndReason): ParseResult[Status] =
status match {
case IntAndReason(code, Some(reason)) =>
Status.fromIntAndReason(code, reason)

case IntAndReason(code, None) =>
Status.fromInt(code)
}

def httpMethodToMethod(httpMethod: HttpMethod): Method =
httpMethod match {
case GET =>
Method.GET

case POST =>
Method.POST

case PUT =>
Method.PUT

case DELETE =>
Method.DELETE

case OPTIONS =>
Method.OPTIONS

case PATCH =>
Method.PATCH

case CONNECT =>
Method.CONNECT

case TRACE =>
Method.TRACE

case HEAD =>
Method.HEAD
}

def buildRequest(request: SimpleRequest): IO[Request[IO]] =
buildUri(request.baseUrl, request.endPoint).flatMap { uri =>
val r = Request[IO](
method = httpMethodToMethod(request.method),
uri = uri
).withHeaders(
request.headers.toHttp4sHeaders
)

request.body
.map { b =>
IO(r.withEntity(b))
}
.getOrElse(IO(r))
}

def buildResponse(status: IntAndReason, headers: Map[String, String], body: Option[String]): Response[IO] =
intToStatus(status) match {
case Left(l) =>
l.toHttpResponse[IO](HttpVersion.`HTTP/1.1`)
case Right(code) =>
val response = Response[IO](
status = code
).withHeaders(headers.toHttp4sHeaders)

body
.map { b =>
response.withEntity(b)
}
.getOrElse(response)
}

}

case class IntAndReason(code: Int, reason: Option[String])
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.itv.scalapact.http4s23.impl

import java.util.concurrent.ConcurrentHashMap

import HttpInstances.ClientConfig
import com.itv.scalapact.shared.IPactStubber
import com.itv.scalapact.shared.http.{IScalaPactHttpClient, IScalaPactHttpClientBuilder, SslContextMap}
import com.itv.scalapact.shared.utils.PactLogger

import scala.concurrent.duration.Duration

trait HttpInstances {

// Note that we create a new stubber anytime this implicit is needed (i.e. this is a `def`).
// We need this because implementations of `IPactStubber` might want to have their own state about the server running.
implicit def serverInstance: IPactStubber =
new PactStubber

private val clients: ConcurrentHashMap[ClientConfig, IScalaPactHttpClient] = new ConcurrentHashMap

implicit def httpClientBuilder(implicit sslContextMap: SslContextMap): IScalaPactHttpClientBuilder = {
(clientTimeout: Duration, sslContextName: Option[String], maxTotalConnections: Int) =>
val clientConfig = ClientConfig(clientTimeout, sslContextName, maxTotalConnections, sslContextMap)
PactLogger.debug(s"Checking client cache for config $clientConfig, cache size is ${clients.size}")
clients.computeIfAbsent(
clientConfig,
_ => ScalaPactHttpClient(clientTimeout, sslContextName, maxTotalConnections)
)
}
}

object HttpInstances {
private final case class ClientConfig(
timeout: Duration,
sslContextName: Option[String],
maxTotalConnections: Int,
sslContextMap: SslContextMap
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.itv.scalapact.http4s23.impl

import cats.effect.IO
import org.http4s.{EntityEncoder, MediaType}
import org.http4s.headers.`Content-Type`

final case class JsonString(value: String)

object JsonString {
implicit val entityEncoder: EntityEncoder[IO, JsonString] =
EntityEncoder
.stringEncoder[IO]
.contramap[JsonString](_.value)
.withContentType(`Content-Type`(MediaType.application.json))
}
Loading