From d129b357a988656da27df75a27fd943ec0881716 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 24 Jun 2020 18:10:57 -0700 Subject: [PATCH 1/4] CacheKey Codec --- build.sbt | 1 + .../mules/http4s/codecs/package.scala | 13 ++ .../mules/http4s/codecs/Arbitraries.scala | 192 +++++++++++++++++- .../mules/http4s/codecs/CodecSpec.scala | 34 +++- 4 files changed, 228 insertions(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index 361455a..9f82342 100644 --- a/build.sbt +++ b/build.sbt @@ -111,6 +111,7 @@ lazy val commonSettings = Seq( "io.chrisdavenport" %% "cats-effect-time" % "0.1.0", "org.specs2" %% "specs2-core" % specs2V % Test, "org.specs2" %% "specs2-scalacheck" % specs2V % Test, + "io.chrisdavenport" %% "cats-scalacheck" % "0.3.0" % Test, "com.codecommit" %% "cats-effect-testing-specs2" % "0.3.0" % Test, "org.http4s" %% "http4s-dsl" % http4sV % Test, ) diff --git a/scodec/src/main/scala/io/chrisdavenport/mules/http4s/codecs/package.scala b/scodec/src/main/scala/io/chrisdavenport/mules/http4s/codecs/package.scala index bd258f9..cff1a7a 100644 --- a/scodec/src/main/scala/io/chrisdavenport/mules/http4s/codecs/package.scala +++ b/scodec/src/main/scala/io/chrisdavenport/mules/http4s/codecs/package.scala @@ -5,6 +5,7 @@ import _root_.scodec.interop.cats._ import _root_.scodec._ import _root_.scodec.codecs._ import org.http4s._ +import java.nio.charset.StandardCharsets package object codecs { private[codecs] val statusCodec : Codec[Status] = int16.exmap( @@ -47,6 +48,18 @@ package object codecs { date => Attempt.successful(date.epochSecond) ) + private[codecs] val method: Codec[Method] = cstring.exmapc(s => + Attempt.fromEither(Method.fromString(s).leftMap(p => Err.apply(p.details))) + )(m => Attempt.successful(m.name)) + + private[codecs] val uri : Codec[Uri] = variableSizeBytesLong(int64, string(StandardCharsets.UTF_8)) + .withToString(s"string64(${StandardCharsets.UTF_8.displayName()})") + .exmapc( + s => Attempt.fromEither(Uri.fromString(s).leftMap(p => Err.apply(p.details))) + )(uri => Attempt.successful(uri.renderString)) + + val keyTupleCodec : Codec[(Method, Uri)] = method ~ uri + val cachedResponseCodec : Codec[CachedResponse] = (statusCodec :: httpVersionCodec :: headersCodec :: bytes).as[CachedResponse] diff --git a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala index d671ef7..06d8abf 100644 --- a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala +++ b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala @@ -1,22 +1,27 @@ package io.chrisdavenport.mules.http4s.codecs import io.chrisdavenport.mules.http4s._ -import org.scalacheck._ import _root_.scodec.bits.ByteVector import org.http4s._ +import org.http4s.implicits._ import org.http4s.util.CaseInsensitiveString import java.time._ +import java.util.Locale +import cats._ +import cats.implicits._ +import org.scalacheck._ +import org.scalacheck.cats.implicits._ trait Arbitraries { - implicit val arbitraryByteVector: Arbitrary[ByteVector] = + implicit lazy val arbitraryByteVector: Arbitrary[ByteVector] = Arbitrary(Gen.containerOf[Array, Byte](Arbitrary.arbitrary[Byte]).map(ByteVector(_))) - implicit val arbStatus: Arbitrary[Status] = + implicit lazy val arbStatus: Arbitrary[Status] = Arbitrary{ Gen.choose(100, 599).map(Status.fromInt(_).fold(throw _, identity)) // Safe because we are in valid range } - implicit val arbHttpVersion : Arbitrary[HttpVersion] = Arbitrary { + implicit lazy val arbHttpVersion : Arbitrary[HttpVersion] = Arbitrary { for { major <- Gen.choose(0, 9) minor <- Gen.choose(0, 9) @@ -54,7 +59,7 @@ trait Arbitraries { val genFieldValue: Gen[String] = genFieldContent - implicit val http4sTestingArbitraryForRawHeader: Arbitrary[Header.Raw] = + implicit lazy val http4sTestingArbitraryForRawHeader: Arbitrary[Header.Raw] = Arbitrary { for { token <- genToken @@ -62,9 +67,9 @@ trait Arbitraries { } yield Header.Raw(CaseInsensitiveString(token), value) } - implicit val headers: Arbitrary[Headers] = Arbitrary(Gen.listOf(Arbitrary.arbitrary[Header.Raw]).map(Headers(_))) + implicit lazy val headers: Arbitrary[Headers] = Arbitrary(Gen.listOf(Arbitrary.arbitrary[Header.Raw]).map(Headers(_))) - implicit val arbCachedResponse: Arbitrary[CachedResponse] = Arbitrary( + implicit lazy val arbCachedResponse: Arbitrary[CachedResponse] = Arbitrary( for { status <- Arbitrary.arbitrary[Status] version <- Arbitrary.arbitrary[HttpVersion] @@ -85,15 +90,182 @@ trait Arbitraries { Gen.choose[Long](min, max).map(HttpDate.unsafeFromEpochSecond) } - implicit val arbHttpDate: Arbitrary[HttpDate] = Arbitrary(genHttpDate) + implicit lazy val arbHttpDate: Arbitrary[HttpDate] = Arbitrary(genHttpDate) - implicit val arbCacheItem = Arbitrary( + implicit lazy val arbCacheItem = Arbitrary( for { created <- Arbitrary.arbitrary[HttpDate] expires <- Arbitrary.arbitrary[Option[HttpDate]] cachedResponse <- Arbitrary.arbitrary[CachedResponse] } yield CacheItem(created, expires, cachedResponse) ) + + implicit lazy val arbMethod = Arbitrary( + Gen.oneOf(Method.all) + ) + + // https://tools.ietf.org/html/rfc2234#section-6 + val genHexDigit: Gen[Char] = Gen.oneOf( + List('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')) + + // https://tools.ietf.org/html/rfc3986#appendix-A + implicit lazy val http4sTestingArbitraryForIpv4Address: Arbitrary[Uri.Ipv4Address] = Arbitrary { + for { + a <- Arbitrary.arbitrary[Byte] + b <- Arbitrary.arbitrary[Byte] + c <- Arbitrary.arbitrary[Byte] + d <- Arbitrary.arbitrary[Byte] + } yield Uri.Ipv4Address(a, b, c, d) + } + + implicit lazy val http4sTestingCogenForIpv4Address: Cogen[Uri.Ipv4Address] = + Cogen[(Byte, Byte, Byte, Byte)].contramap(ipv4 => (ipv4.a, ipv4.b, ipv4.c, ipv4.d)) + + // https://tools.ietf.org/html/rfc3986#appendix-A + implicit lazy val http4sTestingArbitraryForIpv6Address: Arbitrary[Uri.Ipv6Address] = Arbitrary { + for { + a <- Arbitrary.arbitrary[Short] + b <- Arbitrary.arbitrary[Short] + c <- Arbitrary.arbitrary[Short] + d <- Arbitrary.arbitrary[Short] + e <- Arbitrary.arbitrary[Short] + f <- Arbitrary.arbitrary[Short] + g <- Arbitrary.arbitrary[Short] + h <- Arbitrary.arbitrary[Short] + } yield Uri.Ipv6Address(a, b, c, d, e, f, g, h) + } + + implicit lazy val http4sTestingCogenForIpv6Address: Cogen[Uri.Ipv6Address] = + Cogen[(Short, Short, Short, Short, Short, Short, Short, Short)] + .contramap(ipv6 => (ipv6.a, ipv6.b, ipv6.c, ipv6.d, ipv6.e, ipv6.f, ipv6.g, ipv6.h)) + + implicit lazy val http4sTestingArbitraryForUriHost: Arbitrary[Uri.Host] = Arbitrary { + val genRegName = + Gen.listOf(Gen.oneOf(genUnreserved, genPctEncoded, genSubDelims)).map(rn => Uri.RegName(rn.mkString)) + Gen.oneOf(Arbitrary.arbitrary[Uri.Ipv4Address], Arbitrary.arbitrary[Uri.Ipv6Address], genRegName) + } + + implicit lazy val http4sTestingArbitraryForUserInfo: Arbitrary[Uri.UserInfo] = + Arbitrary( + for { + username <- Arbitrary.arbitrary[String] + password <- Arbitrary.arbitrary[Option[String]] + } yield Uri.UserInfo(username, password) + ) + + implicit lazy val http4sTestingCogenForUserInfo: Cogen[Uri.UserInfo] = + Cogen.tuple2[String, Option[String]].contramap(u => (u.username, u.password)) + + implicit lazy val http4sTestingArbitraryForAuthority: Arbitrary[Uri.Authority] = Arbitrary { + for { + maybeUserInfo <- Arbitrary.arbitrary[Option[Uri.UserInfo]] + host <- http4sTestingArbitraryForUriHost.arbitrary + maybePort <- Gen.option(Gen.posNum[Int].suchThat(port => port >= 0 && port <= 65536)) + } yield Uri.Authority(maybeUserInfo, host, maybePort) + } + + val genPctEncoded: Gen[String] = + (Gen.const("%"), genHexDigit.map(_.toString), genHexDigit.map(_.toString)).mapN(_ |+| _ |+| _) + val genUnreserved: Gen[Char] = + Gen.oneOf(Gen.alphaChar, Gen.numChar, Gen.const('-'), Gen.const('.'), Gen.const('_'), Gen.const('~')) + val genSubDelims: Gen[Char] = Gen.oneOf(List('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=')) + + implicit lazy val http4sTestingArbitraryForScheme: Arbitrary[Uri.Scheme] = Arbitrary { + Gen.frequency( + 5 -> Uri.Scheme.http, + 5 -> Uri.Scheme.https, + 1 -> scheme"HTTP", + 1 -> scheme"HTTPS", + 3 -> (for { + head <- Gen.alphaChar + tail <- Gen.listOf( + Gen.frequency( + 36 -> Gen.alphaNumChar, + 1 -> Gen.const('+'), + 1 -> Gen.const('-'), + 1 -> Gen.const('.') + ) + ) + } yield HttpCodec[Uri.Scheme].parseOrThrow(tail.mkString(head.toString, "", ""))) + ) + } + + implicit lazy val http4sTestingCogenForScheme: Cogen[Uri.Scheme] = + Cogen[String].contramap(_.value.toLowerCase(Locale.ROOT)) + + implicit lazy val http4sTestingArbitraryForTransferCoding: Arbitrary[TransferCoding] = Arbitrary { + Gen.oneOf( + TransferCoding.chunked, + TransferCoding.compress, + TransferCoding.deflate, + TransferCoding.gzip, + TransferCoding.identity) + } + + implicit lazy val http4sTestingCogenForTransferCoding: Cogen[TransferCoding] = + Cogen[String].contramap(_.coding.toLowerCase(Locale.ROOT)) + + implicit lazy val http4sTestingCogenForPath: Cogen[Uri.Path] = + Cogen[String].contramap(identity) + + implicit lazy val http4sTestingAbitraryForPath: Arbitrary[Uri.Path] = Arbitrary { + val genSegmentNzNc = + Gen.nonEmptyListOf(Gen.oneOf(genUnreserved, genPctEncoded, genSubDelims, Gen.const("@"))).map(_.mkString) + val genPChar = Gen.oneOf(genUnreserved, genPctEncoded, genSubDelims, Gen.const(":"), Gen.const("@")) + val genSegmentNz = Gen.nonEmptyListOf(genPChar).map(_.mkString) + val genSegment = Gen.listOf(genPChar).map(_.mkString) + val genPathEmpty = Gen.const("") + val genPathAbEmpty = Gen.listOf(Gen.const("/") |+| genSegment).map(_.mkString) + val genPathRootless = genSegmentNz |+| genPathAbEmpty + val genPathNoScheme = genSegmentNzNc |+| genPathAbEmpty + val genPathAbsolute = Gen.const("/") |+| Gen.oneOf(genPathRootless, Gen.const(Monoid[String].empty)) + + Gen.oneOf(genPathAbEmpty, genPathAbsolute, genPathNoScheme, genPathRootless, genPathEmpty).map( + identity)//Uri.Path.fromString) + } + + + + implicit lazy val http4sTestingArbitraryForQueryParam: Arbitrary[(String, Option[String])] = + Arbitrary { + Gen.frequency( + 5 -> { + for { + k <- Arbitrary.arbitrary[String] + v <- Arbitrary.arbitrary[Option[String]] + } yield (k, v) + }, + 2 -> Gen.const(("foo" -> Some("bar"))) // Want some repeats + ) + } + + implicit lazy val http4sTestingArbitraryForQuery: Arbitrary[Query] = + Arbitrary { + for { + n <- Gen.size + vs <- Gen.containerOfN[Vector, (String, Option[String])]( + n % 8, + http4sTestingArbitraryForQueryParam.arbitrary) + } yield Query(vs: _*) + } + + /** https://tools.ietf.org/html/rfc3986 */ + implicit lazy val http4sTestingArbitraryForUri: Arbitrary[Uri] = Arbitrary { + val genPChar = Gen.oneOf(genUnreserved, genPctEncoded, genSubDelims, Gen.const(":"), Gen.const("@")) + val genScheme = Gen.oneOf(Uri.Scheme.http, Uri.Scheme.https) + + val genFragment: Gen[Uri.Fragment] = + Gen.listOf(Gen.oneOf(genPChar, Gen.const("/"), Gen.const("?"))).map(_.mkString) + + for { + scheme <- Gen.option(genScheme) + authority <- Gen.option(http4sTestingArbitraryForAuthority.arbitrary) + path <- http4sTestingAbitraryForPath.arbitrary + query <- http4sTestingArbitraryForQuery.arbitrary + fragment <- Gen.option(genFragment) + } yield Uri(scheme, authority, path, query, fragment) + } + } -object Arbitraries extends Arbitraries \ No newline at end of file +// object Arbitraries extends Arbitraries \ No newline at end of file diff --git a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala index 158ac09..e0f4915 100644 --- a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala +++ b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala @@ -1,9 +1,11 @@ package io.chrisdavenport.mules.http4s.codecs import io.chrisdavenport.mules.http4s._ -import Arbitraries._ +import org.http4s.{Method, Uri} +import org.http4s.implicits._ +// import Arbitraries._ -class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCheck { +class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCheck with Arbitraries { "CachedResponse Codec" should { "round trip succesfully" in prop{ cached: CachedResponse => @@ -28,4 +30,32 @@ class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCh } } } + + "Cache Key Codec" should { + + "round-trip a known uri" in { + val test = (Method.GET, uri"https://www.google.com") + val encoded = keyTupleCodec.encode(test) + val decoded = encoded.flatMap(bv => keyTupleCodec.decode(bv)) + decoded.toEither.map(_.value) must beRight.like{ + case a => (a._1 must_=== test._1) and (a._2 must_=== test._2) + } + } + + "round trip succesfully" in prop { cacheKey: (Method, Uri) => + // Gave up after only 45 passed tests. 501 tests were discarded. + // Uri.fromString(cacheKey._2.renderString).map(_ == cacheKey._2).getOrElse(false) ==> { + val encoded = keyTupleCodec.encode(cacheKey) + val decoded = encoded.flatMap(bv => keyTupleCodec.decode(bv)) + + val checkTraversal = Uri.fromString(cacheKey._2.renderString).map(_ == cacheKey._2).getOrElse(false) + if (checkTraversal) { + decoded.toEither.map(_.value) must beRight.like{ + case a => (a._1 must_=== cacheKey._1) and (a._2 must_=== cacheKey._2) + } + } else { + ok + } + } + } } \ No newline at end of file From e60daad21d9990c9e5b677c026db812ea088ed39 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 24 Jun 2020 18:21:28 -0700 Subject: [PATCH 2/4] Bring Back Object --- .../io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala | 2 +- .../io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala index 06d8abf..d7b7779 100644 --- a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala +++ b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/Arbitraries.scala @@ -268,4 +268,4 @@ trait Arbitraries { } -// object Arbitraries extends Arbitraries \ No newline at end of file +object Arbitraries extends Arbitraries \ No newline at end of file diff --git a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala index e0f4915..bc9a946 100644 --- a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala +++ b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala @@ -3,9 +3,9 @@ package io.chrisdavenport.mules.http4s.codecs import io.chrisdavenport.mules.http4s._ import org.http4s.{Method, Uri} import org.http4s.implicits._ -// import Arbitraries._ +import Arbitraries._ -class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCheck with Arbitraries { +class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCheck { "CachedResponse Codec" should { "round trip succesfully" in prop{ cached: CachedResponse => From a5b589591d87b14294287ca759a5f72720aa314e Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 24 Jun 2020 18:30:00 -0700 Subject: [PATCH 3/4] Better Test --- .../mules/http4s/codecs/CodecSpec.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala index bc9a946..b960432 100644 --- a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala +++ b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala @@ -42,20 +42,18 @@ class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCh } } - "round trip succesfully" in prop { cacheKey: (Method, Uri) => + "round trip succesfully" in prop { cacheKey: (Method, Uri) => Uri.fromString(cacheKey._2.renderString).isRight ==> { // Gave up after only 45 passed tests. 501 tests were discarded. // Uri.fromString(cacheKey._2.renderString).map(_ == cacheKey._2).getOrElse(false) ==> { val encoded = keyTupleCodec.encode(cacheKey) val decoded = encoded.flatMap(bv => keyTupleCodec.decode(bv)) val checkTraversal = Uri.fromString(cacheKey._2.renderString).map(_ == cacheKey._2).getOrElse(false) - if (checkTraversal) { - decoded.toEither.map(_.value) must beRight.like{ - case a => (a._1 must_=== cacheKey._1) and (a._2 must_=== cacheKey._2) - } - } else { - ok + + decoded.toEither.map(_.value) must beRight.like{ + case a if (checkTraversal) => (a._1 must_=== cacheKey._1) and (a._2 must_=== cacheKey._2) + case a => (a._1 must_=== cacheKey._1) } - } + }} } } \ No newline at end of file From 9121b5a3bd93284accf4010d4c6cbdecc0cbff01 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 24 Jun 2020 18:34:30 -0700 Subject: [PATCH 4/4] Better Unit Test --- .../scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala index b960432..254b0e4 100644 --- a/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala +++ b/scodec/src/test/scala/io/chrisdavenport/mules/http4s/codecs/CodecSpec.scala @@ -34,7 +34,7 @@ class CodecSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCh "Cache Key Codec" should { "round-trip a known uri" in { - val test = (Method.GET, uri"https://www.google.com") + val test = (Method.GET, uri"https://chrisdavenport.io:4553/foo/bar/baz?implicit=yes") val encoded = keyTupleCodec.encode(test) val decoded = encoded.flatMap(bv => keyTupleCodec.decode(bv)) decoded.toEither.map(_.value) must beRight.like{