Skip to content

Commit

Permalink
Refactor: Re-write Path using Vector (#1260)
Browse files Browse the repository at this point in the history
* --wip-- [skip ci]

* implementation for apply

* trailing slash fix

* refactor: split path file

* test: restructure tests

* test: add failing test for Path

* refactor: re-write path using Vector

* refactor: remove LeadingSlash

* fix: encoding error in Http

* feat: implement `toString` using `Encode`

* fix: update cookie spec

Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
gciuloaica and tusharmath authored May 28, 2022
1 parent b30c68f commit cc17a70
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 250 deletions.
2 changes: 1 addition & 1 deletion example/src/main/scala/example/ConcreteEntity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object ConcreteEntity extends App {

val app: HttpApp[Any, Nothing] =
user
.contramap[Request](req => CreateUser(req.path.toString)) // Http[Any, Nothing, Request, UserCreated]
.contramap[Request](req => CreateUser(req.path.encode)) // Http[Any, Nothing, Request, UserCreated]
.map(userCreated => Response.text(userCreated.id.toString)) // Http[Any, Nothing, Request, Response]

// Run it like any simple app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CookieDecodeBenchmark {
val name = random.alphanumeric.take(100).mkString("")
val value = random.alphanumeric.take(100).mkString("")
val domain = random.alphanumeric.take(100).mkString("")
val path = Path((0 to 10).map { _ => random.alphanumeric.take(10).mkString("") }.mkString(""))
val path = Path.decode((0 to 10).map { _ => random.alphanumeric.take(10).mkString("") }.mkString(""))
val maxAge = random.nextLong()

private val cookie = Cookie(
Expand Down
8 changes: 4 additions & 4 deletions zio-http/src/main/scala/zhttp/http/Cookie.scala
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ final case class Cookie(
Some(s"$name=$c"),
expires.map(e => s"Expires=$e"),
maxAge.map(a => s"Max-Age=${a.toString}"),
domain.map(d => s"Domain=$d"),
path.map(p => s"Path=${p.encode}"),
domain.filter(_.nonEmpty).map(d => s"Domain=$d"),
path.filter(_.nonEmpty).map(p => s"Path=${p.encode}"),
if (isSecure) Some("Secure") else None,
if (isHttpOnly) Some("HttpOnly") else None,
sameSite.map(s => s"SameSite=${s.asString}"),
Expand Down Expand Up @@ -233,8 +233,8 @@ object Cookie {
domain = headerValue.substring(curr + 7, next)
} else if (headerValue.regionMatches(true, curr, fieldPath, 0, fieldPath.length)) {
val v = headerValue.substring(curr + 5, next)
if (!v.isEmpty) {
path = Path(v)
if (v.nonEmpty) {
path = Path.decode(v)
}
} else if (headerValue.regionMatches(true, curr, fieldSecure, 0, fieldSecure.length)) {
secure = true
Expand Down
2 changes: 1 addition & 1 deletion zio-http/src/main/scala/zhttp/http/Http.scala
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ object Http {
/**
* Applies Http based on the path
*/
def whenPathEq(p: Path): HttpApp[R, E] = http.whenPathEq(p.toString)
def whenPathEq(p: Path): HttpApp[R, E] = http.whenPathEq(p.encode)

/**
* Applies Http based on the path as string
Expand Down
56 changes: 56 additions & 0 deletions zio-http/src/main/scala/zhttp/http/Path.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package zhttp.http

final case class Path(segments: Vector[String], trailingSlash: Boolean) { self =>
def /(segment: String): Path = copy(segments :+ segment)

def /:(name: String): Path = copy(name +: segments)

def drop(n: Int): Path = copy(segments.drop(n))

def dropLast(n: Int): Path = copy(segments.reverse.drop(n))

def encode: String = {
val ss = segments.filter(_.nonEmpty).mkString("/")
ss match {
case "" if trailingSlash => "/"
case "" if !trailingSlash => ""
case ss => "/" + ss + (if (trailingSlash) "/" else "")
}
}

def initial: Path = copy(segments.init)

def isEmpty: Boolean = segments.isEmpty && !trailingSlash

def isEnd: Boolean = segments.isEmpty

def last: Option[String] = segments.lastOption

def nonEmpty: Boolean = !isEmpty

def reverse: Path = copy(segments.reverse)

def startsWith(other: Path): Boolean = segments.startsWith(other.segments)

def take(n: Int): Path = copy(segments.take(n))

def toList: List[String] = segments.toList

override def toString: String = encode
}

object Path {
val empty: Path = Path(Vector.empty, false)

/**
* Decodes a path string into a Path. Can fail if the path is invalid.
*/
def decode(path: String): Path = {
val segments = path.split("/").toVector.filter(_.nonEmpty)
segments.isEmpty match {
case true if path.endsWith("/") => Path(Vector.empty, true)
case true => Path(Vector.empty, false)
case _ => Path(segments, path.endsWith("/"))
}
}
}
101 changes: 0 additions & 101 deletions zio-http/src/main/scala/zhttp/http/PathModule.scala

This file was deleted.

28 changes: 28 additions & 0 deletions zio-http/src/main/scala/zhttp/http/PathSyntax.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package zhttp.http

private[zhttp] trait PathSyntax { module =>
val !! : Path = Path.empty

object /: {
def unapply(path: Path): Option[(String, Path)] = {
for {
head <- path.segments.headOption
tail = path.segments.drop(1)
} yield (head, path.copy(segments = tail))
}
}

object / {
def unapply(path: Path): Option[(Path, String)] = {
if (path.segments.length == 1) {
Some(!! -> path.segments.last)
} else if (path.segments.length >= 2) {
val init = path.segments.init
val last = path.segments.last
Some(path.copy(segments = init) -> last)
} else {
None
}
}
}
}
6 changes: 3 additions & 3 deletions zio-http/src/main/scala/zhttp/http/URL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ final case class URL(
def setPath(path: Path): URL =
copy(path = path)

def setPath(path: String): URL = copy(path = Path(path))
def setPath(path: String): URL = copy(path = Path.decode(path))

def setPort(port: Int): URL = {
val location = kind match {
Expand Down Expand Up @@ -133,12 +133,12 @@ object URL {
path <- Option(uri.getRawPath)
port = Option(uri.getPort).filter(_ != -1).getOrElse(portFromScheme(scheme))
connection = URL.Location.Absolute(scheme, host, port)
} yield URL(Path(path), connection, queryParams(uri.getRawQuery), Fragment.fromURI(uri))
} yield URL(Path.decode(path), connection, queryParams(uri.getRawQuery), Fragment.fromURI(uri))
}

private def fromRelativeURI(uri: URI): Option[URL] = for {
path <- Option(uri.getRawPath)
} yield URL(Path(path), Location.Relative, queryParams(uri.getRawQuery), Fragment.fromURI(uri))
} yield URL(Path.decode(path), Location.Relative, queryParams(uri.getRawQuery), Fragment.fromURI(uri))

private def portFromScheme(scheme: Scheme): Int = scheme match {
case Scheme.HTTP | Scheme.WS => 80
Expand Down
2 changes: 1 addition & 1 deletion zio-http/src/main/scala/zhttp/http/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import zio.ZIO

import java.nio.charset.Charset

package object http extends PathModule with RequestSyntax with RouteDecoderModule {
package object http extends PathSyntax with RequestSyntax with RouteDecoderModule {
type HttpApp[-R, +E] = Http[R, E, Request, Response]
type UHttpApp = HttpApp[Any, Nothing]
type RHttpApp[-R] = HttpApp[R, Throwable]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ private[zhttp] final class ServerResponseWriter[R](
}

def writeNotFound(jReq: HttpRequest)(implicit ctx: Ctx): Unit = {
val error = HttpError.NotFound(Path(jReq.uri()))
val error = HttpError.NotFound(Path.decode(jReq.uri()))
self.write(error, jReq)
}
}
Expand Down
6 changes: 3 additions & 3 deletions zio-http/src/test/scala/zhttp/http/CookieSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ object CookieSpec extends DefaultRunnableSpec {
suite("response cookies") {
testM("encode/decode signed/unsigned cookies with secret") {
check(HttpGen.cookies) { cookie =>
val cookieString = cookie.encode
assert(Cookie.decodeResponseCookie(cookieString, cookie.secret))(isSome(equalTo(cookie))) &&
assert(Cookie.decodeResponseCookie(cookieString, cookie.secret).map(_.encode))(isSome(equalTo(cookieString)))
val expected = cookie.encode
val actual = Cookie.decodeResponseCookie(expected, cookie.secret).map(_.encode)
assert(actual)(isSome(equalTo(expected)))
}
}
} +
Expand Down
Loading

0 comments on commit cc17a70

Please sign in to comment.