From 64e128c76c52ed948d3f1ea4eb016e9c726201fd Mon Sep 17 00:00:00 2001 From: Thomas Hoefer Date: Fri, 8 Sep 2023 21:56:56 +0200 Subject: [PATCH 1/6] added cookie helpers --- docs/dsl/cookies.md | 16 ++++++++- .../src/main/scala/zio/http/Request.scala | 35 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/dsl/cookies.md b/docs/dsl/cookies.md index 3813e8f533..de1677c397 100644 --- a/docs/dsl/cookies.md +++ b/docs/dsl/cookies.md @@ -129,7 +129,21 @@ It updates the response header `Set-Cookie` as ```Set-Cookie: = handler { (req: Request) => + val cookieContent = req.cookie("sessionId").map(_.content) + Response.text(s"cookie content: $cookieContent") + } + ) +``` + +## Getting Cookie from Header + +In HTTP requests, cookies are stored in the `cookie` header. ```scala mdoc private val app3 = diff --git a/zio-http/src/main/scala/zio/http/Request.scala b/zio-http/src/main/scala/zio/http/Request.scala index 397ccf87a9..376e9b9521 100644 --- a/zio-http/src/main/scala/zio/http/Request.scala +++ b/zio-http/src/main/scala/zio/http/Request.scala @@ -19,7 +19,7 @@ package zio.http import java.net.InetAddress import zio.stacktracer.TracingImplicits.disableAutoTrace -import zio.{Trace, ZIO} +import zio.{NonEmptyChunk, Trace, ZIO} import zio.http.internal.HeaderOps @@ -106,6 +106,39 @@ final case class Request( */ def unnest(prefix: Path): Request = copy(url = self.url.copy(path = self.url.path.unnest(prefix))) + + /** + * Returns the cookie with the given name if it exists. + */ + def cookie(name: String): Option[Cookie] = + cookies.flatMap(_.filter(_.name == name).headOption) + + /** + * Uses the cookie with the given name if it exists and runs `f` afterwards. + */ + def cookieWith[R, A](name: String)(f: Cookie => ZIO[R, Throwable, A])(implicit trace: Trace): ZIO[R, Throwable, A] = + cookieWithOrFailImpl(name)(identity)(f) + + /** + * Uses the cookie with the given name if it exists and runs `f` afterwards. + * + * Also, you can replace a `NoSuchElementException` from an absent cookie with + * `E`. + */ + def cookieWithOrFail[R, E, A](name: String)(e: E)(f: Cookie => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] = + cookieWithOrFailImpl(name)(_ => e)(f) + + private def cookieWithOrFailImpl[R, E, A](name: String)(e: Throwable => E)(f: Cookie => ZIO[R, E, A])(implicit + trace: Trace, + ): ZIO[R, E, A] = + ZIO.getOrFailWith(e(new java.util.NoSuchElementException(s"cookie doesn't exist: $name")))(cookie(name)).flatMap(f) + + /** + * Returns all cookies from the request. + */ + def cookies: Option[NonEmptyChunk[Cookie]] = + header(Header.Cookie).map(_.value) + } object Request { From 3179ba6c1d0bb354bedaf51d0e8470ac23123b02 Mon Sep 17 00:00:00 2001 From: Thomas Hoefer Date: Mon, 25 Sep 2023 13:31:37 +0200 Subject: [PATCH 2/6] a few changes --- zio-http/src/main/scala/zio/http/Request.scala | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Request.scala b/zio-http/src/main/scala/zio/http/Request.scala index 376e9b9521..6ad206caae 100644 --- a/zio-http/src/main/scala/zio/http/Request.scala +++ b/zio-http/src/main/scala/zio/http/Request.scala @@ -22,6 +22,7 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.{NonEmptyChunk, Trace, ZIO} import zio.http.internal.HeaderOps +import zio.Chunk final case class Request( version: Version = Version.Default, @@ -111,12 +112,14 @@ final case class Request( * Returns the cookie with the given name if it exists. */ def cookie(name: String): Option[Cookie] = - cookies.flatMap(_.filter(_.name == name).headOption) + cookies.find(_.name == name) /** * Uses the cookie with the given name if it exists and runs `f` afterwards. */ - def cookieWith[R, A](name: String)(f: Cookie => ZIO[R, Throwable, A])(implicit trace: Trace): ZIO[R, Throwable, A] = + def cookieWithZIO[R, A](name: String)(f: Cookie => ZIO[R, Throwable, A])(implicit + trace: Trace, + ): ZIO[R, Throwable, A] = cookieWithOrFailImpl(name)(identity)(f) /** @@ -125,8 +128,10 @@ final case class Request( * Also, you can replace a `NoSuchElementException` from an absent cookie with * `E`. */ - def cookieWithOrFail[R, E, A](name: String)(e: E)(f: Cookie => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] = - cookieWithOrFailImpl(name)(_ => e)(f) + def cookieWithOrFail[R, E, A](name: String)(missingCookieError: E)(f: Cookie => ZIO[R, E, A])(implicit + trace: Trace, + ): ZIO[R, E, A] = + cookieWithOrFailImpl(name)(_ => missingCookieError)(f) private def cookieWithOrFailImpl[R, E, A](name: String)(e: Throwable => E)(f: Cookie => ZIO[R, E, A])(implicit trace: Trace, @@ -136,8 +141,8 @@ final case class Request( /** * Returns all cookies from the request. */ - def cookies: Option[NonEmptyChunk[Cookie]] = - header(Header.Cookie).map(_.value) + def cookies: Chunk[Cookie] = + header(Header.Cookie).fold(Chunk.empty[Cookie])(_.value.toChunk) } From 8c99c0a1735dcc1e9903bfa9ef2dd7a9b0dd5d12 Mon Sep 17 00:00:00 2001 From: Thomas Hoefer Date: Mon, 25 Sep 2023 15:47:58 +0200 Subject: [PATCH 3/6] fix conflicts --- zio-http/src/main/scala/zio/http/Header.scala | 52 ++++++++----------- .../scala/zio/http/codec/RichTextCodec.scala | 2 +- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Header.scala b/zio-http/src/main/scala/zio/http/Header.scala index 96df8c62d3..bfd2bf6b9d 100644 --- a/zio-http/src/main/scala/zio/http/Header.scala +++ b/zio-http/src/main/scala/zio/http/Header.scala @@ -2491,14 +2491,11 @@ object Header { val type1 = RichTextCodec.string.collectOrFail("unsupported main type") { case value if MediaType.mainTypeMap.get(value).isDefined => 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, - { - case x if x.startsWith("x-") => Right(x) - case x => Left(x) - }, - ) + 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) { + case x if x.startsWith("x-") => Right(x) + case x => Left(x) + } val codecType2 = token.repeat.string val codecType = (codecType1 <~ RichTextCodec.char('/').const('/')) ~ codecType2 val attribute = token.repeat.string @@ -2508,32 +2505,27 @@ object Header { val param = (( RichTextCodec.char(';').const(';') ~> - (RichTextCodec.whitespaceChar.repeat | RichTextCodec.empty).transform[Char](_ => ' ', _ => Left(Chunk(()))).const(' ') ~> + (RichTextCodec.whitespaceChar.repeat | RichTextCodec.empty).transform[Char](_ => ' ')(_ => Left(Chunk(()))).const(' ') ~> attribute <~ RichTextCodec.char('=').const('=') ) ~ value) - .transformOrFailLeft[ContentType.Parameter]( - in => ContentType.Parameter.fromCodec(in), - in => in.toCodec, - ) + .transformOrFailLeft[ContentType.Parameter](in => ContentType.Parameter.fromCodec(in))(in => in.toCodec) val params = param.repeat - (codecType ~ params).transform[ContentType]( - { case (mainType, subType, params) => - ContentType( - MediaType.forContentType(s"$mainType/$subType").get, - params.collect { case p if p.key == ContentType.Parameter.Boundary.name => zio.http.Boundary(p.value) }.headOption, - params.collect { case p if p.key == ContentType.Parameter.Charset.name => java.nio.charset.Charset.forName(p.value) }.headOption, - ) - }, - in => - ( - in.mediaType.mainType, - in.mediaType.subType, - Chunk( - in.charset.map(in => Parameter.Charset(Parameter.Payload(Parameter.Charset.name, in, false))), - in.boundary.map(in => Parameter.Boundary(Parameter.Payload(Parameter.Boundary.name, in, false))), - ).flatten, - ), + (codecType ~ params).transform[ContentType] { case (mainType, subType, params) => + ContentType( + MediaType.forContentType(s"$mainType/$subType").get, + params.collect { case p if p.key == ContentType.Parameter.Boundary.name => zio.http.Boundary(p.value) }.headOption, + params.collect { case p if p.key == ContentType.Parameter.Charset.name => java.nio.charset.Charset.forName(p.value) }.headOption, + ) + }(in => + ( + in.mediaType.mainType, + in.mediaType.subType, + Chunk( + in.charset.map(in => Parameter.Charset(Parameter.Payload(Parameter.Charset.name, in, false))), + in.boundary.map(in => Parameter.Boundary(Parameter.Payload(Parameter.Boundary.name, in, false))), + ).flatten, + ), ) } 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 6acbb08091..aedc47f8f3 100644 --- a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala +++ b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala @@ -34,7 +34,7 @@ import zio.{Chunk, NonEmptyChunk} sealed trait RichTextCodec[A] { self => final def string(implicit ev: A =:= Chunk[Char]): RichTextCodec[String] = - self.asType[Chunk[Char]].transform(_.mkString, a => Chunk(a.toList: _*)) + self.asType[Chunk[Char]].transform(_.mkString)(a => Chunk(a.toList: _*)) /** * Returns a new codec that is the sequential composition of this codec and From 854557c7ca867076caf98c4a496b38177308e30a Mon Sep 17 00:00:00 2001 From: Thomas Hoefer Date: Mon, 25 Sep 2023 15:55:25 +0200 Subject: [PATCH 4/6] format --- zio-http/src/main/scala/zio/http/Request.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Request.scala b/zio-http/src/main/scala/zio/http/Request.scala index eb3038c4c2..a3e6694bd3 100644 --- a/zio-http/src/main/scala/zio/http/Request.scala +++ b/zio-http/src/main/scala/zio/http/Request.scala @@ -19,10 +19,9 @@ package zio.http import java.net.InetAddress import zio.stacktracer.TracingImplicits.disableAutoTrace -import zio.{NonEmptyChunk, Trace, ZIO} +import zio.{Chunk, Trace, ZIO} import zio.http.internal.HeaderOps -import zio.Chunk final case class Request( version: Version = Version.Default, From b85d6352355ec3a079f97c14d3c7800218ea8948 Mon Sep 17 00:00:00 2001 From: Thomas Hoefer Date: Tue, 26 Sep 2023 16:06:50 +0200 Subject: [PATCH 5/6] improvements --- .../src/main/scala/zio/http/Request.scala | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Request.scala b/zio-http/src/main/scala/zio/http/Request.scala index a3e6694bd3..fefd0150f3 100644 --- a/zio-http/src/main/scala/zio/http/Request.scala +++ b/zio-http/src/main/scala/zio/http/Request.scala @@ -114,28 +114,33 @@ final case class Request( cookies.find(_.name == name) /** - * Uses the cookie with the given name if it exists and runs `f` afterwards. + * Uses the cookie with the given name if it exists and runs `f` with the + * cookie afterwards. */ def cookieWithZIO[R, A](name: String)(f: Cookie => ZIO[R, Throwable, A])(implicit trace: Trace, ): ZIO[R, Throwable, A] = - cookieWithOrFailImpl(name)(identity)(f) + cookieWithOrFailImpl[R, Throwable, A](name)(new java.util.NoSuchElementException(s"cookie doesn't exist: $name"))(f) /** - * Uses the cookie with the given name if it exists and runs `f` afterwards. + * Uses the cookie with the given name if it exists and runs `f` with the + * cookie afterwards. * - * Also, you can replace a `NoSuchElementException` from an absent cookie with - * `E`. + * Also, you can set a custom failure value from an absent cookie with `E`. */ def cookieWithOrFail[R, E, A](name: String)(missingCookieError: E)(f: Cookie => ZIO[R, E, A])(implicit trace: Trace, ): ZIO[R, E, A] = - cookieWithOrFailImpl(name)(_ => missingCookieError)(f) + cookieWithOrFailImpl(name)(missingCookieError)(f) - private def cookieWithOrFailImpl[R, E, A](name: String)(e: Throwable => E)(f: Cookie => ZIO[R, E, A])(implicit + private def cookieWithOrFailImpl[R, E, A](name: String)(e: E)(f: Cookie => ZIO[R, E, A])(implicit trace: Trace, - ): ZIO[R, E, A] = - ZIO.getOrFailWith(e(new java.util.NoSuchElementException(s"cookie doesn't exist: $name")))(cookie(name)).flatMap(f) + ): ZIO[R, E, A] = { + cookie(name) match { + case Some(value) => f(value) + case None => ZIO.fail(e) + } + } /** * Returns all cookies from the request. From 1c8f53b2f036ff63c491434d27cac67e69fa8f98 Mon Sep 17 00:00:00 2001 From: Thomas Hoefer Date: Tue, 26 Sep 2023 16:09:24 +0200 Subject: [PATCH 6/6] better docs --- zio-http/src/main/scala/zio/http/Request.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zio-http/src/main/scala/zio/http/Request.scala b/zio-http/src/main/scala/zio/http/Request.scala index fefd0150f3..b258a4d180 100644 --- a/zio-http/src/main/scala/zio/http/Request.scala +++ b/zio-http/src/main/scala/zio/http/Request.scala @@ -126,7 +126,7 @@ final case class Request( * Uses the cookie with the given name if it exists and runs `f` with the * cookie afterwards. * - * Also, you can set a custom failure value from an absent cookie with `E`. + * Also, you can set a custom failure value from a missing cookie with `E`. */ def cookieWithOrFail[R, E, A](name: String)(missingCookieError: E)(f: Cookie => ZIO[R, E, A])(implicit trace: Trace,