From 52f2f497a2912283b80955978c6efb9b25edff28 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 9 Nov 2023 05:03:36 +0100 Subject: [PATCH 1/2] Improve `RichTextCodec` decoding performance Error messages are the same per instance of `CharIn`. So we compute them only once. --- .../src/main/scala/zio/http/codec/RichTextCodec.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala index aedc47f8f3..27e489500b 100644 --- a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala +++ b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala @@ -154,7 +154,9 @@ sealed trait RichTextCodec[A] { self => } object RichTextCodec { private[codec] case object Empty extends RichTextCodec[Unit] - private[codec] final case class CharIn(set: BitSet) extends RichTextCodec[Char] + private[codec] final case class CharIn(set: BitSet) extends RichTextCodec[Char] { + lazy val errorMessage = s"Not found: ${set.toArray.map(_.toChar).mkString}" + } private[codec] final case class TransformOrFail[A, B]( codec: RichTextCodec[A], to: A => Either[String, B], @@ -162,7 +164,7 @@ object RichTextCodec { ) extends RichTextCodec[B] private[codec] final case class Alt[A, B](left: RichTextCodec[A], right: RichTextCodec[B]) extends RichTextCodec[Either[A, B]] - private[codec] final case class Lazy[A](codec0: () => RichTextCodec[A]) extends RichTextCodec[A] { + private[codec] final case class Lazy[A](codec0: () => RichTextCodec[A]) extends RichTextCodec[A] { lazy val codec: RichTextCodec[A] = codec0() } private[codec] final case class Zip[A, B, C]( @@ -528,9 +530,9 @@ object RichTextCodec { case Empty => Right((value, ())) - case CharIn(bitset) => + case self @ CharIn(bitset) => if (value.length == 0 || !bitset.contains(value.charAt(0).toInt)) - Left(s"Not found: ${bitset.toArray.map(_.toChar).mkString}") + Left(self.errorMessage) else Right((value.subSequence(1, value.length), value.charAt(0))) From cc451fc174e839883737a9a8388fd1afbafa8a22 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:47:15 +0100 Subject: [PATCH 2/2] Add withError to RichTextCodec; Improve ContentHeader parsing --- zio-http/src/main/scala/zio/http/Header.scala | 15 ++++----- .../scala/zio/http/codec/RichTextCodec.scala | 33 ++++++++++++++++--- .../zio/http/codec/RichTextCodecSpec.scala | 4 +++ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Header.scala b/zio-http/src/main/scala/zio/http/Header.scala index e47b3476a2..1e3a594210 100644 --- a/zio-http/src/main/scala/zio/http/Header.scala +++ b/zio-http/src/main/scala/zio/http/Header.scala @@ -30,6 +30,7 @@ import scala.util.{Either, Failure, Success, Try} import zio._ import zio.http.codec.RichTextCodec +import zio.http.endpoint.openapi.OpenAPI.SecurityScheme.Http import zio.http.internal.DateEncoding sealed trait Header { @@ -2480,16 +2481,12 @@ object Header { private val codec: RichTextCodec[ContentType] = { // char `.` according to BNF not allowed as `token`, but here tolerated - val token = RichTextCodec.filter(_ => true).validate("not a token") { - case ' ' | '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '"' | '/' | '[' | ']' | '?' | '=' => false - case _ => true - } - val tokenQuoted = RichTextCodec.filter(_ => true).validate("not a quoted token") { - case ' ' | '"' => false - case _ => true - } + val token = RichTextCodec.charsNot(' ', '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=') + + val tokenQuoted = RichTextCodec.charsNot(' ', '"') + val type1 = RichTextCodec.string.collectOrFail("unsupported main type") { - case value if MediaType.mainTypeMap.get(value).isDefined => value + case value if MediaType.mainTypeMap.contains(value) => value } val type1x = (RichTextCodec.literalCI("x-") ~ token.repeat.string).transform[String](in => s"${in._1}${in._2}")(in => ("x-", s"${in.substring(2)}")) val codecType1 = (type1 | type1x).transform[String](_.merge) { diff --git a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala index 27e489500b..df2d0b53b1 100644 --- a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala +++ b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala @@ -21,7 +21,6 @@ import java.lang.Integer.parseInt import scala.annotation.tailrec import scala.collection.immutable.BitSet -import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.{Chunk, NonEmptyChunk} /** @@ -108,6 +107,14 @@ sealed trait RichTextCodec[A] { self => */ final def encode(value: A): Either[String, String] = RichTextCodec.encode(value, self) + /** + * This method is Right biased merge + */ + final def merge[B](implicit ev: A <:< Either[B, B]): RichTextCodec[B] = { + val codec = self.asInstanceOf[RichTextCodec[Either[B, B]]] + codec.transform[B](_.merge)(Right(_)) + } + final def optional(default: A): RichTextCodec[Option[A]] = self.transform[Option[A]](a => Some(a))(_.fold(default)(identity)) @@ -115,10 +122,10 @@ sealed trait RichTextCodec[A] { self => ((self ~ repeat).transform[NonEmptyChunk[A]](t => NonEmptyChunk(t._1, t._2: _*))(c => (c.head, c.tail), ) | RichTextCodec.empty.as(Chunk.empty[A])) - .transform[Chunk[A]](_ match { + .transform[Chunk[A]] { case Left(nonEmpty) => nonEmpty case Right(maybeEmpty) => maybeEmpty - })(c => c.nonEmptyOrElse[Either[NonEmptyChunk[A], Chunk[A]]](Right(c))(Left(_))) + }(c => c.nonEmptyOrElse[Either[NonEmptyChunk[A], Chunk[A]]](Right(c))(Left(_))) final def singleton: RichTextCodec[NonEmptyChunk[A]] = self.transform(a => NonEmptyChunk(a))(_.head) @@ -151,11 +158,15 @@ sealed trait RichTextCodec[A] { self => case x if p(x) => x } + final def withError(errorMessage: String): RichTextCodec[A] = + (self | RichTextCodec.fail[A](errorMessage)).merge + } object RichTextCodec { private[codec] case object Empty extends RichTextCodec[Unit] private[codec] final case class CharIn(set: BitSet) extends RichTextCodec[Char] { - lazy val errorMessage = s"Not found: ${set.toArray.map(_.toChar).mkString}" + val errorMessage: Left[String, Nothing] = + Left(s"Expected, but did not find: ${this.describe}") } private[codec] final case class TransformOrFail[A, B]( codec: RichTextCodec[A], @@ -190,6 +201,12 @@ object RichTextCodec { */ def char(c: Char): RichTextCodec[Char] = CharIn(BitSet(c.toInt)) + def chars(cs: Char*): RichTextCodec[Char] = + CharIn(BitSet(cs.map(_.toInt): _*)) + + def charsNot(cs: Char*): RichTextCodec[Char] = + filter(c => !cs.contains(c)) + /** * A codec that describes a digit character. */ @@ -202,6 +219,9 @@ object RichTextCodec { */ val empty: RichTextCodec[Unit] = Empty + def fail[A](message: String): RichTextCodec[A] = + empty.transformOrFail(_ => Left(message))(_ => Left(message)) + /** * Defines a new codec for a single character based on the specified * predicate. @@ -209,6 +229,9 @@ object RichTextCodec { def filter(pred: Char => Boolean): RichTextCodec[Char] = CharIn(BitSet((Char.MinValue to Char.MaxValue).filter(pred).map(_.toInt): _*)) + def filterOrFail(pred: Char => Boolean)(failure: String): RichTextCodec[Char] = + filter(pred).collectOrFail(failure) { case c => c } + /** * A codec that describes a letter character. */ @@ -532,7 +555,7 @@ object RichTextCodec { case self @ CharIn(bitset) => if (value.length == 0 || !bitset.contains(value.charAt(0).toInt)) - Left(self.errorMessage) + self.errorMessage else Right((value.subSequence(1, value.length), value.charAt(0))) diff --git a/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala b/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala index 50ed2a74d6..5ac325ae9f 100644 --- a/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala +++ b/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala @@ -250,6 +250,10 @@ object RichTextCodecSpec extends ZIOHttpSpec { assertTrue(success(123) == codec.decode("123--")) && assertTrue(codec.decode("4123").isLeft) }, + test("With error message") { + val codec = RichTextCodec.literal("123").withError("Not 123") + assertTrue(codec.decode("678") == Left("(Expected, but did not find: Paragraph(Code(“1”,Inline)), Not 123)")) + }, ), ) }