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

Fixes #3034 Allow configuring circe support parser #3583

Merged
merged 4 commits into from
Aug 6, 2020
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: 17 additions & 5 deletions circe/src/main/scala/org/http4s/circe/CirceInstances.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import org.http4s.jawn.JawnInstances
import org.typelevel.jawn.ParseException

trait CirceInstances extends JawnInstances {
private val circeSupportParser =
protected val circeSupportParser =
new CirceSupportParser(maxValueSize = None, allowDuplicateKeys = false)

import circeSupportParser.facade

protected def defaultPrinter: Printer = Printer.noSpaces
Expand All @@ -42,7 +43,10 @@ trait CirceInstances extends JawnInstances {
EntityDecoder.collectBinary(m).subflatMap { chunk =>
val bb = ByteBuffer.wrap(chunk.toArray)
if (bb.hasRemaining)
parseByteBuffer(bb).leftMap(circeParseExceptionMessage)
circeSupportParser
.parseFromByteBuffer(bb)
.toEither
.leftMap(e => circeParseExceptionMessage(ParsingFailure(e.getMessage(), e)))
else
Left(jawnEmptyBodyMessage)
}
Expand Down Expand Up @@ -154,7 +158,9 @@ sealed abstract case class CirceInstancesBuilder private[circe] (
CirceInstances.defaultCirceParseError,
jawnParseExceptionMessage: ParseException => DecodeFailure =
JawnInstances.defaultJawnParseExceptionMessage,
jawnEmptyBodyMessage: DecodeFailure = JawnInstances.defaultJawnEmptyBodyMessage
jawnEmptyBodyMessage: DecodeFailure = JawnInstances.defaultJawnEmptyBodyMessage,
circeSupportParser: CirceSupportParser =
new CirceSupportParser(maxValueSize = None, allowDuplicateKeys = false)
) { self =>
def withPrinter(pp: Printer): CirceInstancesBuilder =
this.copy(defaultPrinter = pp)
Expand All @@ -171,23 +177,29 @@ sealed abstract case class CirceInstancesBuilder private[circe] (
def withEmptyBodyMessage(df: DecodeFailure): CirceInstancesBuilder =
this.copy(jawnEmptyBodyMessage = df)

def withCirceSupportParser(csp: CirceSupportParser): CirceInstancesBuilder =
this.copy(circeSupportParser = csp)

protected def copy(
defaultPrinter: Printer = self.defaultPrinter,
jsonDecodeError: (Json, NonEmptyList[DecodingFailure]) => DecodeFailure =
self.jsonDecodeError,
circeParseExceptionMessage: ParsingFailure => DecodeFailure = self.circeParseExceptionMessage,
jawnParseExceptionMessage: ParseException => DecodeFailure = self.jawnParseExceptionMessage,
jawnEmptyBodyMessage: DecodeFailure = self.jawnEmptyBodyMessage
jawnEmptyBodyMessage: DecodeFailure = self.jawnEmptyBodyMessage,
circeSupportParser: CirceSupportParser = self.circeSupportParser
): CirceInstancesBuilder =
new CirceInstancesBuilder(
defaultPrinter,
jsonDecodeError,
circeParseExceptionMessage,
jawnParseExceptionMessage,
jawnEmptyBodyMessage) {}
jawnEmptyBodyMessage,
circeSupportParser) {}

def build: CirceInstances =
new CirceInstances {
override val circeSupportParser: CirceSupportParser = self.circeSupportParser
override val defaultPrinter: Printer = self.defaultPrinter
override val jsonDecodeError: (Json, NonEmptyList[DecodingFailure]) => DecodeFailure =
self.jsonDecodeError
Expand Down
39 changes: 38 additions & 1 deletion circe/src/test/scala/org/http4s/circe/CirceSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.http4s.jawn.JawnDecodeSupportSpec
import org.http4s.laws.discipline.EntityCodecTests
import org.http4s.testing.Http4sLegacyMatchersIO
import org.specs2.specification.core.Fragment
import io.circe.jawn.CirceSupportParser

// Originally based on ArgonautSpec
class CirceSpec extends JawnDecodeSupportSpec[Json] with Http4sLegacyMatchersIO {
Expand Down Expand Up @@ -69,7 +70,6 @@ class CirceSpec extends JawnDecodeSupportSpec[Json] with Http4sLegacyMatchersIO

"json encoder" should {
val json = Json.obj("test" -> Json.fromString("CirceSupport"))

"have json content type" in {
jsonEncoder[IO].headers.get(`Content-Type`) must_== Some(
`Content-Type`(MediaType.application.json))
Expand Down Expand Up @@ -299,6 +299,43 @@ class CirceSpec extends JawnDecodeSupportSpec[Json] with Http4sLegacyMatchersIO
}
}

"CirceInstances.builder" should {
"should successfully decode when parser allows duplicate keys" in {
val circeInstanceAllowingDuplicateKeys = CirceInstances.builder
.withCirceSupportParser(
new CirceSupportParser(maxValueSize = None, allowDuplicateKeys = true))
.build
val req = Request[IO]()
.withEntity("""{"bar": 1, "bar":2}""")
.withContentType(`Content-Type`(MediaType.application.json))

val decoder = circeInstanceAllowingDuplicateKeys.jsonOf[IO, Foo]
val result = decoder.decode(req, true).value.unsafeRunSync

result must beRight.like {
case Foo(2) => ok
}
}
"should should error out when parser does not allow duplicate keys" in {
val circeInstanceNotAllowingDuplicateKeys = CirceInstances.builder
.withCirceSupportParser(
new CirceSupportParser(maxValueSize = None, allowDuplicateKeys = false))
.build
val req = Request[IO]()
.withEntity("""{"bar": 1, "bar":2}""")
.withContentType(`Content-Type`(MediaType.application.json))

val decoder = circeInstanceNotAllowingDuplicateKeys.jsonOf[IO, Foo]
val result = decoder.decode(req, true).value.unsafeRunSync
result must beLeft.like {
case MalformedMessageBodyFailure(
"Invalid JSON",
Some(ParsingFailure("Invalid json, duplicate key name found: bar", _))) =>
ok
}
}
}

"CirceInstances.builder" should {
"handle JSON parsing errors" in {
val req = Request[IO]()
Expand Down