Skip to content

Commit

Permalink
#46: single-set-cookie support
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Mar 9, 2019
1 parent c5b3157 commit fc571a4
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 12 deletions.
6 changes: 5 additions & 1 deletion core/src/main/scala/tapir/Tapir.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.nio.charset.{Charset, StandardCharsets}
import tapir.Codec.PlainCodec
import tapir.CodecForMany.PlainCodecForMany
import tapir.CodecForOptional.PlainCodecForOptional
import tapir.model.{SetCookie, Cookie}
import tapir.model.{Cookie, SetCookie, SetCookieValue}

trait Tapir {
implicit def stringToPath(s: String): EndpointInput[Unit] = EndpointInput.PathSegment(s)
Expand All @@ -27,6 +27,10 @@ trait Tapir {
def cookie[T: PlainCodecForOptional](name: String): EndpointInput.Cookie[T] =
EndpointInput.Cookie(name, implicitly[PlainCodecForOptional[T]], EndpointIO.Info.empty)
def cookies: EndpointIO.Header[List[Cookie]] = header[List[Cookie]](Cookie.HeaderName)
def setCookie(name: String): EndpointIO.Header[SetCookieValue] = {
implicit val codec: Codec[SetCookieValue, MediaType.TextPlain, String] = SetCookieValue.setCookieValueCodec(name)
header[SetCookieValue](SetCookie.HeaderName)
}
def setCookies: EndpointIO.Header[List[SetCookie]] = header[List[SetCookie]](SetCookie.HeaderName)

def body[T, M <: MediaType](implicit tm: CodecForOptional[T, M, _]): EndpointIO.Body[T, M, _] =
Expand Down
44 changes: 35 additions & 9 deletions core/src/main/scala/tapir/model/SetCookie.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,25 @@ case class SetCookie(name: String,
httpOnly: Boolean = false) {

def toHeaderValue: String = {
val hc = new HttpCookie(name, value)
maxAge.foreach(ma => hc.setMaxAge(ma))
domain.foreach(hc.setDomain)
path.foreach(hc.setPath)
if (secure) hc.setSecure(true)
if (httpOnly) hc.setHttpOnly(true)
hc.toString
var r = s"$name=$value"
maxAge.foreach(v => r += s"; Max-Age=$v")
domain.foreach(v => r += s"; Domain=$v")
path.foreach(v => r += s"; Path=$v")
if (secure) r += "; Secure"
if (httpOnly) r += "; HttpOnly"
r
}

def toSetCookieValue: SetCookieValue = SetCookieValue(value, maxAge, domain, path, secure, httpOnly)
}

object SetCookie {
val HeaderName = "Set-Cookie"

implicit val setCookieCodec: Codec[List[SetCookie], MediaType.TextPlain, String] =
implicit val setCookiesCodec: Codec[List[SetCookie], MediaType.TextPlain, String] =
implicitly[Codec[String, MediaType.TextPlain, String]].mapDecode(parse)(cs => cs.map(_.toHeaderValue).mkString(", "))

implicit val setCookieCodecForMany: CodecForMany[List[SetCookie], MediaType.TextPlain, String] =
implicit val setCookiesCodecForMany: CodecForMany[List[SetCookie], MediaType.TextPlain, String] =
implicitly[CodecForMany[List[List[SetCookie]], MediaType.TextPlain, String]].map(_.flatten)(_.map(List(_)))

def parse(h: String): DecodeResult[List[SetCookie]] = {
Expand All @@ -57,6 +59,30 @@ object SetCookie {
}
}

case class SetCookieValue(value: String,
maxAge: Option[Long] = None,
domain: Option[String] = None,
path: Option[String] = None,
secure: Boolean = false,
httpOnly: Boolean = false) {
def toSetCookie(name: String): SetCookie = SetCookie(name, value, maxAge, domain, path, secure, httpOnly)
}

object SetCookieValue {
implicit def setCookieValueCodec(name: String): Codec[SetCookieValue, MediaType.TextPlain, String] = {
implicitly[Codec[String, MediaType.TextPlain, String]]
.mapDecode(SetCookie.parse(_).flatMap(findNamed(name)).map(_.toSetCookieValue))(cv => cv.toSetCookie(name).toHeaderValue)
}

private def findNamed(name: String)(cs: List[SetCookie]): DecodeResult[SetCookie] = {
cs.filter(_.name == name) match {
case Nil => DecodeResult.Missing
case List(c) => DecodeResult.Value(c)
case l => DecodeResult.Multiple(l.map(_.toHeaderValue))
}
}
}

case class Cookie(name: String, value: String) {
def toHeaderValue: String = s"$name=$value"
}
Expand Down
1 change: 1 addition & 0 deletions doc/endpoint/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ These are:
* `headers` captures all headers, represented as `Seq[(String, String)]`
* `cookie[T](name)` captures a cookie from the `Cookie` header with the given name
* `cookies` captures cookies from the `Cookie` header and represents them as `List[Cookie]`
* `setCookie(name)` captures the value & metadata of the a `Set-Cookie` header with a matching name
* `setCookies` captures cookies from the `Set-Cookie` header and represents them as `List[SetCookie]`
* `body[T, M]`, `stringBody`, `plainBody[T]`, `jsonBody[T]`, `binaryBody[T]`, `formBody[T]`, `multipartBody[T]`
captures the body
Expand Down
11 changes: 10 additions & 1 deletion server/tests/src/main/scala/tapir/server/tests/ServerTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import cats.implicits._
import com.softwaremill.sttp._
import com.softwaremill.sttp.asynchttpclient.cats.AsyncHttpClientCatsBackend
import org.scalatest.{Assertion, BeforeAndAfterAll, FunSuite, Matchers}
import tapir.model.{MultiQueryParams, Part, UsernamePassword}
import tapir.model.{MultiQueryParams, Part, SetCookieValue, UsernamePassword}
import tapir.server.{ServerDefaults, StatusMapper}
import tapir.tests.TestUtil._
import tapir.tests._
Expand Down Expand Up @@ -251,6 +251,15 @@ trait ServerTests[R[_], S, ROUTE] extends FunSuite with Matchers with BeforeAndA
}
}

testServer(
in_set_cookie_value_out_set_cookie_value,
(c: SetCookieValue) => pureResult(c.copy(value = c.value.reverse).asRight[Unit])
) { baseUri =>
sttp.get(uri"$baseUri/api/echo/headers").header("Set-Cookie", "c1=xy; HttpOnly; Path=/").send().map { r =>
r.cookies.toList shouldBe List(com.softwaremill.sttp.Cookie("c1", "yx", None, None, None, Some("/"), secure = false, httpOnly = true))
}
}

// path matching

testServer(endpoint, () => pureResult(Either.right[Unit, Unit](())), "no path should match anything") { baseUri =>
Expand Down
5 changes: 4 additions & 1 deletion tests/src/main/scala/tapir/tests/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.nio.ByteBuffer
import io.circe.generic.auto._
import tapir.json.circe._
import com.softwaremill.macwire._
import tapir.model.{SetCookie, Cookie, MultiQueryParams, UsernamePassword}
import tapir.model._

import scala.io.Source

Expand Down Expand Up @@ -105,6 +105,9 @@ package object tests {
val in_cookies_out_cookies: Endpoint[List[Cookie], Unit, List[SetCookie], Nothing] =
endpoint.get.in("api" / "echo" / "headers").in(cookies).out(setCookies)

val in_set_cookie_value_out_set_cookie_value: Endpoint[SetCookieValue, Unit, SetCookieValue, Nothing] =
endpoint.get.in("api" / "echo" / "headers").in(setCookie("c1")).out(setCookie("c1"))

val in_root_path: Endpoint[Unit, Unit, Unit, Nothing] = endpoint.get.in("")

val in_single_path: Endpoint[Unit, Unit, Unit, Nothing] = endpoint.get.in("api")
Expand Down

0 comments on commit fc571a4

Please sign in to comment.