Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions asset/src/main/scala/krop/asset/AssetRoute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import krop.route.BaseRoute
import krop.route.ClientRoute
import krop.route.Handler
import krop.route.InternalRoute
import krop.route.Param
import krop.route.Params
import krop.route.Path
import krop.route.Request
import krop.route.Response
Expand All @@ -45,7 +45,7 @@ final class AssetRoute(base: Path[EmptyTuple, EmptyTuple], directory: Fs2Path)
val request
: Request[Tuple1[Fs2Path], Tuple1[Fs2Path], EmptyTuple, Tuple1[Fs2Path]] =
Request.get(
base / Param
base / Params
.separatedString("/")
.imap(str => Fs2Path(str))(path => path.toString)
)
Expand Down
1 change: 1 addition & 0 deletions core/jvm/src/main/scala/krop/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ object all {
export krop.route.QueryParam
export krop.route.Path
export krop.route.Param
export krop.route.Params
export krop.route.Segment

export krop.tool.DefaultAssets
Expand Down
40 changes: 20 additions & 20 deletions core/jvm/src/test/scala/krop/route/ParamSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,34 @@ import fs2.io.file.Path as Fs2Path
import munit.FunSuite

class ParamSuite extends FunSuite {
def paramOneDecodesValid[A](param: Param.One[A], values: Seq[(String, A)])(
using munit.Location
def paramDecodesValid[A](param: Param[A], values: Seq[(String, A)])(using
munit.Location
) =
values.foreach { case (str, a) =>
assertEquals(param.decode(str), Right(a))
}

def paramOneDecodesInvalid[A](param: Param.One[A], values: Seq[String])(using
def paramDecodesInvalid[A](param: Param[A], values: Seq[String])(using
munit.Location
) =
values.foreach { (str) => assert(param.decode(str).isLeft) }

def paramAllDecodesValid[A](
param: Param.All[A],
def paramsDecodesValid[A](
params: Params[A],
values: Seq[(Seq[String], A)]
)(using
munit.Location
) =
values.foreach { case (strings, a) =>
assertEquals(param.decode(strings), Right(a))
assertEquals(params.decode(strings), Right(a))
}

test("Param.one decodes valid parameter") {
paramOneDecodesValid(
test("Param decodes valid parameter") {
paramDecodesValid(
Param.int,
Seq(("1" -> 1), ("42" -> 42), ("-10" -> -10))
)
paramOneDecodesValid(
paramDecodesValid(
Param.string,
Seq(
("a" -> "a"),
Expand All @@ -57,29 +57,29 @@ class ParamSuite extends FunSuite {
)
}

test("Param.one fails to decode invalid parameter") {
paramOneDecodesInvalid(Param.int, Seq("a", " ", "xyz"))
test("Param fails to decode invalid parameter") {
paramDecodesInvalid(Param.int, Seq("a", " ", "xyz"))
}

test("Param.all decodes valid parameters") {
paramAllDecodesValid(
Param.seq,
test("Params decodes valid parameters") {
paramsDecodesValid(
Params.seq,
Seq(Seq() -> Seq(), Seq("a", "b", "c") -> Seq("a", "b", "c"))
)
paramAllDecodesValid(
Param.separatedString(","),
paramsDecodesValid(
Params.separatedString(","),
Seq(Seq() -> "", Seq("a") -> "a", Seq("a", "b", "c") -> "a,b,c")
)
paramAllDecodesValid(
Param.all[Int],
paramsDecodesValid(
Params.all[Int],
Seq(
Seq() -> Seq(),
Seq("1") -> Seq(1),
Seq("1", "2", "3") -> Seq(1, 2, 3)
)
)
paramAllDecodesValid(
Param.fs2Path,
paramsDecodesValid(
Params.fs2Path,
Seq(
Seq() -> Fs2Path(""),
Seq("a") -> Fs2Path("a"),
Expand Down
2 changes: 1 addition & 1 deletion core/jvm/src/test/scala/krop/route/PathParseSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.http4s.implicits.*
class PathParseSuite extends FunSuite {
val nonCapturingPath = Path / "user" / "create"
val nonCapturingAllPath = Path / "assets" / "html" / Segment.all
val capturingAllPath = Path / "assets" / "html" / Param.seq
val capturingAllPath = Path / "assets" / "html" / Params.seq
val simplePath = Path / "user" / Param.int.withName("<userId>") / "view"
val simpleQueryPath = Path / "user" / Param.int :? Query[String]("mode")
val multipleQueryPath =
Expand Down
2 changes: 1 addition & 1 deletion core/jvm/src/test/scala/krop/route/PathUnparseSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PathUnparseSuite extends FunSuite {
val rootPath = Path.root
val nonCapturingPath = Path / "user" / "create"
val nonCapturingAllPath = Path / "assets" / "html" / Segment.all
val capturingAllPath = Path / "assets" / "html" / Param.seq
val capturingAllPath = Path / "assets" / "html" / Params.seq
val simplePath = Path / "user" / Param.int.withName("<userId>") / "view"
val pathWithQuery = Path / "user" / "view" :? Query[Int]("id")

Expand Down
111 changes: 18 additions & 93 deletions core/shared/src/main/scala/krop/route/Param.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,22 @@

package krop.route

import fs2.io.file.Path as Fs2Path

/** A [[package.Param]] is used to extract values from a URI's path or query
/** A [[Param]] is used to extract a single value from a URI's path or query
* parameters.
*
* Params can also be inverted, going from a value of type `A` to a `String` or
* sequence of `String`. This allows so-called reverse routing, constructing a
* URI from the parameters.
*
* There are two types of `Param`:
*
* * those that handle a single value (`Param.One`); and
* A `Param` can also be inverted, going from a value of type `A` to a
* `String`. This allows so-called reverse routing, constructing a URI from the
* parameters.
*
* * those that handle as many values as are available (`Param.All`).
* To extract all remaining path segments as a sequence, use [[Params]].
*/
sealed abstract class Param[A] extends Product, Serializable {
import Param.*
final case class Param[A](codec: StringCodec[A]) {
export codec.{decode, encode}

/** Gets the name of this `Param`. By convention it describes the type within
* angle brackets.
*/
def name: String =
this match {
case All(codec) => codec.name
case One(codec) => codec.name
}
def name: String = codec.name

/** Create a `Param` with a more informative name. For example, you might use
* this method to note that an Int is in fact a user id.
Expand All @@ -50,85 +40,20 @@ sealed abstract class Param[A] extends Product, Serializable {
* Param.int.withName("<UserId>")
* ```
*/
def withName(name: String): Param[A] =
this match {
case All(codec) => All(codec.withName(name))
case One(codec) => One(codec.withName(name))
}
def withName(name: String): Param[A] = Param(codec.withName(name))

/** Construct a `Param[B]` from a `Param[A]` using functions to convert from A
* to B and B to A.
*/
def imap[B](f: A => B)(g: B => A): Param[B] = Param(codec.imap(f)(g))
}
object Param {
/* A `Param` that transforms a sequence of `String` to a value of type `A`.
*
* @param name
* The name used when printing this `Param`. Usually a short word in angle
* brackets, like "<Int>" or "<String>".
* @param codec
* The [[SeqStringCodec]] that does encoding and decoding
*/
final case class All[A](codec: SeqStringCodec[A]) extends Param[A] {
export codec.{decode, encode}

/** Construct a `Param.All[B]` from a `Param.All[A]` using functions to
* convert from A to B and B to A.
*/
def imap[B](f: A => B)(g: B => A): All[B] =
All(codec.imap(f)(g))
}

/* A `Param` that matches a single parameter.
*
* @param name
* The name used when printing this `Param`. Usually a short word in angle
* brackets, like "<Int>" or "<String>".
* @param codec
* The [[StringCodec]] that does encoding and decoding
*/
final case class One[A](codec: StringCodec[A]) extends Param[A] {
export codec.{decode, encode}

/** Construct a `Param.One[B]` from a `Param.One[A]` using functions to
* convert from A to B and B to A.
*/
def imap[B](f: A => B)(g: B => A): One[B] =
One(codec.imap(f)(g))
}

/** A `Param` that matches a single `Int` parameter */
val int: Param.One[Int] =
Param.One(StringCodec.int)
val int: Param[Int] =
Param(StringCodec.int)

/** A `Param` that matches a single `String` parameter */
val string: Param.One[String] =
Param.One(StringCodec.string)

/** `Param` that simply accumulates all parameters as a `Seq[String]`.
*/
val seq: Param.All[Seq[String]] =
Param.All(SeqStringCodec.seqString)

/** `Param` that converts path segments to a `fs2.io.file.Path`.
*/
val fs2Path: Param.All[Fs2Path] =
Param
.separatedString("/")
.imap(Fs2Path.apply)(_.toString)

/** Constructs a [[Param]] that decodes input into a `String` by appending all
* the input together with `separator` inbetween each element. Encodes data
* by splitting on `separator`.
*
* For example,
*
* ```scala
* val slash = Param.separatedString("/")
* ```
*
* decodes `Seq("a", "b", "c")` to `"a/b/c"` and encodes `"a/b/c"` as
* `Seq("a", "b", "c")`.
*/
def separatedString(separator: String): Param.All[String] =
Param.All(SeqStringCodec.separatedString(separator))

def all[A](using codec: StringCodec[A]): Param.All[Seq[A]] =
Param.All(SeqStringCodec.all(using codec))
val string: Param[String] =
Param(StringCodec.string)
}
76 changes: 76 additions & 0 deletions core/shared/src/main/scala/krop/route/Params.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2023 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package krop.route

import fs2.io.file.Path as Fs2Path

/** A [[Params]] is used to extract as many values as are available from a URI's
* path segments.
*
* A `Params` can also be inverted, going from a value of type `A` to a
* sequence of `String`. This allows so-called reverse routing, constructing a
* URI from the parameters.
*
* To extract a single value from a path segment, use [[Param]].
*/
final case class Params[A](codec: SeqStringCodec[A]) {
export codec.{decode, encode}

/** Gets the name of this `Params`. By convention it describes the type within
* angle brackets.
*/
def name: String = codec.name

/** Create a `Params` with a more informative name. */
def withName(name: String): Params[A] = Params(codec.withName(name))

/** Construct a `Params[B]` from a `Params[A]` using functions to convert from
* A to B and B to A.
*/
def imap[B](f: A => B)(g: B => A): Params[B] = Params(codec.imap(f)(g))
}
object Params {

/** `Params` that simply accumulates all parameters as a `Seq[String]`. */
val seq: Params[Seq[String]] =
Params(SeqStringCodec.seqString)

/** `Params` that converts path segments to a `fs2.io.file.Path`. */
val fs2Path: Params[Fs2Path] =
Params
.separatedString("/")
.imap(Fs2Path.apply)(_.toString)

/** Constructs a [[Params]] that decodes input into a `String` by appending
* all the input together with `separator` inbetween each element. Encodes
* data by splitting on `separator`.
*
* For example,
*
* ```scala
* val slash = Params.separatedString("/")
* ```
*
* decodes `Seq("a", "b", "c")` to `"a/b/c"` and encodes `"a/b/c"` as
* `Seq("a", "b", "c")`.
*/
def separatedString(separator: String): Params[String] =
Params(SeqStringCodec.separatedString(separator))

def all[A](using codec: StringCodec[A]): Params[Seq[A]] =
Params(SeqStringCodec.all(using codec))
}
Loading
Loading