Skip to content

Commit

Permalink
Updated Response[A] to support OrNotFound, OrElse and Sum combinators
Browse files Browse the repository at this point in the history
  • Loading branch information
vivekragunathan authored and noelwelsh committed Feb 1, 2024
1 parent 215c71c commit c6f224e
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 68 deletions.
17 changes: 8 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import scala.sys.process._
import scala.sys.process.*
import laika.rewrite.link.LinkConfig
import laika.rewrite.link.ApiLinks
import laika.theme.Theme

Global / onChangedBuildSource := ReloadOnSourceChanges

ThisBuild / tlBaseVersion := "0.6" // your current series x.y
ThisBuild / tlBaseVersion := "0.7" // your current series x.y

ThisBuild / organization := "org.creativescala"
ThisBuild / organizationName := "Creative Scala"
Expand Down Expand Up @@ -71,16 +71,15 @@ lazy val commonSettings = Seq(
)
)

lazy val root = crossProject(JSPlatform, JVMPlatform)
lazy val krop = crossProject(JSPlatform, JVMPlatform)
.in(file("."))
.settings(moduleName := "krop")
lazy val rootJs =
root.js.aggregate(
core.js
)

lazy val kropJs =
krop.js.aggregate(core.js)

lazy val rootJvm =
root.jvm.aggregate(
krop.jvm.aggregate(
core.jvm,
examples,
unidocs
Expand Down Expand Up @@ -171,6 +170,6 @@ lazy val examples = project
// developers. If you don't set this, Krop runs in production mode.
run / javaOptions += "-Dkrop.mode=development",
run / fork := true,
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.4.11" % Runtime
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.4.12" % Runtime
)
.dependsOn(core.jvm)
14 changes: 7 additions & 7 deletions core/shared/src/main/scala/krop/route/Param.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ sealed abstract class Param[A] extends Product, Serializable {
/** Gets a human-readable description of this `Param`. */
def describe: String =
this match {
case All(name, _, _) => s"${name}*"
case All(name, _, _) => s"$name*"
case One(name, _, _) => name
}

Expand Down Expand Up @@ -108,21 +108,21 @@ object Param {
* convert from A to B and B to A.
*/
def imap[B](f: A => B)(g: B => A): One[B] =
One(name, v => parse(v).map(f), b => unparse(g(b)))
One(name, parse(_).map(f), g.andThen(unparse))
}

/** A `Param` that matches a single `Int` parameter */
val int: Param.One[Int] =
Param.One("<Int>", str => Try(str.toInt), i => i.toString)
Param.One("<Int>", str => Try(str.toInt), _.toString)

/** A `Param` that matches a single `String` parameter */
val string: Param.One[String] =
Param.One("<String>", str => Success(str), identity)
Param.One("<String>", Success(_), identity)

/** `Param` that simply accumulates all parameters as a `Seq[String]`.
*/
val seq: Param.All[Seq[String]] =
Param.All("<String>", seq => Success(seq), identity)
Param.All("<String>", Success(_), identity)

/** `Param` that matches all parameters and converts them to a `String` by
* adding `separator` between matched elements. The inverse splits on this
Expand All @@ -141,7 +141,7 @@ object Param {
def lift[A](one: One[A]): Param.All[Seq[A]] =
Param.All(
one.name,
seq => seq.traverse(one.parse),
as => as.map(one.unparse)
_.traverse(one.parse),
_.map(one.unparse)
)
}
29 changes: 13 additions & 16 deletions core/shared/src/main/scala/krop/route/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ package krop.route

import cats.syntax.all.*
import krop.route
import krop.route.Param.All
import krop.route.Param.One
import krop.route.Param.{All, One}
import org.http4s.Uri
import org.http4s.Uri.{Path as UriPath}
import org.http4s.Uri.Path as UriPath

import scala.annotation.tailrec
import scala.collection.mutable
import scala.compiletime.constValue
import scala.util.Failure
import scala.util.Success
import scala.util.{Failure, Success}

/** A [[krop.route.Path]] represents a pattern to match against the path
* component of the URI of a request.`Paths` are created starting with
Expand Down Expand Up @@ -102,6 +101,7 @@ final class Path[P <: Tuple, Q] private (
def pathTo(params: P): String = {
val paramsArray = params.toArray

@tailrec
def loop(
idx: Int,
segments: Vector[Segment | Param[?]],
Expand Down Expand Up @@ -146,7 +146,7 @@ final class Path[P <: Tuple, Q] private (
}
.mkString("&")

s"pathTo(params)?${qParams}"
s"pathTo(params)?$qParams"
}

/** Optionally extract the captured parts of the URI's path. */
Expand All @@ -161,26 +161,23 @@ final class Path[P <: Tuple, Q] private (
} else {
matchSegments.head match {
case Segment.One(value) =>
if !pathSegments.isEmpty && pathSegments(0).decoded() == value then
if pathSegments.nonEmpty && pathSegments(0).decoded() == value then
loop(matchSegments.tail, pathSegments.tail)
else None

case Segment.All =>
Some(EmptyTuple)
case Segment.All => Some(EmptyTuple)

case Param.One(_, parse, _) =>
if !pathSegments.isEmpty then {
val raw = pathSegments(0).decoded()
val attempt = parse(raw)
attempt match {
if pathSegments.isEmpty then None
else
parse(pathSegments(0).decoded()) match {
case Failure(_) => None
case Success(value) =>
loop(matchSegments.tail, pathSegments.tail) match {
case None => None
case Some(tail) => Some(value *: tail)
}
}
} else None

case Param.All(_, parse, _) =>
parse(pathSegments.map(_.decoded())) match {
Expand Down Expand Up @@ -216,7 +213,7 @@ final class Path[P <: Tuple, Q] private (

val q = query.describe

if q.isEmpty then p else s"${p}?${q}"
if q.isEmpty then p else s"$p?$q"
}

def /(segment: String): Path[P, Q] = {
Expand Down Expand Up @@ -254,5 +251,5 @@ final class Path[P <: Tuple, Q] private (
)
}
object Path {
val root = Path[EmptyTuple, Unit](Vector.empty, Query.empty, true)
final val root = Path[EmptyTuple, Unit](Vector.empty, Query.empty, true)
}
2 changes: 1 addition & 1 deletion core/shared/src/main/scala/krop/route/Request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import cats.syntax.all.*
import org.http4s.EntityDecoder
import org.http4s.Media
import org.http4s.Method
import org.http4s.{Request as Http4sRequest}
import org.http4s.Request as Http4sRequest

/** A [[krop.route.Request]] describes a pattern within a [[org.http4s.Request]]
* that will be routed to a handler. For example, it can look for a particular
Expand Down
96 changes: 61 additions & 35 deletions core/shared/src/main/scala/krop/route/Response.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,81 @@
package krop.route

import cats.effect.IO
import fs2.io.file.{Path as Fs2Path}
import fs2.io.file.Path as Fs2Path
import krop.KropRuntime
import org.http4s.StaticFile
import org.http4s.Status
import org.http4s.dsl.io.*
import org.http4s.{Request as Http4sRequest}
import org.http4s.{Response as Http4sResponse}
import org.http4s.Request as Http4sRequest
import org.http4s.Response as Http4sResponse

/** A [[krop.route.Response]] produces a [[org.http4s.Response]] given a value
* of type A and a [[org.http4s.Request]].
*/
trait Response[A] {

/** Produce the [[org.http4s.Response]] given a request and the value of type
* A.
*/
sealed trait Response[A] {
def respond(request: Http4sRequest[IO], value: A)(using
runtime: KropRuntime
): IO[Http4sResponse[IO]]

def orNotFound: Response[Option[A]] =
Response.OrNotFound(this)

def orElse(that: Response[A]): Response[A] =
Response.OrElse(this, that)

def sum[O](that: Response[O]): Response[Either[A, O]] =
Response.Sum(this, that)
}

object Response {
case class StaticResource(pathPrefix: String) extends Response[String]:
/** Respond with a resource loaded by the Classloader. The `pathPrefix` is
* the prefix within the resources where the Classloader will look. E.g.
* "/krop/assets/". The `String` value is the rest of the resource name.
* E.g "krop.css".
*/
override def respond(request: Http4sRequest[IO], fileName: String)(using
runtime: KropRuntime
): IO[Http4sResponse[IO]] =
val path = pathPrefix ++ fileName
StaticFile
.fromResource(path, Some(request))
.getOrElseF(
IO(
runtime.logger.error(
s"Resource.staticResource couldn't load a resource from path $path."
)
) *> InternalServerError()
)

case class OrNotFound[Z](source: Response[Z]) extends Response[Option[Z]]:
override def respond(request: Http4sRequest[IO], value: Option[Z])(using
runtime: KropRuntime
): IO[Http4sResponse[IO]] =
value match
case Some(a) => source.respond(request, a)
case None => IO.pure(Http4sResponse.notFound)

case class OrElse[O](first: Response[O], second: Response[O])
extends Response[O]:
override def respond(request: Http4sRequest[IO], value: O)(using
runtime: KropRuntime
): IO[Http4sResponse[IO]] =
first
.respond(request, value)
.orElse(second.respond(request, value))

case class Sum[I, O](left: Response[I], right: Response[O])
extends Response[Either[I, O]]:
override def respond(request: Http4sRequest[IO], value: Either[I, O])(using
runtime: KropRuntime
): IO[Http4sResponse[IO]] =
value match
case Left(b) => left.respond(request, b)
case Right(a) => right.respond(request, a)

/** Respond with a resource loaded by the Classloader. The `pathPrefix` is the
* prefix within the resources where the Classloader will look. E.g.
* "/krop/assets/". The `String` value is the rest of the resource name. E.g
* "krop.css".
*/
def staticResource(pathPrefix: String): Response[String] =
new Response[String] {
def respond(
request: Http4sRequest[IO],
fileName: String
)(using
runtime: KropRuntime
): IO[Http4sResponse[IO]] = {
val path = pathPrefix ++ fileName
StaticFile
.fromResource(path, Some(request))
.getOrElseF(
IO(
runtime.logger.error(
s"""
|Resource.staticResource couldn't load a resource from path ${path}.
""".stripMargin
)
) *> InternalServerError()
)
}
}
StaticResource(pathPrefix)

/** Respond with a file loaded from the filesystem. The `pathPrefix` is the
* directory within the file system where the files will be found. E.g.
Expand Down Expand Up @@ -108,7 +134,7 @@ object Response {
runtime.logger
.error(
s"""
|Resource.staticFile couldn't load a file from path ${path}.
|Resource.staticFile couldn't load a file from path $path.
|
| This path represents ${p.absolute} as an absolute path.
| A file ${
Expand Down

0 comments on commit c6f224e

Please sign in to comment.